diff --git a/PKG-INFO b/PKG-INFO index 9c85866..a644200 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,123 +1,123 @@ Metadata-Version: 2.1 Name: attrs-strict -Version: 0.0.5.1 +Version: 0.0.7 Summary: Runtime validators for attrs Home-page: https://github.com/bloomberg/attrs-strict Author: Erik-Cristian Seulean Author-email: eseulean@bloomberg.net License: Apache 2.0 Project-URL: Source, https://github.com/bloomberg/attrs-strict Project-URL: Tracker, https://github.com/bloomberg/attrs-strict/issues Project-URL: Documentation, https://github.com/bloomberg/attrs-strict/blob/master/README.md#attrs-runtime-validation Description: [![Latest version on PyPi](https://badge.fury.io/py/attrs-strict.svg)](https://badge.fury.io/py/attrs-strict) [![Supported Python versions](https://img.shields.io/pypi/pyversions/attrs-strict.svg)](https://pypi.org/project/attrs-strict/) [![Travis build status](https://travis-ci.com/bloomberg/attrs-strict.svg?branch=master)](https://travis-ci.com/bloomberg/attrs-strict.svg?branch=master) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) # attrs runtime validation `attrs-strict` is a Python package that contains runtime validation for [`attrs`]((https://github.com/python-attrs/attrs)) data classes based on the types existing in the typing module. ## Rationale The purpose of the library is to provide runtime validation for attributes specified in [`attrs`](https://www.attrs.org/en/stable/) data classes. The types supported are all the builtin types and most of the ones defined in the typing library. For Python 2, the typing module is available through the backport found [`here`](https://pypi.org/project/typing/). ## Quick Start Type enforcement is based on the `type` attribute set on any field specified in an `attrs` dataclass. If the type argument is not specified no validation takes place. `pip install attrs-strict` ```python from typing import List import attr from attrs_strict import type_validator >>> @attr.s ... class SomeClass(object): ... list_of_numbers = attr.ib( ... validator=type_validator(), ... type=List[int] ... ) ... >>> sc = SomeClass([1,2,3,4]) >>> sc SomeClass(list_of_numbers=[1, 2, 3, 4]) >>> try: ... other = SomeClass([1,2,3,'four']) ... except ValueError as error: ... print(repr(error)) attrs_strict._error.AttributeTypeError: list_of_numbers must be typing.List[int] (got four that is a ) in [1, 2, 3, 'four'] ``` Nested type exceptions are validated acordingly, and a backtrace to the initial container is maintained to ease with debugging. This means that if an exception occurs because a nested element doesn't have the correct type, the representation of the exception will contain the path to the specific element that caused the exception. ```python from typing import List, Tuple import attr from attrs_strict import type_validator >>> @attr.s ... class SomeClass(object): ... names = attr.ib( ... validator=type_validator(), type=List[Tuple[str, str]] ... ) >>> sc = SomeClass(names=[('Moo', 'Moo'), ('Zoo',123)]) attrs_strict._error.AttributeTypeError: names must be typing.List[typing.Tuple[str, str]] (got 123 that is a ) in ('Zoo', 123) in [('Moo', 'Moo'), ('Zoo', 123)] ``` ### What is currently supported ? - Currently there's support for simple types and types specified in the `typing` module: `List`, `Dict`, `DefaultDict`, `Set`, `Union`, `Tuple` and any combination of them. This means that you can specify nested types like `List[List[Dict[int, str]]]` and the validation would check if attribute has the specific type. + Currently there's support for simple types and types specified in the `typing` module: `List`, `Dict`, `DefaultDict`, `Set`, `Union`, `Tuple`, `NewType`, and any combination of them. This means that you can specify nested types like `List[List[Dict[int, str]]]` and the validation would check if attribute has the specific type. `Callables`, `TypeVars` or `Generics` are not supported yet but there are plans to support this in the future. ## Building For development, the project uses `tox` in order to install dependencies, run tests and generate documentation. In order to be able to do this, you need tox `pip install tox` and after that invoke `tox` in the root of the project. ## Installation Run `pip install attrs-strict` to install the latest stable version from [PyPi](https://pypi.org/project/attrs-strict/). Documentation is hosted on [readthedocs](https://attrs-strict.readthedocs.io/en/latest/). For the latest version, on github `pip install git+https://github.com/bloomberg/attrs-strict`. Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4 Description-Content-Type: text/markdown diff --git a/README.md b/README.md index f9bcbbc..6c87078 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,132 @@ [![Latest version on PyPi](https://badge.fury.io/py/attrs-strict.svg)](https://badge.fury.io/py/attrs-strict) [![Supported Python versions](https://img.shields.io/pypi/pyversions/attrs-strict.svg)](https://pypi.org/project/attrs-strict/) [![Travis build status](https://travis-ci.com/bloomberg/attrs-strict.svg?branch=master)](https://travis-ci.com/bloomberg/attrs-strict.svg?branch=master) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) # attrs runtime validation `attrs-strict` is a Python package that contains runtime validation for [`attrs`]((https://github.com/python-attrs/attrs)) data classes based on the types existing in the typing module. ## Menu - [Rationale](#rationale) - [Quick start](#quick-start) - [Building](#building) - [Installation](#installation) - [Contributions](#contributions) - [License](#license) - [Code of Conduct](#code-of-conduct) - [Security Vulnerability Reporting](#security-vulnerability-reporting) ## Rationale The purpose of the library is to provide runtime validation for attributes specified in [`attrs`](https://www.attrs.org/en/stable/) data classes. The types supported are all the builtin types and most of the ones defined in the typing library. For Python 2, the typing module is available through the backport found [`here`](https://pypi.org/project/typing/). ## Quick Start Type enforcement is based on the `type` attribute set on any field specified in an `attrs` dataclass. If the type argument is not specified no validation takes place. `pip install attrs-strict` ```python from typing import List import attr from attrs_strict import type_validator >>> @attr.s ... class SomeClass(object): ... list_of_numbers = attr.ib( ... validator=type_validator(), ... type=List[int] ... ) ... >>> sc = SomeClass([1,2,3,4]) >>> sc SomeClass(list_of_numbers=[1, 2, 3, 4]) >>> try: ... other = SomeClass([1,2,3,'four']) ... except ValueError as error: ... print(repr(error)) attrs_strict._error.AttributeTypeError: list_of_numbers must be typing.List[int] (got four that is a ) in [1, 2, 3, 'four'] ``` Nested type exceptions are validated acordingly, and a backtrace to the initial container is maintained to ease with debugging. This means that if an exception occurs because a nested element doesn't have the correct type, the representation of the exception will contain the path to the specific element that caused the exception. ```python from typing import List, Tuple import attr from attrs_strict import type_validator >>> @attr.s ... class SomeClass(object): ... names = attr.ib( ... validator=type_validator(), type=List[Tuple[str, str]] ... ) >>> sc = SomeClass(names=[('Moo', 'Moo'), ('Zoo',123)]) attrs_strict._error.AttributeTypeError: names must be typing.List[typing.Tuple[str, str]] (got 123 that is a ) in ('Zoo', 123) in [('Moo', 'Moo'), ('Zoo', 123)] ``` ### What is currently supported ? -Currently there's support for simple types and types specified in the `typing` module: `List`, `Dict`, `DefaultDict`, `Set`, `Union`, `Tuple` and any combination of them. This means that you can specify nested types like `List[List[Dict[int, str]]]` and the validation would check if attribute has the specific type. +Currently there's support for simple types and types specified in the `typing` module: `List`, `Dict`, `DefaultDict`, `Set`, `Union`, `Tuple`, `NewType`, and any combination of them. This means that you can specify nested types like `List[List[Dict[int, str]]]` and the validation would check if attribute has the specific type. `Callables`, `TypeVars` or `Generics` are not supported yet but there are plans to support this in the future. ## Building For development, the project uses `tox` in order to install dependencies, run tests and generate documentation. In order to be able to do this, you need tox `pip install tox` and after that invoke `tox` in the root of the project. ## Installation Run `pip install attrs-strict` to install the latest stable version from [PyPi](https://pypi.org/project/attrs-strict/). Documentation is hosted on [readthedocs](https://attrs-strict.readthedocs.io/en/latest/). For the latest version, on github `pip install git+https://github.com/bloomberg/attrs-strict`. ## Contributions We :heart: contributions. Have you had a good experience with this project? Why not share some love and contribute code, or just let us know about any issues you had with it? We welcome issue reports [here](../../issues); be sure to choose the proper issue template for your issue, so that we can be sure you're providing the necessary information. Before sending a [Pull Request](../../pulls), please make sure you read our [Contribution Guidelines](https://github.com/bloomberg/.github/blob/master/CONTRIBUTING.md). ## License Please read the [LICENSE](LICENSE) file. ## Code of Conduct This project has adopted a [Code of Conduct](https://github.com/bloomberg/.github/blob/master/CODE_OF_CONDUCT.md). If you have any concerns about the Code, or behavior which you have experienced in the project, please contact us at opensource@bloomberg.net. ## Security Vulnerability Reporting If you believe you have identified a security vulnerability in this project, please send email to the project team at opensource@bloomberg.net, detailing the suspected issue and any methods you've found to reproduce it. Please do NOT open an issue in the GitHub repository, as we'd prefer to keep vulnerability reports private until we've had an opportunity to review and address them. diff --git a/attrs_strict.egg-info/PKG-INFO b/attrs_strict.egg-info/PKG-INFO index 9c85866..a644200 100644 --- a/attrs_strict.egg-info/PKG-INFO +++ b/attrs_strict.egg-info/PKG-INFO @@ -1,123 +1,123 @@ Metadata-Version: 2.1 Name: attrs-strict -Version: 0.0.5.1 +Version: 0.0.7 Summary: Runtime validators for attrs Home-page: https://github.com/bloomberg/attrs-strict Author: Erik-Cristian Seulean Author-email: eseulean@bloomberg.net License: Apache 2.0 Project-URL: Source, https://github.com/bloomberg/attrs-strict Project-URL: Tracker, https://github.com/bloomberg/attrs-strict/issues Project-URL: Documentation, https://github.com/bloomberg/attrs-strict/blob/master/README.md#attrs-runtime-validation Description: [![Latest version on PyPi](https://badge.fury.io/py/attrs-strict.svg)](https://badge.fury.io/py/attrs-strict) [![Supported Python versions](https://img.shields.io/pypi/pyversions/attrs-strict.svg)](https://pypi.org/project/attrs-strict/) [![Travis build status](https://travis-ci.com/bloomberg/attrs-strict.svg?branch=master)](https://travis-ci.com/bloomberg/attrs-strict.svg?branch=master) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) # attrs runtime validation `attrs-strict` is a Python package that contains runtime validation for [`attrs`]((https://github.com/python-attrs/attrs)) data classes based on the types existing in the typing module. ## Rationale The purpose of the library is to provide runtime validation for attributes specified in [`attrs`](https://www.attrs.org/en/stable/) data classes. The types supported are all the builtin types and most of the ones defined in the typing library. For Python 2, the typing module is available through the backport found [`here`](https://pypi.org/project/typing/). ## Quick Start Type enforcement is based on the `type` attribute set on any field specified in an `attrs` dataclass. If the type argument is not specified no validation takes place. `pip install attrs-strict` ```python from typing import List import attr from attrs_strict import type_validator >>> @attr.s ... class SomeClass(object): ... list_of_numbers = attr.ib( ... validator=type_validator(), ... type=List[int] ... ) ... >>> sc = SomeClass([1,2,3,4]) >>> sc SomeClass(list_of_numbers=[1, 2, 3, 4]) >>> try: ... other = SomeClass([1,2,3,'four']) ... except ValueError as error: ... print(repr(error)) attrs_strict._error.AttributeTypeError: list_of_numbers must be typing.List[int] (got four that is a ) in [1, 2, 3, 'four'] ``` Nested type exceptions are validated acordingly, and a backtrace to the initial container is maintained to ease with debugging. This means that if an exception occurs because a nested element doesn't have the correct type, the representation of the exception will contain the path to the specific element that caused the exception. ```python from typing import List, Tuple import attr from attrs_strict import type_validator >>> @attr.s ... class SomeClass(object): ... names = attr.ib( ... validator=type_validator(), type=List[Tuple[str, str]] ... ) >>> sc = SomeClass(names=[('Moo', 'Moo'), ('Zoo',123)]) attrs_strict._error.AttributeTypeError: names must be typing.List[typing.Tuple[str, str]] (got 123 that is a ) in ('Zoo', 123) in [('Moo', 'Moo'), ('Zoo', 123)] ``` ### What is currently supported ? - Currently there's support for simple types and types specified in the `typing` module: `List`, `Dict`, `DefaultDict`, `Set`, `Union`, `Tuple` and any combination of them. This means that you can specify nested types like `List[List[Dict[int, str]]]` and the validation would check if attribute has the specific type. + Currently there's support for simple types and types specified in the `typing` module: `List`, `Dict`, `DefaultDict`, `Set`, `Union`, `Tuple`, `NewType`, and any combination of them. This means that you can specify nested types like `List[List[Dict[int, str]]]` and the validation would check if attribute has the specific type. `Callables`, `TypeVars` or `Generics` are not supported yet but there are plans to support this in the future. ## Building For development, the project uses `tox` in order to install dependencies, run tests and generate documentation. In order to be able to do this, you need tox `pip install tox` and after that invoke `tox` in the root of the project. ## Installation Run `pip install attrs-strict` to install the latest stable version from [PyPi](https://pypi.org/project/attrs-strict/). Documentation is hosted on [readthedocs](https://attrs-strict.readthedocs.io/en/latest/). For the latest version, on github `pip install git+https://github.com/bloomberg/attrs-strict`. Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4 Description-Content-Type: text/markdown diff --git a/attrs_strict.egg-info/SOURCES.txt b/attrs_strict.egg-info/SOURCES.txt index 1a4b8cf..7f94f26 100644 --- a/attrs_strict.egg-info/SOURCES.txt +++ b/attrs_strict.egg-info/SOURCES.txt @@ -1,30 +1,32 @@ .gitignore .pre-commit-config.yaml .readthedocs.yml .travis.yml LICENSE README.md pyproject.toml setup.cfg setup.py tox.ini attrs_strict/__init__.py +attrs_strict/_commons.py attrs_strict/_error.py attrs_strict/_type_validation.py attrs_strict/_version.py attrs_strict.egg-info/PKG-INFO attrs_strict.egg-info/SOURCES.txt attrs_strict.egg-info/dependency_links.txt attrs_strict.egg-info/requires.txt attrs_strict.egg-info/top_level.txt doc/api.rst doc/conf.py doc/index.rst tests/__init__.py tests/test_base_types.py tests/test_container.py tests/test_dict.py tests/test_list.py tests/test_module.py +tests/test_newtype.py tests/test_tuple.py tests/test_union.py \ No newline at end of file diff --git a/attrs_strict/_commons.py b/attrs_strict/_commons.py new file mode 100644 index 0000000..52369a0 --- /dev/null +++ b/attrs_strict/_commons.py @@ -0,0 +1,13 @@ +def is_newtype(type_): + return ( + hasattr(type_, "__name__") + and hasattr(type_, "__supertype__") + and type_.__module__ == "typing" + ) + + +def format_type(type_): + if is_newtype(type_): + return "NewType({}, {})".format(type_.__name__, type_.__supertype__) + + return str(type_) diff --git a/attrs_strict/_error.py b/attrs_strict/_error.py index de8e013..c3b8630 100644 --- a/attrs_strict/_error.py +++ b/attrs_strict/_error.py @@ -1,93 +1,96 @@ +from ._commons import format_type + + class TypeValidationError(Exception): def __repr__(self): return "<{}>".format(str(self)) class BadTypeError(TypeValidationError, ValueError): def __init__(self): self.containers = [] def add_container(self, container): self.containers.append(container) def _render(self, error): if self.containers: backtrack = " in ".join( [str(container) for container in self.containers] ) return "{} in {}".format(error, backtrack) return error class AttributeTypeError(BadTypeError): def __init__(self, container, attribute): super(AttributeTypeError, self).__init__() self.container = container self.attribute = attribute def __str__(self): error = "{} must be {} (got {} that is a {})".format( self.attribute.name, - self.attribute.type, + format_type(self.attribute.type), self.container, type(self.container), ) return self._render(error) class EmptyError(BadTypeError): def __init__(self, container, attribute): super(EmptyError, self).__init__() self.container = container self.attribute = attribute def __str__(self): error = "{} can not be empty and must be {} (got {})".format( self.attribute.name, - self.attribute.type, + format_type(self.attribute.type), self.container, ) return self._render(error) class TupleError(BadTypeError): def __init__(self, container, attribute, tuple_types): super(TupleError, self).__init__() self.attribute = attribute self.container = container self.tuple_types = tuple_types def __str__(self): error = ( "Element {} has {} elements than types " "specified in {}. Expected {} received {}" ).format( self.container, self._more_or_less(), self.attribute, len(self.tuple_types), len(self.container), ) return self._render(error) def _more_or_less(self): return "more" if len(self.container) > len(self.tuple_types) else "less" class UnionError(BadTypeError): def __init__(self, container, attribute, expected_type): super(UnionError, self).__init__() self.attribute = attribute self.container = container self.expected_type = expected_type def __str__(self): error = "Value of {} {} is not of type {}".format( self.attribute, self.container, self.expected_type ) return self._render(error) diff --git a/attrs_strict/_type_validation.py b/attrs_strict/_type_validation.py index 735822f..2dc75d2 100644 --- a/attrs_strict/_type_validation.py +++ b/attrs_strict/_type_validation.py @@ -1,124 +1,154 @@ import collections import typing -from ._error import AttributeTypeError, BadTypeError, EmptyError, TupleError, UnionError +from ._commons import is_newtype +from ._error import ( + AttributeTypeError, + BadTypeError, + EmptyError, + TupleError, + UnionError, +) + +try: + from collections.abc import Mapping + from collections.abc import MutableMapping +except ImportError: + from collections import Mapping + from collections import MutableMapping + + +class SimilarTypes: + Dict = { + dict, + collections.OrderedDict, + collections.defaultdict, + Mapping, + MutableMapping, + typing.Dict, + typing.DefaultDict, + typing.Mapping, + typing.MutableMapping, + } + List = {set, list, typing.List, typing.Set} + Tuple = {tuple, typing.Tuple} def type_validator(empty_ok=True): """ Validates the attributes using the type argument specified. If the type argument is not present, the attribute is considered valid. :param empty_ok: Boolean flag that indicates if the field can be empty in case of a collection or None for builtin types. """ def _validator(instance, attribute, field): if not empty_ok and not field: raise EmptyError(field, attribute) _validate_elements(attribute, field, attribute.type) return _validator def _validate_elements(attribute, value, expected_type): - base_type = ( - expected_type.__origin__ - if hasattr(expected_type, "__origin__") + if ( + hasattr(expected_type, "__origin__") and expected_type.__origin__ is not None - else expected_type - ) - - if base_type is None: + ): + base_type = expected_type.__origin__ + elif is_newtype(expected_type): + base_type = expected_type.__supertype__ + else: + base_type = expected_type + + if base_type is None or base_type == typing.Any: return if base_type != typing.Union and not isinstance(value, base_type): raise AttributeTypeError(value, attribute) - if base_type in {set, list, typing.List, typing.Set}: + if base_type in SimilarTypes.List: _handle_set_or_list(attribute, value, expected_type) - elif base_type in { - dict, - collections.OrderedDict, - collections.defaultdict, - typing.Dict, - typing.DefaultDict, - }: + elif base_type in SimilarTypes.Dict: _handle_dict(attribute, value, expected_type) - elif base_type in {tuple, typing.Tuple}: + elif base_type in SimilarTypes.Tuple: _handle_tuple(attribute, value, expected_type) elif base_type == typing.Union: _handle_union(attribute, value, expected_type) def _handle_set_or_list(attribute, container, expected_type): - element_type, = expected_type.__args__ + (element_type,) = expected_type.__args__ for element in container: try: _validate_elements(attribute, element, element_type) except BadTypeError as error: error.add_container(container) raise error def _handle_dict(attribute, container, expected_type): key_type, value_type = expected_type.__args__ for key in container: try: _validate_elements(attribute, key, key_type) _validate_elements(attribute, container[key], value_type) except BadTypeError as error: error.add_container(container) raise error def _handle_tuple(attribute, container, expected_type): tuple_types = expected_type.__args__ + if len(tuple_types) == 2 and tuple_types[1] == Ellipsis: + element_type = tuple_types[0] + tuple_types = (element_type, ) * len(container) if len(container) != len(tuple_types): raise TupleError(container, attribute.type, tuple_types) for element, expected_type in zip(container, tuple_types): try: _validate_elements(attribute, element, expected_type) except BadTypeError as error: error.add_container(container) raise error def _handle_union(attribute, value, expected_type): union_has_none_type = any( elem is None.__class__ for elem in expected_type.__args__ ) if value is None and union_has_none_type: return for arg in expected_type.__args__: try: _validate_elements(attribute, value, arg) return except ValueError: pass raise UnionError(value, attribute.name, expected_type) # ----------------------------------------------------------------------------- # Copyright 2019 Bloomberg Finance L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------- END-OF-FILE ----------------------------------- diff --git a/attrs_strict/_version.py b/attrs_strict/_version.py index 4263f04..b6a22d3 100644 --- a/attrs_strict/_version.py +++ b/attrs_strict/_version.py @@ -1,16 +1,16 @@ -__version__ = '0.0.5.1' +__version__ = '0.0.7' # -------------------------------------------------------------------------- # Copyright 2019 Bloomberg Finance L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------- END-OF-FILE -------------------------------- diff --git a/doc/index.rst b/doc/index.rst index e754aff..e825329 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,91 +1,91 @@ attrs_strict documentation ============================ Background ---------- The purpose of the library is to provide runtime validation for attributes specified in `attrs `_ data classes. The types supported are all the builtin types and most of the ones defined in the typing library. For Python 2, the typing module is available through the backport found `here `_. Getting started --------------- Run :code:`pip install attrs-strict` to install the latest stable version from PyPi. The source code is hosted on github at ``_. The library currently supports :code:`Python2.7`, :code:`Python3.6` and :code:`Python3.7`. Usage and examples ------------------ Type enforcement is based on the :code:`type` attribute set on any field specified in an :code:`attrs` dataclass. If the type argument is not specified no validation takes place. .. code-block:: python from typing import List import attr from attrs_strict import type_validator, ContainerError >>> @attr.s ... class SomeClass(object): ... list_of_numbers = attr.ib( ... validator=type_validator(), ... type=List[int] ... ) ... >>> sc = SomeClass([1,2,3,4]) >>> sc SomeClass(list_of_numbers=[1, 2, 3, 4]) >>> SomeClass([1,2,3,'four']) attrs_strict._error.AttributeTypeError( "list_of_numbers must be typing.List[int]" "(got four that is a ) in [1, 2, 3, 'four']" ) Nested type exceptions are validated acordingly, and a backtrace to the initial container is maintained to ease with debugging. This means that if an exception occurs because a nested element doesn't have the correct type, the representation of the exception will contain the path to the specific element that caused the exception. .. code-block:: python from typing import List, Tuple import attr from attrs_strict import type_validator, ContainerError >>> @attr.s ... class SomeClass(object): ... names = attr.ib( ... validator=type_validator(), type=List[Tuple[str, str]] ... ) >>> sc = SomeClass(names=[('Moo', 'Moo'), ('Zoo',123)]) attrs_strict._error.AttributeTypeError( "names must be" "typing.List[typing.Tuple[str, str]] (got 123 that is a ) in" "('Zoo', 123) in [('Moo', 'Moo'), ('Zoo', 123)]" ) What is currently supported ? ----------------------------- Currently there's support for builtin types and types specified in the :code:`typing` module: :code:`List`, :code:`Dict`, :code:`DefaultDict`, :code:`Set`, :code:`Union`, -:code:`Tuple` and any combination of them. This means that you can specify nested -types like :code:`List[List[Dict[int, str]]]` and the validation would check if -attribute has the specific type. +:code:`Tuple`, :code:`NewType` and any combination of them. This means that you can +specify nested types like :code:`List[List[Dict[int, str]]]` and the validation would +check if attribute has the specific type. :code:`Callables`, :code:`TypeVars` or :code:`Generics` are not supported yet but there are plans to support this in the future. .. toctree:: :maxdepth: 1 api diff --git a/tests/test_dict.py b/tests/test_dict.py index 55c68ed..c82423a 100644 --- a/tests/test_dict.py +++ b/tests/test_dict.py @@ -1,45 +1,122 @@ import collections -from typing import DefaultDict, List +from typing import Any, DefaultDict, Dict, List, Mapping, MutableMapping import pytest from attrs_strict import type_validator +try: + from collections.abc import Mapping as CollectionsMapping + from collections.abc import MutableMapping as CollectionsMutableMapping +except ImportError: + from collections import Mapping as CollectionsMapping + from collections import MutableMapping as CollectionsMutableMapping + + try: from unittest.mock import MagicMock except ImportError: from mock import Mock as MagicMock def test_defaultdict_raise_error(): elem = collections.defaultdict(int) elem[5] = [1, 2, 3] validator = type_validator() attr = MagicMock() attr.name = "foo" attr.type = DefaultDict[int, List[str]] with pytest.raises(ValueError) as error: validator(None, attr, elem) assert ( "" ).format(int, int) == repr(error.value) def test_defaultdict_with_correct_type_no_raise(): elem = collections.defaultdict(int) elem[5] = [1, 2, 3] elem[6] = [4, 5, 6] validator = type_validator() attr = MagicMock() attr.name = "foo" attr.type = DefaultDict[int, List[int]] validator(None, attr, elem) + + +def test_dict_with_any_does_not_raise(): + elem = {"foo": 123, "b": "abc"} + + validator = type_validator() + + attr = MagicMock() + attr.name = "zoo" + attr.type = Dict[str, Any] + + validator(None, attr, elem) + + +@pytest.mark.parametrize( + "data, type, validator_type, error_message", + [ + ( + {"foo": 123}, + CollectionsMapping, + Mapping, + ( + "" ) == repr(error.value) def test_tuple_of_tuple_raises(): element = ((1, 2), (3, 4, 5)) attr = MagicMock() attr.name = "foo" attr.type = Tuple[Tuple[int, int], Tuple[int, int]] validator = type_validator() with pytest.raises(ValueError) as error: validator(None, attr, element) assert ( "" ) == repr(error.value) + + +def test_variable_length_tuple(): + element = (1, 2, 3, 4) + + attr = MagicMock() + attr.name = "foo" + attr.type = Tuple[int, ...] + + validator = type_validator() + + validator(None, attr, element) + + +def test_variable_length_tuple_empty(): + element = () + + attr = MagicMock() + attr.name = "foo" + attr.type = Tuple[int, ...] + + validator = type_validator() + + validator(None, attr, element) + + +def test_variable_length_tuple_raises(): + element = (1, 2, 3, "4") + + attr = MagicMock() + attr.name = "foo" + attr.type = Tuple[int, ...] + + validator = type_validator() + + with pytest.raises(ValueError) as error: + validator(None, attr, element) + + assert ( + "" + ).format(str) == repr(error.value)