From 66346f164c46b7d04321f396e3f76445ca5778d3 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 12:07:38 +0200 Subject: [PATCH 01/23] =?UTF-8?q?=F0=9F=94=A5=20Removed=20all=20`validit-v?= =?UTF-8?q?1`=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/README.md | 9 - examples/example1/example1.json | 47 ---- examples/example1/example1.py | 21 -- examples/example1/example1.toml | 29 --- examples/example1/example1.yaml | 34 --- examples/isodates/isodates.py | 11 - examples/isodates/isodates.toml | 11 - examples/isodates/isodates.yaml | 8 - setup.py | 22 +- tests/test_container_dump.py | 156 -------------- tests/test_creation.py | 168 --------------- tests/test_examples.py | 120 ----------- tests/test_validate.py | 367 -------------------------------- validit/__init__.py | 31 +-- validit/containers.py | 107 ---------- validit/errors/__init__.py | 1 - validit/errors/errors.py | 127 ----------- validit/errors/managers.py | 56 ----- validit/errors/parsing.py | 74 ------- validit/exceptions.py | 17 -- validit/templates/__init__.py | 10 - validit/templates/base.py | 31 --- validit/templates/templates.py | 294 ------------------------- validit/utils.py | 54 ----- validit/validate.py | 183 ---------------- 25 files changed, 3 insertions(+), 1985 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/example1/example1.json delete mode 100644 examples/example1/example1.py delete mode 100644 examples/example1/example1.toml delete mode 100644 examples/example1/example1.yaml delete mode 100644 examples/isodates/isodates.py delete mode 100644 examples/isodates/isodates.toml delete mode 100644 examples/isodates/isodates.yaml delete mode 100644 tests/test_container_dump.py delete mode 100644 tests/test_creation.py delete mode 100644 tests/test_examples.py delete mode 100644 tests/test_validate.py delete mode 100644 validit/containers.py delete mode 100644 validit/errors/__init__.py delete mode 100644 validit/errors/errors.py delete mode 100644 validit/errors/managers.py delete mode 100644 validit/errors/parsing.py delete mode 100644 validit/exceptions.py delete mode 100644 validit/templates/__init__.py delete mode 100644 validit/templates/base.py delete mode 100644 validit/templates/templates.py delete mode 100644 validit/utils.py delete mode 100644 validit/validate.py diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 7a8db60..0000000 --- a/examples/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Examples - -This folder contains multiple examples of templates created with the *validit* -module. Each subfolder contains a `.py` file which stores a single variable -`__template__` that represents the template of the other files (`.json`, `.yaml`, -`.toml`) in the same subdirectory. - -The examples are part of the CI tests, which insures that they are valid examples -and that the templates are correct. diff --git a/examples/example1/example1.json b/examples/example1/example1.json deleted file mode 100644 index bce4334..0000000 --- a/examples/example1/example1.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "title": "Example1", - "owner": { - "name": "Alon Krymgand", - "dob": "2004-02-21T07:32:00Z" - }, - "database": { - "server": "192.168.1.1", - "ports": [ - 8001, - 8001, - 8002 - ], - "connection_max": 5000, - "enabled": true - }, - "clients": { - "data": [ - [ - "gamma", - "delta" - ], - [ - 1, - 2 - ] - ], - "hosts": [ - "alpha", - "omega" - ] - }, - "servers": [ - { - "ip": "10.0.0.1", - "dc": "eqdc10" - }, - { - "ip": "10.0.0.2", - "dc": "eqdc10" - } - ], - "format": { - "name": "JSON", - "owner": "Douglas Crockford" - } -} diff --git a/examples/example1/example1.py b/examples/example1/example1.py deleted file mode 100644 index ab624eb..0000000 --- a/examples/example1/example1.py +++ /dev/null @@ -1,21 +0,0 @@ -from validit import Template, TemplateList, TemplateDict - -__template__ = TemplateDict( - title=Template(str), - owner=TemplateDict( - name=Template(str), - dob=Template(str), - ), - database=TemplateDict( - server=Template(str), - ports=TemplateList(Template(int)), - connection_max=Template(int), - enabled=Template(bool), - ), - clients=TemplateDict( - data=TemplateList( - TemplateList(Template(str, int)), - ), - hosts=TemplateList(Template(str)), - ), -) diff --git a/examples/example1/example1.toml b/examples/example1/example1.toml deleted file mode 100644 index 4957e77..0000000 --- a/examples/example1/example1.toml +++ /dev/null @@ -1,29 +0,0 @@ -title = "Example1" - -[owner] -name = "Alon Krymgand" -dob = "2004-02-21T07:32:00Z" - -[database] -server = "192.168.1.1" -ports = [ 8001, 8001, 8002,] -connection_max = 5000 -enabled = true - -[clients] -data = [ [ "gamma", "delta",], [ 1, 2,],] -hosts = [ "alpha", "omega",] - -[[servers]] -ip = "10.0.0.1" -dc = "eqdc10" - -[[servers]] -ip = "10.0.0.2" -dc = "eqdc10" - -[format] - -name = "TOML" -owner = "Tom Preston-Werner" - diff --git a/examples/example1/example1.yaml b/examples/example1/example1.yaml deleted file mode 100644 index 6e60c02..0000000 --- a/examples/example1/example1.yaml +++ /dev/null @@ -1,34 +0,0 @@ -title: "Example1" - -owner: - name: Alon Krymgand - dob: "2004-02-21T07:32:00Z" - -database: - server: "192.168.1.1" - ports: - - 8001 - - 8001 - - 8002 - connection_max: 5000 - enabled: True - -clients: - data: - - [gamma, delta] - - [1, 2] - - hosts: - - alpha - - omega - -servers: - - ip: "10.0.0.1" - dc: eqdc10 - - - ip: "10.0.0.2" - dc: eqdc10 - -format: - name: YAML - owner: Clark Evans diff --git a/examples/isodates/isodates.py b/examples/isodates/isodates.py deleted file mode 100644 index b7ade97..0000000 --- a/examples/isodates/isodates.py +++ /dev/null @@ -1,11 +0,0 @@ -from datetime import datetime, date -from validit import Template, TemplateDict, TemplateList - - -__template__ = TemplateDict( - posts=TemplateList(TemplateDict( - title=Template(str), - author=Template(str), - posted=Template(datetime, date), - )), -) diff --git a/examples/isodates/isodates.toml b/examples/isodates/isodates.toml deleted file mode 100644 index 7353e49..0000000 --- a/examples/isodates/isodates.toml +++ /dev/null @@ -1,11 +0,0 @@ -[[posts]] - -title = "Post #1" -author = "Alon Krymgand" -posted = 2021-06-16T11:58:37.032868 - -[[posts]] - -title = "Post From The Future" -author = "Elon Musk" -posted = 2069-04-20 diff --git a/examples/isodates/isodates.yaml b/examples/isodates/isodates.yaml deleted file mode 100644 index ef53b21..0000000 --- a/examples/isodates/isodates.yaml +++ /dev/null @@ -1,8 +0,0 @@ -posts: - - title: "Post #1" - author: Alon Krymgand - posted: 2021-06-16T11:58:37.032868 - - - title: Post From The Future - author: Elon Musk - posted: 2069-04-20 diff --git a/setup.py b/setup.py index b04401f..0d43ecc 100644 --- a/setup.py +++ b/setup.py @@ -7,11 +7,6 @@ def load_readme(): REQUIRES = ( - 'termcolor==1.1.0', - - # the dataclasses module is prebuilt into python>=3.7 - # For Python 3.6, it is supported using a backport - 'dataclasses; python_version < "3.7"', ) @@ -20,27 +15,12 @@ def load_readme(): 'pytest>=6.2, <6.3', 'flake8>=3.9, <3.10' ), - 'yaml': ( - 'pyyaml>=5.4, <5.5', - ), - 'toml': ( - 'toml>=0.10.2, <0.11', - ), } -EXTRAS['all'] = tuple( - package - for group in EXTRAS - if group != 'dev' - for package in EXTRAS[group] -) - -EXTRAS['dev'] += EXTRAS['all'] - setup( name='validit', description='Easily define and validate configuration file structures 📂🍒', - version='1.3.2', + version='2.0.0-dev', author='RealA10N', author_email='downtown2u@gmail.com', long_description=load_readme(), diff --git a/tests/test_container_dump.py b/tests/test_container_dump.py deleted file mode 100644 index f9d22e6..0000000 --- a/tests/test_container_dump.py +++ /dev/null @@ -1,156 +0,0 @@ -import pytest - -from validit.utils import DefaultValue -from validit.containers import HeadContainer - -from validit import ( - Template, - TemplateAny, - TemplateDict, - TemplateList, - Optional, -) - -cases = [ - { - 'name': 'string', - 'template': Template(str), - 'cases': ( - {'in': 'Hello', 'out': 'Hello'}, - {'in': str(), 'out': ''}, - {'in': DefaultValue, 'out': DefaultValue}, - ), - }, - { - 'name': 'any', - 'template': TemplateAny(), - 'cases': ( - {'in': None, 'out': None}, - {'in': DefaultValue, 'out': DefaultValue}, - {'in': [{'user': 'Alon', 'code': 123, 'another': True}], - 'out': [{'user': 'Alon', 'code': 123, 'another': True}]}, - ) - }, - { - 'name': 'dict', - 'template': TemplateDict(user=Template(str), code=Template(int)), - 'cases': ( - {'in': None, - 'out': None}, - {'in': 123, - 'out': 123}, - {'in': {}, - 'out': {}}, - {'in': {'user': 123}, - 'out': {'user': 123}}, - {'in': {'user': None}, - 'out': {'user': None}}, - {'in': {'user': DefaultValue}, - 'out': {}}, - {'in': {'user': 'A10N'}, - 'out': {'user': 'A10N'}}, - {'in': {'other': None}, - 'out': {}}, - {'in': {'other': 'A10N'}, - 'out': {}}, - {'in': {'user': 'A10N', 'other': 'A10N'}, - 'out': {'user': 'A10N'}}, - {'in': {'user': 123, 'code': 'A10N'}, - 'out': {'user': 123, 'code': 'A10N'}}, - {'in': {'user': 'A10N', 'code': 123}, - 'out': {'user': 'A10N', 'code': 123}}, - {'in': {'user': 'A10N', 'code': 123, 'other': None}, - 'out': {'user': 'A10N', 'code': 123}}, - ), - }, - { - 'name': 'optional-no-default', - 'template': Optional(Template(str)), - 'cases': ( - {'in': None, 'out': None}, - {'in': 'string', 'out': 'string'}, - {'in': DefaultValue, 'out': DefaultValue}, - ), - }, - { - 'name': 'optional-with-default', - 'template': Optional(Template(str), default='DEFAULT'), - 'cases': ( - {'in': None, 'out': None}, - {'in': 'string', 'out': 'string'}, - {'in': DefaultValue, 'out': 'DEFAULT'}, - ), - }, - { - 'name': 'optional-dict', - 'template': TemplateDict(name=Optional(Template(str), default='Unknown')), - 'cases': ( - {'in': {}, 'out': {'name': 'Unknown'}}, - {'in': {'age': 17}, 'out': {'name': 'Unknown'}}, - {'in': {'name': 'Alon'}, 'out': {'name': 'Alon'}}, - {'in': {'name': 'Alon', 'age': 17}, 'out': {'name': 'Alon'}}, - {'in': None, 'out': None}, - {'in': DefaultValue, 'out': DefaultValue}, - ), - }, - { - 'name': 'list', - 'template': TemplateList(Template(int, float)), - 'cases': ( - {'in': [], 'out': []}, - {'in': (), 'out': []}, - {'in': [1, 2.3, 4], 'out': [1, 2.3, 4]}, - {'in': (1.23,), 'out': [1.23]}, - {'in': [1, 'hello'], 'out': [1, 'hello']}, - # Even if value from wrong type, will dump the given data. - {'in': None, 'out': None}, - {'in': 'NotAList', 'out': 'NotAList'} - ), - }, - { - 'name': 'list-of-dicts', - 'template': TemplateList(TemplateDict(user=Template(str), code=Template(int))), - 'cases': ( - {'in': None, 'out': None}, - {'in': [], 'out': []}, - {'in': [{'user': 'Alon', 'code': 123, 'another': True}], - 'out': [{'user': 'Alon', 'code': 123}]}, - {'in': [{'yes': True}, {'no': False}], - 'out': [{}, {}]}, - {'in': ['hello', {'hi': 'hello'}], - 'out': ['hello', {}]}, - ) - }, -] - - -def generate_params(): - params = list() - - for test in cases: - for case in test['cases']: - params.append(pytest.param( - test['template'], - case['in'], - case['out'], - id=test.get('name'), - )) - - return params - - -@pytest.mark.parametrize('template, iin, out', generate_params()) -def test_dumps(template, iin, out): - - container = HeadContainer() - template.container_dump( - container=container, - data=iin - ) - - if container.data != out: - pytest.fail( - 'Container dump result unexpected\n' - f"expected: '{out}'\n" - f"got: '{container.data}'\n" - ) diff --git a/tests/test_creation.py b/tests/test_creation.py deleted file mode 100644 index ffa915c..0000000 --- a/tests/test_creation.py +++ /dev/null @@ -1,168 +0,0 @@ -""" Test the creation of a template structure. """ - -import pytest -from validit import Template, TemplateDict, TemplateList, Optional, Options -from validit.exceptions import InvalidTemplateConfiguration, InvalidDefaultValue - - -class ExampleObj: pass -class SonOfExample(ExampleObj): pass -class AnotherObj: pass - - -VALID_BASE_TYPES = ( - (ExampleObj,), - (int, float, complex), - (str, bool, AnotherObj), - (ExampleObj, SonOfExample, AnotherObj), -) - -INVALID_TYPES = ( - 1, str(), bool(), ExampleObj(), SonOfExample(), - (str, int()), - (bool, int, float, 'str'), - (AnotherObj, ExampleObj(), SonOfExample()), -) - - -class TestBaseTemplate: - - @pytest.mark.parametrize('template', VALID_BASE_TYPES) - def test_creation(self, template): - """ Test that a template constractor provided with one or more types - can be initialized without errors. """ - Template(*template) - - @pytest.mark.parametrize('template', INVALID_TYPES) - def test_creation_fails(self, template): - """ Test that a template of instance (and not a type) raises an - error. """ - with pytest.raises(InvalidTemplateConfiguration): - Template(template) - - -class TestTemplateOptional: - @pytest.mark.parametrize('template', VALID_BASE_TYPES) - def test_creation(self, template): - """ Test that a template constractor provided with one or more types - can be initialized without errors. """ - Optional(Template(*template)) - - @pytest.mark.parametrize('template', ( - Template(str), Template(int, str), - TemplateDict(user=Template(str), password=Template(int)), - TemplateList(Template(str)), - TemplateDict(user=Template(ExampleObj), password=Template(AnotherObj)), - Options('yes', 'no'), - )) - def test_complex_creation(self, template): - """ Test that a optional template constractor accepts advance templates - like dictionary template and list template. """ - Optional(template) - - @pytest.mark.parametrize('template', INVALID_TYPES) - def test_creation_fails(self, template): - """ Test that a template of instance (and not a type) raises an - error. """ - with pytest.raises(InvalidTemplateConfiguration): - Optional(template) - - @pytest.mark.parametrize('kwargs', ( - {'template': Template(str), 'default': 'string'}, - {'template': Template(str), 'default': ''}, - {'template': Template(int, float), 'default': 0}, - {'template': Template(int, float), 'default': 123}, - {'template': Template(int, float), 'default': 123.456}, - {'template': TemplateDict(name=Template(str)), - 'default': {'name': 'Alon'}}, - {'template': Template(ExampleObj), 'default': ExampleObj()}, - {'template': Options('yes', 'no'), 'default': 'no'}, - )) - def test_creation_default(self, kwargs): - Optional(**kwargs) - - @pytest.mark.parametrize('kwargs', ( - {'template': Template(str), 'default': None}, - {'template': Template(str), 'default': 123}, - {'template': Template(int), 'default': 1.23}, - {'template': TemplateDict(name=Template(str)), - 'default': {'name': 123}}, - {'template': TemplateDict(name=Template(str)), - 'default': {}}, - )) - def test_creation_invalid_default_value(self, kwargs): - with pytest.raises(InvalidDefaultValue): - Optional(**kwargs) - - -class TestTemplateDict: - - @pytest.mark.parametrize('template', ( - {'username': Template(str)}, - { - 'name': Template(str), - 'id': Template(int), - 'height': Template(int, float), - }, - )) - def test_creation(self, template): - TemplateDict(**template) - - @pytest.mark.parametrize('template', ( - {'typeNotTemplate': str}, - {'instance': 'hello!'}, - {'string': Template(str), - 'object': ExampleObj, - } - )) - def test_creation_fails(self, template): - with pytest.raises(InvalidTemplateConfiguration): - TemplateDict(**template) - - -class TestTemplateList: - - @pytest.mark.parametrize('template', ( - Template(int), - Template(str), - Template(int, float, complex), - Template(str, ExampleObj, AnotherObj), - TemplateDict(username=Template(str), secretcode=Template(int)), - TemplateList(Template(int)), - )) - def test_creation(self, template): - TemplateList(template) - - @pytest.mark.parametrize('template', ( - int, str, 'astring', ExampleObj, SonOfExample(), AnotherObj, - )) - def test_creation_fails(self, template): - with pytest.raises(InvalidTemplateConfiguration): - TemplateList(template) - - @pytest.mark.parametrize('types', VALID_BASE_TYPES) - @pytest.mark.parametrize('lengths', ( - range(10), range(82), range(20, 32, 3), - {1, 2}, [1, 2, 3], [i for i in range(10_000)] - )) - def test_length(self, types, lengths): - TemplateList(Template(*types), valid_lengths=lengths) - - @pytest.mark.parametrize('types', VALID_BASE_TYPES) - @pytest.mark.parametrize('lengths', ( - 10, 0, range, - )) - def test_length_fails(self, types, lengths): - with pytest.raises(InvalidTemplateConfiguration): - TemplateList(Template(*types), valid_lengths=lengths) - - -class TestOptions: - - @pytest.mark.parametrize('instances', ( - (1, 2, 3), - ('yes', 'no'), - (True, False, None), - )) - def test_creation(self, instances): - Options(*instances) diff --git a/tests/test_examples.py b/tests/test_examples.py deleted file mode 100644 index 445bddb..0000000 --- a/tests/test_examples.py +++ /dev/null @@ -1,120 +0,0 @@ -import typing -import os -import sys - -import pytest -from collections import namedtuple - -from validit import ( - Validate, - ValidateFromJSON, - ValidateFromYAML, - ValidateFromTOML, -) - -THIS = __file__ -HERE = os.path.dirname(THIS) -TOP = os.path.join(HERE, os.pardir) -EXAMPLES_FOLDER = os.path.join(TOP, 'examples') - -VALIDATOR_EXTENSIONS = { - '.json': ValidateFromJSON, - '.yaml': ValidateFromYAML, - '.toml': ValidateFromTOML, -} - -ExampleInfo = namedtuple('ExampleInfo', ['files', 'template']) - - -def to_validator(name: str): - for ext, validator in VALIDATOR_EXTENSIONS.items(): - if name.endswith(ext): - return validator - - return None - - -def get_example_files() -> typing.List[ExampleInfo]: - params = list() - - examples = [ - name - for name in os.listdir(EXAMPLES_FOLDER) - if os.path.isdir( - os.path.join(EXAMPLES_FOLDER, name) - ) - ] - - for name in examples: - folder = os.path.join(EXAMPLES_FOLDER, name) - - # Loading example files and corresponding validators - files = { - os.path.join(folder, file): to_validator(file) - for file in os.listdir(folder) - if to_validator(file) is not None - } - - # Loading the template - sys.path.append(folder) - pyfile = next( - name - for name in os.listdir(folder) - if name.endswith('.py') - ) - filename = os.path.splitext(pyfile)[0] - template = __import__(filename, fromlist=['__template__']) - - # Add the current example into the params list - params.append( - ExampleInfo( - files=files, - template=template.__template__ - ) - ) - - return params - - -@pytest.mark.parametrize('template, examples', [ - pytest.param( - info.template, - info.files, - ) - for info in get_example_files() -]) -def test_matching_example_data(template, examples): - - data = list() - for filepath, validator in examples.items(): - with open(filepath, 'r') as file: - data.append( - validator(template, file).data - ) - - if any(data[0] != ddata for ddata in data): - pytest.fail( - 'Data from different example files are not equal.' - ) - - -@pytest.mark.parametrize('template, filepath, validator', [ - pytest.param( - info.template, - filepath, - validator, - id=os.path.basename(filepath) - ) - for info in get_example_files() - for filepath, validator in info.files.items() -]) -def test_no_validation_errors(template, filepath, validator): - - with open(filepath, 'r') as file: - result: Validate = validator(template, file) - - if result.errors: - pytest.fail( - f'Found {result.errors.count} validation errors when expected none:\n' + - str(result.errors) - ) diff --git a/tests/test_validate.py b/tests/test_validate.py deleted file mode 100644 index 6a24a77..0000000 --- a/tests/test_validate.py +++ /dev/null @@ -1,367 +0,0 @@ -import pytest -import typing -from dataclasses import dataclass - -from validit import ( - Template, - TemplateList, - TemplateDict, - TemplateAny, - Optional, - Options, -) - -from validit.templates import BaseTemplate - -from validit.errors.managers import TemplateCheckRaiseOnError -from validit.errors import ( - TemplateCheckError, - TemplateCheckInvalidOptionError as InvalidOptionError, - TemplateCheckInvalidDataError as WrongTypeError, - TemplateCheckMissingDataError as MissingDataError, - TemplateCheckListLengthError as ListLengthError, -) - -from validit.utils import DefaultValue -from validit.containers import HeadContainer - - -class ExampleObj: ... # noqa: E701 -class AnotherObj: ... # noqa: E701 - - -@dataclass(frozen=True) -class Check: - data: typing.Any - error: typing.Type[TemplateCheckError] = None - msg: typing.Optional[str] = None - - -@dataclass(frozen=True) -class CheckGroup: - cases: typing.List[typing.Any] - error: typing.Type[TemplateCheckError] = None - - def to_checks(self,): - return [Check(data, self.error) for data in self.cases] - - -@dataclass(frozen=True) -class SingleTest: - template: BaseTemplate - check: Check - - def run(self,): - arguments = { - 'container': HeadContainer(self.check.data), - 'errors': TemplateCheckRaiseOnError(), - } - - if self.check.error is None: - self.template.validate(**arguments) - - else: - with pytest.raises(self.check.error): - self.template.validate(**arguments) - - -@dataclass(frozen=True) -class TemplateTest: - name: str - template: BaseTemplate - checks: typing.List[typing.Union[Check, CheckGroup]] - - def collect_checks(self,): - final = list() - for check in self.checks: - if isinstance(check, CheckGroup): - final.extend(check.to_checks()) - else: - final.append(check) - return final - - def to_single_tests(self,): - return [ - SingleTest(template=self.template, check=check) - for check in self.collect_checks() - ] - - -@dataclass(frozen=True) -class CollectionTest: - tests: typing.List[TemplateTest] - - def to_single_tests(self,): - return [ - single - for test in self.tests - for single in test.to_single_tests() - ] - - -tests = CollectionTest([ - TemplateTest( - name='simple', - template=Template(str, int), - checks=[ - CheckGroup([ - 'string', str(), int(), 123456, 0, - ]), - Check( - ExampleObj(), - error=WrongTypeError, - msg="Expected 'str' or 'int' but got 'ExampleObj'", - ), - Check( - DefaultValue, - error=MissingDataError, - msg='Missing required information', - ), - ] - ), - TemplateTest( - name='any', - template=TemplateAny(), - checks=[ - CheckGroup([ - None, - list(), - dict(), - 'test', - 123, - ExampleObj, - ExampleObj(), - ]), - Check(DefaultValue, MissingDataError), - ] - ), - TemplateTest( - name='list', - template=TemplateList(Template(str)), - checks=[ - CheckGroup([ - [], - tuple(), - ['hello', 'there!'], - ('a list', 'of strings!') * 100, - ]), - Check( - 123, - error=WrongTypeError, - msg="Expected 'list' or 'tuple' but got 'int'", - ), - CheckGroup([ - None, - set('hello'), - ['hello', ExampleObj()], - [None], - ], error=WrongTypeError) - ] - ), - TemplateTest( - name='dict', - template=TemplateDict( - username=Template(str), - code=Template(int) - ), - checks=[ - CheckGroup([ - {'username': 'RealA10N', 'code': 123}, - {'username': '', 'code': 0, ExampleObj: AnotherObj()}, - ]), - CheckGroup([ - {'username': 'RealA10N', 'code': '123'}, - {'code': 123, 'username': str}, - ], error=WrongTypeError), - CheckGroup([ - dict(), - {'code': 123}, - ], error=MissingDataError), - ], - ), - TemplateTest( - name='list-of-dicts', - template=TemplateList(TemplateDict( - username=Template(str), - code=Template(int), - )), - checks=[ - CheckGroup([ - list(), - [ - {'username': 'RealA10N', 'code': 123}, - {'username': 'elonmusk', 'code': 42069}, - ], - ]), - CheckGroup([ - {'username': 'RealA10N', 'code': 123}, - [ExampleObj()], - [ - {'username': 'RealA10N', 'code': 123}, - {'username': 'elonmusk', 'code': 12.34}, - ], - ], error=WrongTypeError), - CheckGroup([ - [{'username': 'RealA10N'}], - [ - {'username': 'RealA10N', 'code': 123}, - {'code': 456}, - ], - ], error=MissingDataError), - ], - ), - TemplateTest( - name='list-length-set', - template=TemplateList(Template(int, float), valid_lengths={1, 2, 3}), - checks=[ - CheckGroup([ - [21], - [1, 1.2], - [1.23, 123, 43], - ]), - CheckGroup([ - [], - [1, 2, 3, 4], - ], error=ListLengthError), - CheckGroup([ - [1, 'notnum'], - 'notlist', - ], error=WrongTypeError), - ], - ), - TemplateTest( - name='list-lengths-range', - template=TemplateList(Template(int), range(0, 100, 3)), - checks=[ - CheckGroup([ - [], - [1, 1, 1], - [0] * 12, - [0] * 51, - [0] * 99, - ]), - CheckGroup([ - [0], - [0] * 100, - [0] * 31, - ], error=ListLengthError), - ], - ), - TemplateTest( - name='optional', - template=TemplateDict( - username=Template(str), - nickname=Optional(Template(str)), - ), - checks=[ - CheckGroup([ - {'username': 'RealA10N', 'nickname': 'Alon'}, - {'username': 'RealA10N'}, - ]), - CheckGroup([ - {'username': 'RealA10N', 'nickname': 123}, - {'username': 123}, - ], error=WrongTypeError), - CheckGroup([ - {'nickname': 'Alon'}, - ], error=MissingDataError), - ], - ), - TemplateTest( - name='complex-optional', - template=TemplateList(TemplateDict( - username=Template(str), - realname=Optional(TemplateDict( - first=Template(str), - last=Template(str), - )), - )), - checks=[ - CheckGroup([ - list(), - [{'username': 'Alon'}], - [ - {'username': 'Alon'}, - {'username': 'A10N', 'realname': { - 'first': 'Alon', 'last': 'Krymgand'}}, - ], - ]), - CheckGroup([ - [{'username': 'A10N', 'realname': {'first': 'Alon'}}], - [{'username': 'A10N', 'realname': {'last': 'Krymgand'}}], - [ - {'username': 'Alon'}, - {'username': 'A10N', 'realname': {'last': 'Krymgand'}} - ], - [ - {'username': 'Alon'}, - {'username': 'A10N', 'realname': {'last': 'Krymgand'}}, - {'username': 'A10N', 'realname': { - 'first': 'Alon', 'last': 'Krymgand'}}, - ], - ], error=MissingDataError), - CheckGroup([ - None, dict(), - {'username': 'A10N'}, - [{'username': 123}], - [{'username': 'A10N', 'realname': {'first': 'Alon', 'last': 123}}], - ], error=WrongTypeError), - ] - ), - TemplateTest( - name='options-left-right', - template=Options('L', 'R'), - checks=[ - CheckGroup(['L', 'R']), - CheckGroup([ - 'r', - 'U', - None, - DefaultValue, - ExampleObj, - ExampleObj(), - ], error=InvalidOptionError), - Check( - 21, - error=InvalidOptionError, - msg="Expected 'L' or 'R' but got 21" - ), - ] - ), - TemplateTest( - name='optional-options', - template=Optional(Options('jpeg', 'png', 'gif'), default='gif'), - checks=[ - CheckGroup(['jpeg', 'png', 'gif']), - Check(DefaultValue), - CheckGroup(['hello', 123, None], error=InvalidOptionError) - ], - ), -]) - - -@pytest.mark.parametrize('test', tests.to_single_tests()) -def test_check_first_error(test: SingleTest): - - arguments = { - 'container': HeadContainer(test.check.data), - 'errors': TemplateCheckRaiseOnError(), - } - - if test.check.error is None: - test.template.validate(**arguments) - - else: - with pytest.raises(test.check.error) as einfo: - test.template.validate(**arguments) - - error: TemplateCheckError = einfo.value - if test.check.msg is not None and error.msg != test.check.msg: - pytest.fail( - '\n'.join([ - 'Expected message:', - test.check.msg, - 'But instead got message:', - error.msg, - ]) - ) diff --git a/validit/__init__.py b/validit/__init__.py index 2c10cef..567416b 100644 --- a/validit/__init__.py +++ b/validit/__init__.py @@ -1,31 +1,4 @@ -from .templates import ( - Template, - TemplateAny, - TemplateDict, - TemplateList, - Optional, - Options, -) +__all__ = [] -from .validate import ( - Validate, - ValidateFromJSON, - ValidateFromYAML, - ValidateFromTOML, -) - -__all__ = [ - 'Template', - 'TemplateAny', - 'TemplateDict', - 'TemplateList', - 'Optional', - 'Options', - 'Validate', - 'ValidateFromJSON', - 'ValidateFromYAML', - 'ValidateFromTOML', -] - -__version__ = '1.3.2' +__version__ = '2.0.0-dev' __author__ = 'Alon Krymgand Osovsky' diff --git a/validit/containers.py b/validit/containers.py deleted file mode 100644 index d1b55b4..0000000 --- a/validit/containers.py +++ /dev/null @@ -1,107 +0,0 @@ -import typing -from abc import ABC, abstractmethod -from validit.utils import DefaultValue - - -class BaseContainer(ABC): - - @property - @abstractmethod - def data(self,): - """ Returns the data that the container holds. """ - - @data.setter - @abstractmethod - def data(self, value): - """ Sets the data inside the container. """ - - @property - @abstractmethod - def path(self,) -> typing.Tuple[typing.Union[str, int]]: - """ Returns a tuple that represents the path taken from the head container - to the current one. If the current container is the head container, will - return an empty tuple. """ - - def __getitem__(self, index): - """ Returns a new container instace that represents the data in from - the current container in the given index. """ - return Container(parent=self, chiled=index) - - def __iter__(self,): - return ContainerIterator(self) - - def __str__(self,) -> str: - """ Returns a string with the stored data string representation. """ - return f'Container<{self.data}>' - - -class ContainerIterator: - """ An iterator that loops over a container and yields its - container-children. """ - - def __init__(self, container: BaseContainer): - self.__container = container - - # By default, will use the iterator of the data to pass into __getitem__ - # This is the default behavior of a dictionray, because the iterator of - # a dict return its keys (and they are passed to __getitem__ to retrive - # values). Handling the special case of lists and tuples (which the - # iterator yields the values and not the indices) will be done separately. - - if isinstance(container.data, (list, tuple)): - self.__items = iter(range(len(container.data))) - - else: - self.__items = iter(container.data) - - def __next__(self,): - item = next(self.__items) - return self.__container[item] - - -class HeadContainer(BaseContainer): - """ The head container. This is the root of the tree, and only this instance - actually stores the data. All other instances just store pointers (in some - way or another) to a part of the data in this container. """ - - def __init__(self, data: typing.Any = DefaultValue): - self.__data = data - - @property - def data(self,): - return self.__data - - @data.setter - def data(self, value): - self.__data = value - - @property - def path(self,): - return () - - -class Container(BaseContainer): - """ A regular container that stores data. """ - - def __init__(self, parent: BaseContainer, chiled: typing.Union[str, int]): - self.__parent = parent - self.__chiled = chiled - - @property - def data(self,): - """ Returns the data that is stored in the container. If there is no - data in the container, returns the `DefaultValue` object. """ - - try: - return self.__parent.data[self.__chiled] - except LookupError: - # If data doesn't exist - return DefaultValue - - @data.setter - def data(self, value) -> None: - self.__parent.data[self.__chiled] = value - - @property - def path(self,): - return self.__parent.path + (self.__chiled,) diff --git a/validit/errors/__init__.py b/validit/errors/__init__.py deleted file mode 100644 index 0179aa8..0000000 --- a/validit/errors/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .errors import * diff --git a/validit/errors/errors.py b/validit/errors/errors.py deleted file mode 100644 index c1db9ec..0000000 --- a/validit/errors/errors.py +++ /dev/null @@ -1,127 +0,0 @@ -import typing -import re - -from termcolor import colored -from validit.containers import BaseContainer - - -def readable_list(items: typing.List[str]) -> str: - """ Converts a list of strings into a comma seperated list. """ - - if not items: - return str() - - body, end = items[:-1], items[-1] - s = ', '.join(repr(cur) for cur in body) - - if s: - s += ' or ' - - return s + repr(end) - - -class TemplateCheckError(Exception): - """ A general object that represents a template check error. - Although you can create instances of it, it is highly recommended to use - subclasses of it to better describe the check error. """ - - def __init__(self, - container: BaseContainer = None, - msg: str = None, - ) -> None: - self.container = container - self.msg = msg - - super().__init__(self.description) - - @property - def path(self,) -> typing.Tuple[str]: - """ A collection of strings that represents the path from the main data - to the area in which the current error occurred. """ - return self.container.path if self.container else () - - @property - def path_str(self,) -> str: - """ A string that represents the path where the template check error - occurred. """ - - return colored( - ''.join(f'[{element}]' for element in self.path), - 'yellow' - ) if self.path else '' - - @property - def description(self,) -> str: - """ Generates and returns a string that represents the current template - check error. """ - return colored(self.msg, 'red') if self.msg else '' - - def __str__(self,) -> str: - """ Generates and returns a colors string that represents the current - template check error. """ - - spacing = ' ' if self.description and self.path_str else '' - return f'{self.path_str}{spacing}{self.description}' - - @property - def no_color_str(self,) -> str: - """ A string non colored string that represents the current template - error. """ - return re.sub('\033\\[([0-9]+)(;[0-9]+)*m', '', str(self)) - - -class TemplateCheckMissingDataError(TemplateCheckError): - """ An object that represents a template check error in which some required - data is missing. """ - - def __init__(self, container: BaseContainer) -> None: - super().__init__(container, msg='Missing required information') - - -class TemplateCheckInvalidOptionError(TemplateCheckError): - - def __init__(self, - container: BaseContainer, - expected: tuple, - got: typing.Any, - ) -> None: - self.expected = expected - self.got = got - - super().__init__( - container, - msg=f"Expected {readable_list(expected)} but got {repr(got)}" - ) - - -class TemplateCheckInvalidDataError(TemplateCheckError): - """ An object that represents a template check error in which the expected - data was found, but it didn't follow the expected format / type. """ - - def __init__(self, - container: BaseContainer, - expected: typing.Tuple[type], - got: type - ) -> None: - self.expected = expected - self.got = got - - expected_str = readable_list([cls.__name__ for cls in expected]) - got_type = type(got) if not isinstance(got, type) else got - got_str = repr(got_type.__name__) - - super().__init__( - container, - msg=f"Expected {expected_str} but got {got_str}", - ) - - -class TemplateCheckListLengthError(TemplateCheckError): - """ An object that represents a template check error in which a given list - has an invalid length according to the template configuration. """ - - def __init__(self, container: BaseContainer, expected: typing.Any, got: int): - super().__init__( - container, - f'List length {got} is not {expected!r}', - ) diff --git a/validit/errors/managers.py b/validit/errors/managers.py deleted file mode 100644 index 68fea9c..0000000 --- a/validit/errors/managers.py +++ /dev/null @@ -1,56 +0,0 @@ -import typing -from abc import ABC, abstractmethod -from termcolor import colored - -from .errors import TemplateCheckError - - -class TemplateCheckErrorManager(ABC): - - @abstractmethod - def register_error(self, error: TemplateCheckError) -> None: - pass - - -class TemplateCheckErrorCollection(TemplateCheckErrorManager): - """ An object that collects errors and can display them to the user. """ - - def __init__(self): - self.errors = list() - - def __iter__(self,) -> typing.Iterator[TemplateCheckError]: - return (error for error in self.errors) - - def __len__(self,) -> int: - """ Returns the number of registered errors """ - return self.count - - def __bool__(self,) -> bool: - """ Returns `True` only if there are errors registered """ - return self.count != 0 - - def __str__(self) -> str: - """ Returns a colored string that shows the results of the check """ - return '\n'.join(error.__str__() for error in self) - - @property - def count(self,) -> int: - """ Returns the number of registered errors """ - return len([error for error in self]) - - def register_error(self, error: TemplateCheckError) -> None: - """ Add an error to the collection. """ - self.errors.append(error) - - def dump_errors(self, destination: TemplateCheckErrorManager): - """ Dumps each error in the current error collection into the given - error manager. """ - - for error in self: - destination.register_error(error) - - -class TemplateCheckRaiseOnError(TemplateCheckErrorManager): - - def register_error(self, error: TemplateCheckError): - raise error diff --git a/validit/errors/parsing.py b/validit/errors/parsing.py deleted file mode 100644 index 605f445..0000000 --- a/validit/errors/parsing.py +++ /dev/null @@ -1,74 +0,0 @@ -import typing -from .errors import TemplateCheckError - - -class FileParsingError(TemplateCheckError): - """ Raised or registered into a error manager when a data file is not - formatted correctly and is invalid. """ - - def __init__(self, - filetype: str = None, - msg: str = None, - pos: typing.Tuple[int, int] = None, - ): - - if filetype: - string = f'Failed to parse {filetype} file' - else: - string = 'Failed to parse file' - - if msg: - string += f': {msg}' - - if pos: - string += f' (line {pos[0]} column {pos[1]})' - - super().__init__(msg=string) - - -class JsonParsingError(FileParsingError): - """ Raised or registered into a error manager when a JSON file is not - formatted correctly and is invalid. """ - - def __init__(self, exception): - """ Recives a `json.JSONDecodeError` error and passes it to the file - parsing error constructor. """ - - super().__init__( - filetype='JSON', - msg=exception.msg, - pos=(exception.lineno, exception.colno), - ) - - -class YamlParsingError(FileParsingError): - """ Raised or registered into a error manager when a YAML file is not - formatted correctly and is invalid. """ - - def __init__(self, exception): - """ Recives a `yaml.YAMLError` error and passes it to the file - parsing error constructor. """ - - super().__init__( - filetype='YAML', - msg=exception.problem, - pos=( - exception.problem_mark.line, - exception.problem_mark.column - ), - ) - - -class TomlParsingError(FileParsingError): - """ Raised or registered into a error manager when a YAML file is not - formatted correctly and is invalid. """ - - def __init__(self, exception): - """ Recives a `toml.TomlDecodeError` error and passes it to the file - parsing error constructor. """ - - super().__init__( - filetype='TOML', - msg=exception.msg, - pos=(exception.lineno, exception.colno), - ) diff --git a/validit/exceptions.py b/validit/exceptions.py deleted file mode 100644 index 893e49c..0000000 --- a/validit/exceptions.py +++ /dev/null @@ -1,17 +0,0 @@ -class ValidItError(Exception): - """ The base exception for this module. """ - - -class MissingExtras(ValidItError): - """ Raised when trying to use extra features without installing the - required packages (for example, trying to load YAML or TOML files without - installing required extra packages). """ - - -class InvalidTemplateConfiguration(ValidItError): - """ Raised when a template configuration is not valid. """ - - -class InvalidDefaultValue(ValidItError): - """ Raised when the given default value doesn't match the given template. - Used with the `Optional` object. """ diff --git a/validit/templates/__init__.py b/validit/templates/__init__.py deleted file mode 100644 index 2d6ce56..0000000 --- a/validit/templates/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .base import BaseTemplate - -from .templates import ( - Template, - TemplateAny, - TemplateDict, - TemplateList, - Optional, - Options, -) diff --git a/validit/templates/base.py b/validit/templates/base.py deleted file mode 100644 index 19131e1..0000000 --- a/validit/templates/base.py +++ /dev/null @@ -1,31 +0,0 @@ -import typing -from abc import ABC, abstractmethod - -from validit.containers import BaseContainer -from validit.utils import DefaultValue - -from validit.errors.managers import ( - TemplateCheckErrorManager as ErrorManager, - TemplateCheckErrorCollection as ErrorCollection, -) - - -class BaseTemplate(ABC): - - @abstractmethod - def container_dump(self, - container: BaseContainer, - data: typing.Any = DefaultValue, - ) -> None: - """ Recives an container and some data. Dumps only the relevent data - (according to the template) into the container. The previously saved - data in the container will be deleted. """ - - @abstractmethod - def validate(self, - container: BaseContainer, - errors: ErrorManager, - ) -> None: - """ Preforms a validation check that validates if the given data - follows the defined template. Returns an error manager object that - contains a record of all mismatches. """ diff --git a/validit/templates/templates.py b/validit/templates/templates.py deleted file mode 100644 index d91ec85..0000000 --- a/validit/templates/templates.py +++ /dev/null @@ -1,294 +0,0 @@ -import typing - -from copy import deepcopy - -from validit.errors.managers import ( - TemplateCheckErrorManager as ErrorManager, - TemplateCheckErrorCollection as ErrorCollection, - TemplateCheckRaiseOnError as RaiseOnErrorManager, -) - -from validit.errors import ( - TemplateCheckError, - TemplateCheckInvalidOptionError, - TemplateCheckInvalidDataError, - TemplateCheckMissingDataError, - TemplateCheckListLengthError, -) - -from .base import BaseTemplate - -from validit.containers import ( - BaseContainer, - HeadContainer, -) - -from validit.exceptions import InvalidTemplateConfiguration, InvalidDefaultValue -from validit.utils import AnyLength, DefaultValue - - -def classname(instance): - return type(instance).__name__ - - -class Template(BaseTemplate): - - def __init__(self, *types: type): - self.types = types - - for type_ in types: - if not isinstance(type_, type): - # If not a basic type like `str`, `int`, etc. - raise InvalidTemplateConfiguration( - f"The '{classname(self)}' constructor accepts object types, " + - f"not '{classname(type_)}'" - ) - - def container_dump(self, - container: BaseContainer, - data: typing.Any = DefaultValue, - ) -> None: - if data is not DefaultValue: - container.data = data - - def validate(self, - container: BaseContainer, - errors: ErrorManager, - ) -> None: - - if container.data is DefaultValue: - errors.register_error( - TemplateCheckMissingDataError(container) - ) - - elif not isinstance(container.data, self.types): - # If the given data is not an instance of the allowed types, - # an error is registered. - errors.register_error(TemplateCheckInvalidDataError( - container=container, - expected=self.types, - got=container.data, - )) - - -class TemplateAny(Template): - - def __init__(self,): - super().__init__(object) - - def container_dump(self, - container: BaseContainer, - data=DefaultValue) -> None: - container.data = deepcopy(data) - - -class Optional(BaseTemplate): - - def __init__(self, - template: Template, - default: typing.Any = DefaultValue - ) -> None: - self.__template = template - self.__default = default - - if not isinstance(template, BaseTemplate): - raise InvalidTemplateConfiguration( - f"The '{classname(self)}' constructor accepts Templates, " + - f"not '{classname(template)}'" - ) - - # If default value is provided, checks if the default value - # matches the template - if default is not DefaultValue: - try: - self.__template.validate( - container=HeadContainer(default), - errors=RaiseOnErrorManager(), - ) - - except TemplateCheckError as error: - raise InvalidDefaultValue( - f"'{classname(self)}' received a default value that doesn't match the template: " + - error.no_color_str - ) from None - - def container_dump(self, - container: BaseContainer, - data: typing.Any = DefaultValue, - ) -> None: - if data is DefaultValue: - data = self.__default - if data is not DefaultValue: - container.data = data - - def validate(self, - container: BaseContainer, - errors: ErrorManager, - ) -> None: - if container.data is not DefaultValue: - # Only preforms the check if the data is provided. - # if data is not given (data=Default), skips the check! - self.__template.validate(container, errors) - - -class TemplateList(Template): - - def __init__(self, template: Template, valid_lengths: typing.Any = AnyLength()): - super().__init__(list, tuple) - self.template = template - self.length = valid_lengths - - if not isinstance(template, Template): - raise InvalidTemplateConfiguration( - f"The '{classname(self)}' constructor recives a template instance, " + - f'not {classname(template)}' - ) - - if (not hasattr(valid_lengths, '__contains__') - ) or isinstance(valid_lengths, type): - raise InvalidTemplateConfiguration( - f"{valid_lengths!r} is not a valid set of list lengths" - ) - - def container_dump(self, - container: BaseContainer, - data: typing.Any = DefaultValue, - ) -> None: - - if not isinstance(data, (list, tuple)): - # If data is not a list (maybe default value) - # contanier will remain with default value. - container.data = data - return - - container.data = list() - for index, element in enumerate(data): - - # Increase container length by one - container.data.append(DefaultValue) - - # Dump the rest of the data with the element template - self.template.container_dump( - container=container[index], - data=element - ) - - def validate(self, - container: BaseContainer, - errors: ErrorManager, - ) -> None: - - # Check if data is a list - temp_errors = ErrorCollection() - super().validate(container, temp_errors) - - if temp_errors: - temp_errors.dump_errors(errors) - - else: - if len(container.data) not in self.length: - errors.register_error(TemplateCheckListLengthError( - container=container, - expected=self.length, - got=len(container.data), - )) - - # For each element in the list, - # check if it follows the element template - for cur in container: - self.template.validate( - container=cur, - errors=errors, - ) - - -class TemplateDict(Template): - - def __init__(self, **template): - super().__init__(dict) - self.template = template - - # Check if all values are template instances - try: error_element = next( - element - for element in template.values() - if not isinstance(element, BaseTemplate) - ) - - # If found a non template instance, raises an error - except StopIteration: pass - else: - raise InvalidTemplateConfiguration( - "Kwargs values should be template instances, " + - f"not '{classname(error_element)}'" - ) - - def container_dump(self, - container: BaseContainer, - data: typing.Any = DefaultValue, - ) -> None: - - if not isinstance(data, dict): - # If data is not a dict (maybe default value) - # contanier will remain with default value. - container.data = data - return - - container.data = dict() - for key, template in self.template.items(): - template.container_dump( - container=container[key], - data=data.get(key, DefaultValue) - ) - - def validate(self, - container: BaseContainer, - errors: ErrorManager, - ) -> None: - - # Check if the data is a dictionary - temp_errors = ErrorCollection() - super().validate(container, temp_errors) - - if temp_errors: - temp_errors.dump_errors(errors) - - else: - # If no errors in super validation check, run actual validation - for key, template in self.template.items(): - template.validate( - container=container[key], - errors=errors, - ) - - -class Options(BaseTemplate): - """ This template recives INSTANCES of objects (and not types), and when - validating, checks for EQUALITY between the data and the given instance. - This is useful in some cases where the options are pre-defined and limited. - - For example: `Options('L', 'R')` to allow the data to only be strings that - represent directions. """ - - def __init__(self, *instances: typing.Any): - self.instances = instances - - def container_dump(self, - container: BaseContainer, - data: typing.Any = DefaultValue, - ) -> None: - container.data = data - - def validate(self, - container: BaseContainer, - errors: ErrorManager, - ) -> None: - for cur in self.instances: - if cur is container.data: - return - - errors.register_error(TemplateCheckInvalidOptionError( - container=container, - expected=self.instances, - got=container.data, - )) diff --git a/validit/utils.py b/validit/utils.py deleted file mode 100644 index bda1ebd..0000000 --- a/validit/utils.py +++ /dev/null @@ -1,54 +0,0 @@ -import typing -from validit.exceptions import MissingExtras - - -class DefaultValue: - """ A default value used in the `TemplateDict` object to indicate that the - key is missing in the given data. """ - - def __repr__(self): - return "" - - -class AnyLength: - """ A class that "contains everything". Will return `True` for any call - to the `__contains__` method. Used as the default `valid_lengths` parameter - to `TemplateList` check method, because with this object every list length - is valid. """ - - @staticmethod - def __contains__(*_): - return True - - @staticmethod - def __repr__() -> str: - return 'AnyLength' - - -class ExtraModules: - """ A helper object that imports and stores extra modules. If one or more - of the needed modules fails to import, raises a custom error. """ - - def __init__(self, - class_name: str, - extra_name: str, - module_names: typing.List[str] - ) -> None: - """ Tries to import the extra modules, and saves them locally. - If one or more of the imports fails, raises a custom error. """ - self._modules = dict() - - for module in module_names: - try: - # Try importing and saving the current module - self._modules[module] = __import__(module) - - # If can't import, raise an error - except ImportError as error: - raise MissingExtras( - f"To use the '{class_name}' object you must install additional required packages. " + - f"Use 'pip install validit[{extra_name}]'" - ) from error - - def __getattr__(self, name): - return self._modules.get(name) diff --git a/validit/validate.py b/validit/validate.py deleted file mode 100644 index 690ad20..0000000 --- a/validit/validate.py +++ /dev/null @@ -1,183 +0,0 @@ -import typing - -from dataclasses import dataclass, field -from termcolor import colored - -from validit.errors.managers import TemplateCheckErrorCollection as ErrorCollection -from validit.templates.base import BaseTemplate -from validit.containers import HeadContainer -from validit.utils import ExtraModules - -from validit.errors.parsing import ( - JsonParsingError, - YamlParsingError, - TomlParsingError, -) - - -@dataclass -class ValidateInformation: - """ A dataclass that stores information about a validation check. - An instance of this object can be passed to the `Validate` constructor. - If the `Validate` constructor recives raw data (which will happen most in - most cases), it will convert the data to a `ValidateInformation` instance. - """ - - data: typing.Any = None - errors: ErrorCollection = field(default_factory=ErrorCollection) - fatal_error: bool = False - - -class Validate: - - def __init__(self, - template: BaseTemplate, - data: typing.Union[ValidateInformation, typing.Any] - ) -> None: - """ Validate the given data with the given template. """ - - if not isinstance(data, ValidateInformation): - data = ValidateInformation(data=data) - - self._info: ValidateInformation = data - self._data: HeadContainer = HeadContainer() - self._template: BaseTemplate = template - - if not self._info.fatal_error: - template.container_dump(self._data, self._info.data) - template.validate(self._data, self._info.errors) - - @property - def template(self,) -> BaseTemplate: - """ Returns the template given to the constructor. """ - return self._template - - @property - def errors(self,) -> ErrorCollection: - """ Returns an error collection object that stores a collection - of validation errors. """ - return self._info.errors - - @property - def data(self,) -> typing.Any: - """ The user data after it has been parsed. Data that is not required - by the template is removed, and data that is not provided by the user - but has a default value will be included. """ - return self._data.data - - @property - def original(self,) -> typing.Any: - """ The original user data, as given to the constructor. """ - return self._info.data - - def __str__(self,) -> str: - return self.errors.__str__() - - -class ValidateFromFile(Validate): - - def __init__(self, - template: BaseTemplate, - data: ValidateInformation, - title: str = None, - ) -> None: - """ Recives an open file (or file-like) object. Reads the data from it, - parses it with the corresponding format and returns the validation - results. """ - - self.__title = title - super().__init__(template, data) - - def __str__(self) -> str: - """ Returns a string colored that represents the template error check - results and errors with the given data. """ - - additional = (colored(self.__title, 'cyan') + ' ' - ) if self.__title else '' - - return '\n'.join( - additional + line - for line in super().__str__().splitlines() - ) - - -class ValidateFromJSON(ValidateFromFile): - - def __init__(self, - template: BaseTemplate, - fp: typing.IO, - title: str = None, - ) -> None: - """ Validate data from a JSON file, using a user-made template. """ - - extras = ExtraModules( - class_name=self.__class__.__name__, - extra_name='json', - module_names=('json',), - ) - - info = ValidateInformation() - - try: - info.data = extras.json.load(fp) - - except extras.json.JSONDecodeError as error: - info.fatal_error = True - info.errors.register_error(JsonParsingError(error)) - - finally: - super().__init__(template, info, title) - - -class ValidateFromYAML(ValidateFromFile): - - def __init__(self, - template: BaseTemplate, - fp: typing.IO, - title: str = None, - ) -> None: - - extras = ExtraModules( - class_name=self.__class__.__name__, - extra_name='yaml', - module_names=('yaml',), - ) - - info = ValidateInformation() - - try: - info.data = extras.yaml.full_load(fp) - - except extras.yaml.YAMLError as error: - info.fatal_error = True - info.errors.register_error(YamlParsingError(error)) - - finally: - super().__init__(template, info, title) - - -class ValidateFromTOML(ValidateFromFile): - - def __init__(self, - template: BaseTemplate, - fp: typing.IO, - title: str = None, - ) -> None: - - extras = ExtraModules( - class_name=self.__class__.__name__, - extra_name='toml', - module_names=('toml',), - ) - - info = ValidateInformation() - - try: - info.data = extras.toml.load(fp) - - except extras.toml.TomlDecodeError as error: - info.fatal_error = True - info.errors.register_error(TomlParsingError(error)) - - finally: - super().__init__(template, info, title) From f0eceabe99a8bbafab4ceaef8206381501c3c8e2 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 12:16:06 +0200 Subject: [PATCH 02/23] =?UTF-8?q?=E2=9C=A8=20Added=20abstract=20`Schema`?= =?UTF-8?q?=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- validit/schema.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 validit/schema.py diff --git a/validit/schema.py b/validit/schema.py new file mode 100644 index 0000000..c79587b --- /dev/null +++ b/validit/schema.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + + +class Schema(ABC): + + @abstractmethod + def validate(self, data): + """ An abstract method that will validate the given data according to + the current schema. This returns a generator that will yield + 'ValidationError' instances for every error in the data. + The generator won't yield any values if the data is valid. """ From 263a29e90b40729b24ad41fb34a41a612460ffbc Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 12:30:02 +0200 Subject: [PATCH 03/23] =?UTF-8?q?=E2=9C=A8=20Added=20`String`=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- validit/errors.py | 16 ++++++++++++++++ validit/string.py | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 validit/errors.py create mode 100644 validit/string.py diff --git a/validit/errors.py b/validit/errors.py new file mode 100644 index 0000000..1dd4fa1 --- /dev/null +++ b/validit/errors.py @@ -0,0 +1,16 @@ +from typing import Type, Any, Pattern + + +class ValidationError: + pass + + +class ValidationTypeError(ValidationError): + + def __init__(self, expected: Type, got: Any) -> None: + pass + + +class ValidationRegexError(ValidationError): + def __init__(self, string: str, pattern: Pattern) -> None: + pass diff --git a/validit/string.py b/validit/string.py new file mode 100644 index 0000000..c03a6b4 --- /dev/null +++ b/validit/string.py @@ -0,0 +1,17 @@ +from .schema import Schema +from .errors import ValidationError, ValidationRegexError, ValidationTypeError + +from typing import Iterator, Pattern + + +class String(Schema): + + def __init__(self, pattern: Pattern = None) -> None: + self.pattern = pattern + + def validate(self, data) -> Iterator[ValidationError]: + if not isinstance(data, str): + yield ValidationTypeError(str, data) + elif self.pattern is not None: + if not self.pattern.fullmatch(data): + yield ValidationRegexError(data, self.pattern) From e47c570b99f88bb782b707f5218bfc033c00d9af Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 12:30:31 +0200 Subject: [PATCH 04/23] =?UTF-8?q?=F0=9F=94=A5=20Removed=20`install=5Frequi?= =?UTF-8?q?res`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/setup.py b/setup.py index 0d43ecc..0c2b641 100644 --- a/setup.py +++ b/setup.py @@ -6,10 +6,6 @@ def load_readme(): return readme.read() -REQUIRES = ( -) - - EXTRAS = { 'dev': ( 'pytest>=6.2, <6.3', @@ -37,6 +33,5 @@ def load_readme(): ], packages=find_packages(), python_requires='>=3.6', - install_requires=REQUIRES, extras_require=EXTRAS, ) From fc9fff5552b4e0c7bac3ec02062031b80a753e88 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 12:41:42 +0200 Subject: [PATCH 05/23] =?UTF-8?q?=E2=9C=A8=20Added=20`String`=20and=20`Sch?= =?UTF-8?q?ema`=20to=20`=5F=5Finit=5F=5F`=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- validit/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/validit/__init__.py b/validit/__init__.py index 567416b..ec52c80 100644 --- a/validit/__init__.py +++ b/validit/__init__.py @@ -1,4 +1,7 @@ -__all__ = [] +from .schema import Schema +from .string import String + +__all__ = ['Schema', 'String'] __version__ = '2.0.0-dev' __author__ = 'Alon Krymgand Osovsky' From db68c6a648c5bd85dffd8d0f467ff313649270a7 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 12:42:09 +0200 Subject: [PATCH 06/23] =?UTF-8?q?=E2=9C=85=20Added=20`String`=20schema=20t?= =?UTF-8?q?ests=20(no=20regex)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_string.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/test_string.py diff --git a/tests/test_string.py b/tests/test_string.py new file mode 100644 index 0000000..5f72fdc --- /dev/null +++ b/tests/test_string.py @@ -0,0 +1,31 @@ +from abc import abstractclassmethod +import pytest + +from validit import String +from validit.errors import ValidationTypeError + + +class TestString: + + @pytest.mark.parametrize('data', ( + str(), + 'hello!', + 'שלום לכם!', + '123', + )) + def test_valid_no_regex(self, data): + errors = tuple(String().validate(data)) + assert len(errors) == 0 + + @pytest.mark.parametrize('data', ( + None, + 123, + 12.34, + 0, + True, + False, + )) + def test_invalid_no_regex(self, data): + errors = tuple(String().validate(data)) + assert len(errors) == 1 + assert isinstance(errors[0], ValidationTypeError) From 365732b6732f772d879505ca82df52cc4e8e8050 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 13:02:18 +0200 Subject: [PATCH 07/23] =?UTF-8?q?=E2=9C=85=20Added=20`String`=20regex=20te?= =?UTF-8?q?sts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_string.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/test_string.py b/tests/test_string.py index 5f72fdc..becad4c 100644 --- a/tests/test_string.py +++ b/tests/test_string.py @@ -1,8 +1,8 @@ -from abc import abstractclassmethod import pytest +import re from validit import String -from validit.errors import ValidationTypeError +from validit.errors import ValidationTypeError, ValidationRegexError class TestString: @@ -29,3 +29,24 @@ def test_invalid_no_regex(self, data): errors = tuple(String().validate(data)) assert len(errors) == 1 assert isinstance(errors[0], ValidationTypeError) + + @pytest.mark.parametrize('pattern, data', ( + (r'.+', 'Hello There!'), + (r'\w+', 'OnlyEnglishWorksHere'), + (r'\w+', 'th1s_sh0uld_BE_FINE_t00'), + (r'\d+', '1234'), + (r'\w*', ''), + )) + def test_valid_regex(self, pattern: str, data: str): + errors = tuple(String(re.compile(pattern)).validate(data)) + assert len(errors) == 0 + + @pytest.mark.parametrize('pattern, data', ( + (r'.+', str()), + (r'\w*', 'This is not pure English!'), + (r'.{,10}', 'This string is too long.'), + )) + def test_invalid_regex(self, pattern: str, data: str): + errors = tuple(String(re.compile(pattern)).validate(data)) + assert len(errors) == 1 + assert isinstance(errors[0], ValidationRegexError) From f3f970d276637b725614d946e3e4cc088e739f07 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 13:25:28 +0200 Subject: [PATCH 08/23] =?UTF-8?q?=E2=9C=A8=20Added=20`=5F=5Frepr=5F=5F`=20?= =?UTF-8?q?to=20Schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- validit/schema.py | 6 ++++++ validit/string.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/validit/schema.py b/validit/schema.py index c79587b..d0f152d 100644 --- a/validit/schema.py +++ b/validit/schema.py @@ -9,3 +9,9 @@ def validate(self, data): the current schema. This returns a generator that will yield 'ValidationError' instances for every error in the data. The generator won't yield any values if the data is valid. """ + + @abstractmethod + def __repr__(self) -> str: + """ An abstract method that should return a string that represents the + schema. This string may be used is validation error messages, if + validation fails. """ diff --git a/validit/string.py b/validit/string.py index c03a6b4..6a52332 100644 --- a/validit/string.py +++ b/validit/string.py @@ -15,3 +15,9 @@ def validate(self, data) -> Iterator[ValidationError]: elif self.pattern is not None: if not self.pattern.fullmatch(data): yield ValidationRegexError(data, self.pattern) + + def __repr__(self) -> str: + extra = str() + if self.pattern is not None: + extra = f'[{self.pattern.pattern}]' + return 'String' + extra From bd73adcc491ee48eb7eb0e23e936a747c65e75ee Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 13:26:16 +0200 Subject: [PATCH 09/23] =?UTF-8?q?=E2=9C=A8=20Added=20`Union`=20Schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- validit/errors.py | 9 ++++++++- validit/union.py | 24 ++++++++++++++++++++++++ validit/utils.py | 6 ++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 validit/union.py create mode 100644 validit/utils.py diff --git a/validit/errors.py b/validit/errors.py index 1dd4fa1..414ca0a 100644 --- a/validit/errors.py +++ b/validit/errors.py @@ -1,4 +1,6 @@ -from typing import Type, Any, Pattern +from validit import Schema + +from typing import Type, Any, Pattern, Tuple class ValidationError: @@ -14,3 +16,8 @@ def __init__(self, expected: Type, got: Any) -> None: class ValidationRegexError(ValidationError): def __init__(self, string: str, pattern: Pattern) -> None: pass + + +class ValidationUnionError(ValidationError): + def __init__(self, expected: Tuple[Schema], got: Any) -> None: + pass diff --git a/validit/union.py b/validit/union.py new file mode 100644 index 0000000..fc5c553 --- /dev/null +++ b/validit/union.py @@ -0,0 +1,24 @@ +from validit import Schema +from validit.errors import ValidationUnionError +from validit.utils import shorten + +from typing import Iterator + + +class Union(Schema): + + def __init__(self, *options: Schema) -> None: + self.options = options + + def validate(self, data) -> Iterator[ValidationUnionError]: + for option in self.options: + if not option.validate(data): + # If there are no errors, this option is valid. + return + + # If all options are invalid + yield ValidationUnionError(self.options, data) + + def __repr__(self) -> str: + extra = ', '.join(repr(op) for op in self.options) + return f"Union[{shorten(extra)}]" diff --git a/validit/utils.py b/validit/utils.py new file mode 100644 index 0000000..cc80bcb --- /dev/null +++ b/validit/utils.py @@ -0,0 +1,6 @@ +import textwrap + + +def shorten(text: str): + """ If the given text is too long, cuts the end and replaces it with '...'. """ + return textwrap.shorten(text, width=25, placeholder='...') From 677a094ab4a65bced37c813019a4675effd11c40 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 13:33:12 +0200 Subject: [PATCH 10/23] =?UTF-8?q?=E2=9C=A8=20Added=20`Optional`=20Schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- validit/union.py | 11 ++++++++++- validit/utils.py | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/validit/union.py b/validit/union.py index fc5c553..8dcea81 100644 --- a/validit/union.py +++ b/validit/union.py @@ -1,6 +1,6 @@ from validit import Schema from validit.errors import ValidationUnionError -from validit.utils import shorten +from validit.utils import shorten, MISSING from typing import Iterator @@ -22,3 +22,12 @@ def validate(self, data) -> Iterator[ValidationUnionError]: def __repr__(self) -> str: extra = ', '.join(repr(op) for op in self.options) return f"Union[{shorten(extra)}]" + + +class Optional(Union): + def __init__(self, *options: Schema) -> None: + super().__init__(MISSING, *options) + + def __repr__(self) -> str: + extra = ', '.join(repr(op) for op in self.options if op is not MISSING) + return f"Optional[{shorten(extra)}]" diff --git a/validit/utils.py b/validit/utils.py index cc80bcb..d0fcd46 100644 --- a/validit/utils.py +++ b/validit/utils.py @@ -4,3 +4,8 @@ def shorten(text: str): """ If the given text is too long, cuts the end and replaces it with '...'. """ return textwrap.shorten(text, width=25, placeholder='...') + + +class MISSING: + """ A dummy class that is uses with different 'get' methods as a default value + when the requested value is missing. Used instead of the builtin 'None'. """ From 60edc73fdcdb68deeec38bbf7c6462f5e0895507 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 14:08:48 +0200 Subject: [PATCH 11/23] =?UTF-8?q?=E2=9C=A8=20Added=20`Boolean`=20Schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_boolean.py | 30 ++++++++++++++++++++++++++++++ validit/__init__.py | 3 ++- validit/boolean.py | 13 +++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/test_boolean.py create mode 100644 validit/boolean.py diff --git a/tests/test_boolean.py b/tests/test_boolean.py new file mode 100644 index 0000000..a8c8d0a --- /dev/null +++ b/tests/test_boolean.py @@ -0,0 +1,30 @@ +from typing import Any +import pytest + +from validit import Boolean +from validit.errors import ValidationTypeError + + +class TestBoolean: + + @pytest.mark.parametrize('data', ( + True, + False, + )) + def test_valid(self, data: bool): + errors = tuple(Boolean().validate(data)) + assert not errors + + @pytest.mark.parametrize('data', ( + '', + 'Hello', + 0, + 100, + None, + 0.0, + 3.14, + )) + def test_invalid(self, data): + errors = tuple(Boolean().validate(data)) + assert len(errors) == 1 + assert isinstance(errors[0], ValidationTypeError) diff --git a/validit/__init__.py b/validit/__init__.py index ec52c80..e38518e 100644 --- a/validit/__init__.py +++ b/validit/__init__.py @@ -1,7 +1,8 @@ from .schema import Schema from .string import String +from .boolean import Boolean -__all__ = ['Schema', 'String'] +__all__ = ['Schema', 'String', 'Boolean'] __version__ = '2.0.0-dev' __author__ = 'Alon Krymgand Osovsky' diff --git a/validit/boolean.py b/validit/boolean.py new file mode 100644 index 0000000..1b713e3 --- /dev/null +++ b/validit/boolean.py @@ -0,0 +1,13 @@ +from typing import Iterator +from validit import Schema +from validit.errors import ValidationTypeError + + +class Boolean(Schema): + + def validate(self, data) -> Iterator[ValidationTypeError]: + if not isinstance(data, bool): + yield ValidationTypeError(bool, data) + + def __repr__(self) -> str: + return 'Boolean' From ea433b7f35720b7f0f1bbb4d0f8a5ad1a6e57914 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 14:55:26 +0200 Subject: [PATCH 12/23] =?UTF-8?q?=E2=9C=85=20Added=20`Union`=20Schema=20te?= =?UTF-8?q?sts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_union.py | 32 ++++++++++++++++++++++++++++++++ validit/__init__.py | 3 ++- validit/union.py | 4 +++- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/test_union.py diff --git a/tests/test_union.py b/tests/test_union.py new file mode 100644 index 0000000..2556f5a --- /dev/null +++ b/tests/test_union.py @@ -0,0 +1,32 @@ +import pytest + + +from validit import Union, Boolean, String +from validit.errors import ValidationUnionError + +import re + + +class TestUnion: + + @pytest.mark.parametrize('options, data', ( + ((Boolean(), String()), 'this is a string'), + ((Boolean(), String()), True), + ((Boolean(), String()), False), + ((Boolean(),), False), + ((Boolean(),), True), + ((String(),), 'Hello'), + )) + def test_valid(self, options, data): + errors = tuple(Union(*options).validate(data)) + assert not errors + + @pytest.mark.parametrize('options, data', ( + ((Boolean(), String(re.compile('.+'))), ''), + ((Boolean(), String()), None), + (tuple(), None) + )) + def test_invalid(self, options, data): + errors = tuple(Union(*options).validate(data)) + assert len(errors) == 1 + assert isinstance(errors[0], ValidationUnionError) diff --git a/validit/__init__.py b/validit/__init__.py index e38518e..d21dd34 100644 --- a/validit/__init__.py +++ b/validit/__init__.py @@ -1,8 +1,9 @@ from .schema import Schema from .string import String from .boolean import Boolean +from .union import Union -__all__ = ['Schema', 'String', 'Boolean'] +__all__ = ['Schema', 'String', 'Boolean', 'Union'] __version__ = '2.0.0-dev' __author__ = 'Alon Krymgand Osovsky' diff --git a/validit/union.py b/validit/union.py index 8dcea81..86dfcba 100644 --- a/validit/union.py +++ b/validit/union.py @@ -12,7 +12,9 @@ def __init__(self, *options: Schema) -> None: def validate(self, data) -> Iterator[ValidationUnionError]: for option in self.options: - if not option.validate(data): + try: + next(option.validate(data)) + except StopIteration: # If there are no errors, this option is valid. return From 48e86b1bbfc432574781637a2b2c86afcedb4e6f Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 15:31:59 +0200 Subject: [PATCH 13/23] =?UTF-8?q?=E2=9C=A8=20Added=20`Dictionary`=20Schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_dictionary.py | 69 ++++++++++++++++++++++++++++++++++++++++ validit/__init__.py | 5 +-- validit/dictionary.py | 29 +++++++++++++++++ validit/errors.py | 6 ++++ validit/union.py | 10 ++++-- validit/utils.py | 8 ++++- 6 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 tests/test_dictionary.py create mode 100644 validit/dictionary.py diff --git a/tests/test_dictionary.py b/tests/test_dictionary.py new file mode 100644 index 0000000..1c6b839 --- /dev/null +++ b/tests/test_dictionary.py @@ -0,0 +1,69 @@ +import pytest +import re + +from validit import Dictionary, String, Optional +from validit.boolean import Boolean +from validit.errors import ValidationKeyError, ValidationTypeError + + +class TestDictionary: + + @pytest.mark.parametrize('options, data', ( + ( + {'name': String(re.compile('[a-zA-Z]{3,15}'))}, + {'name': 'Alon'} + ), + ( + {'name': String(), 'nickname': Optional(String())}, + {'name': 'Alon', 'nickname': 'a10n'} + ), + ( + {'name': String(), 'nickname': Optional(String())}, + {'name': 'Alon'} + ), + ( + {'name': String(), 'over 18 y/o?': Boolean()}, + {'name': 'Alon', 'over 18 y/o?': True} + ), + ( + {'name': String(), 'more': Dictionary(over_18=Boolean())}, + {'name': 'Alon', 'more': {'over_18': True}} + ), + )) + def test_valid(self, options, data): + errors = tuple(Dictionary(**options).validate(data)) + assert not errors + + @pytest.mark.parametrize('options, data', ( + ( + {'name': String()}, + {'name': 'Alon', 'nickname': 'a10n'} + ), + )) + def test_invalid_key(self, options, data): + errors = tuple(Dictionary(**options).validate(data)) + assert len(errors) == 1 + assert isinstance(errors[0], ValidationKeyError) + + @pytest.mark.parametrize('options, data', ( + ( + {'name': String()}, + ['hello'], + ), + ( + {'name': String()}, + {'name': True}, + ), + ( + {'name': String()}, + {'name': None}, + ), + ( + {'name': String()}, + {'name': 123}, + ), + )) + def test_invalid_type(self, options, data): + errors = tuple(Dictionary(**options).validate(data)) + assert len(errors) == 1 + assert isinstance(errors[0], ValidationTypeError) diff --git a/validit/__init__.py b/validit/__init__.py index d21dd34..67e8f24 100644 --- a/validit/__init__.py +++ b/validit/__init__.py @@ -1,9 +1,10 @@ from .schema import Schema from .string import String from .boolean import Boolean -from .union import Union +from .union import Union, Optional +from .dictionary import Dictionary -__all__ = ['Schema', 'String', 'Boolean', 'Union'] +__all__ = ['Schema', 'String', 'Boolean', 'Union', 'Optional', 'Dictionary'] __version__ = '2.0.0-dev' __author__ = 'Alon Krymgand Osovsky' diff --git a/validit/dictionary.py b/validit/dictionary.py new file mode 100644 index 0000000..0e834a2 --- /dev/null +++ b/validit/dictionary.py @@ -0,0 +1,29 @@ +from typing import Iterator +from validit import Schema +from validit.errors import ValidationError, ValidationTypeError, ValidationKeyError + +from validit.utils import MISSING, shorten + + +class Dictionary(Schema): + def __init__(self, **options: Schema) -> None: + self.options = options + + def validate(self, data) -> Iterator[ValidationError]: + if not isinstance(data, dict): + yield ValidationTypeError(dict, data) + return + + for key in data: + if key not in self.options: + yield ValidationKeyError(key) + + for key, schema in self.options.items(): + yield from schema.validate(data.get(key, MISSING)) + + def __repr__(self) -> str: + extra = ', '.join(( + f"{key!r}={schema!r}" + for key, schema in self.options.items() + )) + return f'Dictionary[{shorten(extra)}]' diff --git a/validit/errors.py b/validit/errors.py index 414ca0a..53acfb7 100644 --- a/validit/errors.py +++ b/validit/errors.py @@ -21,3 +21,9 @@ def __init__(self, string: str, pattern: Pattern) -> None: class ValidationUnionError(ValidationError): def __init__(self, expected: Tuple[Schema], got: Any) -> None: pass + + +class ValidationKeyError(ValidationError): + def __init__(self, key: str) -> None: + pass + diff --git a/validit/union.py b/validit/union.py index 86dfcba..8b4dff8 100644 --- a/validit/union.py +++ b/validit/union.py @@ -27,9 +27,13 @@ def __repr__(self) -> str: class Optional(Union): - def __init__(self, *options: Schema) -> None: - super().__init__(MISSING, *options) + + def validate(self, data) -> Iterator[ValidationUnionError]: + if data is MISSING: + return + else: + yield from super().validate(data) def __repr__(self) -> str: - extra = ', '.join(repr(op) for op in self.options if op is not MISSING) + extra = ', '.join(repr(op) for op in self.options) return f"Optional[{shorten(extra)}]" diff --git a/validit/utils.py b/validit/utils.py index d0fcd46..ea13772 100644 --- a/validit/utils.py +++ b/validit/utils.py @@ -6,6 +6,12 @@ def shorten(text: str): return textwrap.shorten(text, width=25, placeholder='...') -class MISSING: +class MissingType: """ A dummy class that is uses with different 'get' methods as a default value when the requested value is missing. Used instead of the builtin 'None'. """ + + def __repr__(self) -> str: + return 'MISSING' + + +MISSING = MissingType() From 672f098af2cd84a539a574513618719f55d4fb13 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 16:32:25 +0200 Subject: [PATCH 14/23] =?UTF-8?q?=E2=9C=A8=20Added=20`Number`=20and=20`Int?= =?UTF-8?q?eger`=20Schemas=20(no=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- validit/errors.py | 8 ++++++++ validit/number.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 validit/number.py diff --git a/validit/errors.py b/validit/errors.py index 53acfb7..b033aa0 100644 --- a/validit/errors.py +++ b/validit/errors.py @@ -27,3 +27,11 @@ class ValidationKeyError(ValidationError): def __init__(self, key: str) -> None: pass + +class ValidationRangeError(ValidationError): + def __init__(self, + got: float, + minimun: float = None, + maximum: float = None, + ) -> None: + pass diff --git a/validit/number.py b/validit/number.py new file mode 100644 index 0000000..6e508e4 --- /dev/null +++ b/validit/number.py @@ -0,0 +1,49 @@ +from validit import Schema +from validit.errors import ( + ValidationError, + ValidationTypeError, + ValidationRangeError, +) + +from typing import Iterator + + +class Number(Schema): + + def __init__( + self, + minimum: float = None, + maximum: float = None, + ) -> None: + self.minimum = minimum + self.maximum = maximum + + def validate(self, data) -> Iterator[ValidationError]: + if not isinstance(data, (float, int)): + yield ValidationTypeError(float, data) + + elif self.minimum is not None and data < self.minimum: + yield ValidationRangeError(data, self.minimum, self.maximum) + + elif self.maximum is not None and data > self.maximum: + yield ValidationRangeError(data, self.minimum, self.maximum) + + def __repr__(self) -> str: + mini = f'>={self.minimum}' if self.minimum is not None else '' + maxi = f'<={self.maximum}' if self.maximum is not None else '' + extra = ', ' if mini and maxi else '' + return f'Number[{mini}{extra}{maxi}]' + + +class Integer(Number): + def validate(self, data) -> Iterator[ValidationError]: + if not isinstance(data, int): + yield ValidationTypeError(int, data) + else: + yield from super().validate(data) + + def __repr__(self) -> str: + mini = f'>={self.minimum}' if self.minimum is not None else '' + maxi = f'<={self.maximum}' if self.maximum is not None else '' + extra = ', ' if mini and maxi else '' + return f'Integer[{mini}{extra}{maxi}]' From 517ab0677b3fe9dbd7e236e34428f459c5cf1e4f Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 16:35:07 +0200 Subject: [PATCH 15/23] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Rearranged=20`=5F=5F?= =?UTF-8?q?init=5F=5F.py`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- validit/__init__.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/validit/__init__.py b/validit/__init__.py index 67e8f24..37e3942 100644 --- a/validit/__init__.py +++ b/validit/__init__.py @@ -1,10 +1,27 @@ from .schema import Schema + from .string import String from .boolean import Boolean -from .union import Union, Optional +from .number import Number, Integer + from .dictionary import Dictionary +from .union import Union, Optional + +__all__ = [ + # base + 'Schema', + + # primitive + 'String', + 'Boolean', + 'Number', + 'Integer', -__all__ = ['Schema', 'String', 'Boolean', 'Union', 'Optional', 'Dictionary'] + # nested + 'Dictionary', + 'Optional', + 'Union', +] __version__ = '2.0.0-dev' __author__ = 'Alon Krymgand Osovsky' From bb59d729db1e8aaf5acb224bd21b208cde35437d Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 16:40:40 +0200 Subject: [PATCH 16/23] =?UTF-8?q?=F0=9F=91=B7=20Added=20CI=20tests=20with?= =?UTF-8?q?=20py3.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0afbb29..d48874b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - version: [3.6, 3.7, 3.8, 3.9] + version: [3.6, 3.7, 3.8, 3.9, 3.10] steps: - name: Clone 👀 From fc9da0bbd46f3259336106686e292bea2782c2b3 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 16:41:45 +0200 Subject: [PATCH 17/23] =?UTF-8?q?=F0=9F=92=9A=20Fixed=20CI=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d48874b..b8d4ba4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - version: [3.6, 3.7, 3.8, 3.9, 3.10] + version: ['3.6', '3.7', '3.8', '3.9', '3.10'] steps: - name: Clone 👀 From 56eee8acf9aa81b669a899b23601ed80f4ff74f3 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 20:02:15 +0200 Subject: [PATCH 18/23] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Updated=20implementa?= =?UTF-8?q?tion=20of=20`Number`=20Schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- validit/number.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/validit/number.py b/validit/number.py index 6e508e4..b338e5c 100644 --- a/validit/number.py +++ b/validit/number.py @@ -1,3 +1,5 @@ +import numbers + from validit import Schema from validit.errors import ( ValidationError, @@ -19,7 +21,7 @@ def __init__( self.maximum = maximum def validate(self, data) -> Iterator[ValidationError]: - if not isinstance(data, (float, int)): + if not isinstance(data, numbers.Number): yield ValidationTypeError(float, data) elif self.minimum is not None and data < self.minimum: From a1554469e1a0d56debe0d912f307f4cc3ce9f8fc Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 20:10:50 +0200 Subject: [PATCH 19/23] =?UTF-8?q?=E2=9C=85=20Added=20`Number`=20and=20`Int?= =?UTF-8?q?eger`=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_number.py | 76 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/test_number.py diff --git a/tests/test_number.py b/tests/test_number.py new file mode 100644 index 0000000..5ace963 --- /dev/null +++ b/tests/test_number.py @@ -0,0 +1,76 @@ +import pytest + +import math +from fractions import Fraction + +from validit import Number, Integer +from validit.errors import ValidationTypeError, ValidationRangeError + + +class TestNumber: + + @pytest.mark.parametrize('options, data', ( + ( + {}, + (-100**1000, 0, 100**1000, 1, 123, 123.456) + ), + ( + {'minimum': 0, 'maximum': 120}, + (0, 120, math.pi, 1, 119, 60, Fraction(1, 2), True, False), + ), + ( + {'minimum': 0}, + (0, 0.0, 123456789.987654321, 1, 100, 10**100, 10**1000, 10e-100), + ) + )) + def test_valid(self, options, data): + schema = Number(**options) + for i in data: + errors = tuple(schema.validate(i)) + assert not errors + + @pytest.mark.parametrize('options, data', ( + ( + {'minimum': 1}, + (10e-10, 0.000012 + 0.000000000234, -10**100, -1, 0, False), + ), + ( + {'maximum': 100}, + (101, 120, 100e100), + ), + ( + {'minimum': 0, 'maximum': 120}, + (-1, -10e-10, 121, 100e100), + ), + )) + def test_invalid_range(self, options, data): + schema = Number(**options) + for i in data: + errors = tuple(schema.validate(i)) + assert len(errors) == 1 + assert isinstance(errors[0], ValidationRangeError) + + @pytest.mark.parametrize('data', ( + 'string', '123', '123.456', '1.0', None, + )) + def test_invalid_type(self, data): + errors = tuple(Number().validate(data)) + assert len(errors) == 1 + assert isinstance(errors[0], ValidationTypeError) + + @pytest.mark.parametrize('data', ( + -100**1000, 0, 100**1000, 1, 123, 0, 1, 100, 1000, 2**100 + )) + def test_valid_integer(self, data): + schema = Integer() + errors = tuple(schema.validate(data)) + assert not errors + + @pytest.mark.parametrize('data', ( + 12.34, 0.0, 1.1, Fraction(1, 2), complex(10, 10) + )) + def test_invalid_integer_type(self, data): + schema = Integer() + errors = tuple(schema.validate(data)) + assert len(errors) == 1 + assert isinstance(errors[0], ValidationTypeError) From e86492d5412b1b0162c85f9f4b6cefba9cd48162 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 20:27:54 +0200 Subject: [PATCH 20/23] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20`String`=20schema=20?= =?UTF-8?q?now=20accepts=20regex=20patterns=20as=20strings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_string.py | 5 ++--- validit/string.py | 10 +++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_string.py b/tests/test_string.py index becad4c..e2aa963 100644 --- a/tests/test_string.py +++ b/tests/test_string.py @@ -1,5 +1,4 @@ import pytest -import re from validit import String from validit.errors import ValidationTypeError, ValidationRegexError @@ -38,7 +37,7 @@ def test_invalid_no_regex(self, data): (r'\w*', ''), )) def test_valid_regex(self, pattern: str, data: str): - errors = tuple(String(re.compile(pattern)).validate(data)) + errors = tuple(String(pattern).validate(data)) assert len(errors) == 0 @pytest.mark.parametrize('pattern, data', ( @@ -47,6 +46,6 @@ def test_valid_regex(self, pattern: str, data: str): (r'.{,10}', 'This string is too long.'), )) def test_invalid_regex(self, pattern: str, data: str): - errors = tuple(String(re.compile(pattern)).validate(data)) + errors = tuple(String(pattern).validate(data)) assert len(errors) == 1 assert isinstance(errors[0], ValidationRegexError) diff --git a/validit/string.py b/validit/string.py index 6a52332..39e03f2 100644 --- a/validit/string.py +++ b/validit/string.py @@ -1,13 +1,17 @@ +import re + from .schema import Schema from .errors import ValidationError, ValidationRegexError, ValidationTypeError -from typing import Iterator, Pattern +from typing import Iterator class String(Schema): - def __init__(self, pattern: Pattern = None) -> None: - self.pattern = pattern + def __init__(self, pattern: str = None) -> None: + self.pattern = None + if pattern is not None: + self.pattern = re.compile(pattern) def validate(self, data) -> Iterator[ValidationError]: if not isinstance(data, str): From 6df1935204b6f6c13f71e84ffcafde7609dc1c40 Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 20:40:58 +0200 Subject: [PATCH 21/23] =?UTF-8?q?=E2=9C=A8=20Added=20external=20`validate`?= =?UTF-8?q?=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 9 +++++++++ validit/__init__.py | 4 ++++ validit/validate.py | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 validit/validate.py diff --git a/setup.py b/setup.py index 0c2b641..374c7fa 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,14 @@ def load_readme(): ), } + +REQUIRES = ( + # the dataclasses module is prebuilt into python>=3.7 + # For Python 3.6, it is supported using a backport + 'dataclasses; python_version < "3.7"', +) + + setup( name='validit', description='Easily define and validate configuration file structures 📂🍒', @@ -33,5 +41,6 @@ def load_readme(): ], packages=find_packages(), python_requires='>=3.6', + install_requires=REQUIRES, extras_require=EXTRAS, ) diff --git a/validit/__init__.py b/validit/__init__.py index 37e3942..de99aa3 100644 --- a/validit/__init__.py +++ b/validit/__init__.py @@ -7,9 +7,13 @@ from .dictionary import Dictionary from .union import Union, Optional +from .validate import validate, ValidationResults + __all__ = [ # base 'Schema', + 'validate', + 'ValidationResults', # primitive 'String', diff --git a/validit/validate.py b/validit/validate.py new file mode 100644 index 0000000..3e83274 --- /dev/null +++ b/validit/validate.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +from validit import Schema +from validit.errors import ValidationError + + +from typing import Any, Tuple, Union, Type + + +@dataclass +class ValidationResults: + schema: Schema + data: Any + errors: Tuple[ValidationError] + + +def validate(schema: Union[Type, Schema], data) -> ValidationResults: + errors = tuple(schema.validate(data)) + return ValidationResults(schema, data, errors) From 0e8e912305afbb0891b79fa9f4767f5542dd782f Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 21:10:20 +0200 Subject: [PATCH 22/23] =?UTF-8?q?=F0=9F=93=9D=20Updated=20`README.md`=20to?= =?UTF-8?q?=20v2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 127 +++++++----------------------------------------------- 1 file changed, 16 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index 4e3091f..f9bd138 100644 --- a/README.md +++ b/README.md @@ -2,134 +2,39 @@ [![Test](https://img.shields.io/github/workflow/status/reala10n/validit/%E2%9C%94%20Test?label=test)](https://github.com/RealA10N/validit/actions/workflows/test.yaml) [![PyPI](https://img.shields.io/pypi/v/validit)](https://pypi.org/project/validit/) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/validit)](https://pypi.org/project/validit/) -[![GitHub Repo stars](https://img.shields.io/github/stars/reala10n/validit?style=social)](https://github.com/RealA10N/validit) _Easily define configuration file structures, and validate files using the templates. 🍒📂_ - [Installation](#installation) - - [Support for additional file formats](#support-for-additional-file-formats) - [Usage](#usage) - - [Defining a template](#defining-a-template) - - [Validating data](#validating-data) - - [Validating data from files](#validating-data-from-files) -- [Using validit as a dependency](#using-validit-as-a-dependency) + - [Defining a schema](#defining-a-schema) ## Installation -**validit** is tested on CPython 3.6, 3.7, 3.8, and 3.9. +**validit** is tested on CPython 3.6 - 3.10. Simply install using pip: ```bash $ (sudo) pip install validit ``` -### Support for additional file formats - -By default, _validit_ only supports `JSON` configuration files, or -already loaded data (not directly from a configuration file). However, using -additional dependencies, _validit_ supports the following file formats: - -- `JSON` -- `YAML` -- `TOML` - -To install _validit_ with the additional required dependencies to support -your preferred file format, use: - -```yaml -pip install validit[yaml] # install dependencies for yaml files -pip install validit[toml] # toml files -pip install validit[json,toml] # json and toml files -pip install validit[all] # all available file formats -``` - ## Usage -### Defining a template - -To create a template, you will need the basic `Template` module, and usually the -other three basic modules `TemplateList`, `TemplateDict`, and `Optional`. - -In the following example, we will create a basic template that represents a single user: - -```python -from validit import Template, TemplateList, TemplateDict, Optional - -TemplateUser = TemplateDict( # a dictionary with 2 required values - username=Template(str), # username must be a string - passcode=Template(int, str), # can be a string or an integer. - nickname=Optional(Template(str)), # optional - if provided, must be a string. -) -``` - -### Validating data - -To validate your data with a template, you should use the `Validate` object. - -```python -from validit import Template, TemplateDict, Optional, Validate +### Defining a schema -template = TemplateDict( - username=Template(str), - passcode=Template(int, str), - nickname=Optional(Template(str)), -) +**validit** uses Python objects for constructing the Schema. +We provide a handful collection of Schema classes that should cover all your +basic usage cases, especially if you are going to validate a JSON, TOML or a YAML configuration file. +We will briefly talk about how you can create your own Schema objects later. -data = { - 'username': 'RealA10N', - 'passcode': 123, -} +The classes that we provide for defining schemas are: -valid = Validate(template, data) -if valid.errors: # if one or more errors found - print(valid.errors) # print errors to console - exit(1) # exit the script with exit code 1 +Name | Description +---------------------|------------------------------------------------------------- +`validit.String` | Validates a string and matches it with a _RegEx_ pattern. +`validit.Number` | Validates any real number in a certain range. +`validit.Integer` | Validates an integer in a certain range. +`validit.Union` | Matches a value with at least one schema. +`validit.Dictionary` | Validates a dictionary and all values inside it recursively. +`validit.Optional` | Converts a field inside a `Dictionary` into an optional one. -else: # if data matches the template - run_script(valid.data) # run the script with the loaded data -``` - -#### Validating data from files - -If your data is stored in a file, it is possible to use the `ValidateFromJSON`, -`ValidateFromYAML` or `ValidateFromTOML` objects instead: - -```python -from validit import Template, TemplateDict, Optional, ValidateFromYAML - -filepath = '/path/to/data.yaml' -template = TemplateDict( - username=Template(str), - passcode=Template(int, str), - nickname=Optional(Template(str)), -) - -with open(filepath, 'r') as file: - # load and validate data from the file - valid = ValidateFromYAML(file, template) - -if valid.errors: # if one or more errors found - print(valid.errors) # print errors to console - exit(1) # exit the script with exit code 1 - -else: # if data matches the template - run_script(valid.data) # run the script with the loaded data -``` - -## Using validit as a dependency - -_validit_ is still under active development, and some core features -may change substantially in the near future. - -If you are planning to use _validit_ as a dependency for your project, -we highly recommend specifying the exact version of the module you are using -in the `requirements.txt` file or `setup.py` scripts. - -For example, to pinpoint version _v1.3.2_ use the following line in your -`requirements.txt` file: - -```yaml -validit==1.3.2 -validit[yaml]==1.3.2 # If using extra file formats -``` From e635b0c737081f136f89ec894d3aaf40525c922b Mon Sep 17 00:00:00 2001 From: "Alon (Ubuntu)" Date: Tue, 4 Jan 2022 21:12:55 +0200 Subject: [PATCH 23/23] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Updated=20test=20to?= =?UTF-8?q?=20new=20`String`=20RegEx=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_dictionary.py | 3 +-- tests/test_union.py | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_dictionary.py b/tests/test_dictionary.py index 1c6b839..3e80138 100644 --- a/tests/test_dictionary.py +++ b/tests/test_dictionary.py @@ -1,5 +1,4 @@ import pytest -import re from validit import Dictionary, String, Optional from validit.boolean import Boolean @@ -10,7 +9,7 @@ class TestDictionary: @pytest.mark.parametrize('options, data', ( ( - {'name': String(re.compile('[a-zA-Z]{3,15}'))}, + {'name': String(r'[a-zA-Z]{3,15}')}, {'name': 'Alon'} ), ( diff --git a/tests/test_union.py b/tests/test_union.py index 2556f5a..0350390 100644 --- a/tests/test_union.py +++ b/tests/test_union.py @@ -1,11 +1,8 @@ import pytest - from validit import Union, Boolean, String from validit.errors import ValidationUnionError -import re - class TestUnion: @@ -22,7 +19,7 @@ def test_valid(self, options, data): assert not errors @pytest.mark.parametrize('options, data', ( - ((Boolean(), String(re.compile('.+'))), ''), + ((Boolean(), String(r'.+')), ''), ((Boolean(), String()), None), (tuple(), None) ))