Page MenuHomeSoftware Heritage

No OneTemporary

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 <class 'str'>) 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 <class 'int'>) 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 @@
<!-- begin -->
[![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.
<!-- end -->
## 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)
<!-- begin -->
## 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 <class 'str'>) 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 <class 'int'>) 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`.
<!-- end -->
## 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 <class 'str'>) 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 <class 'int'>) 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 <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/>`_.
Getting started
---------------
Run :code:`pip install attrs-strict` to install the latest stable version from PyPi.
The source code is hosted on github at `<https://github.com/bloomberg/attrs-strict>`_.
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 <class 'str'>) 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 <class 'int'>) 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 (
"<foo must be typing.DefaultDict[int, typing.List[str]] "
"(got 1 that is a {}) in [1, 2, 3] in "
"defaultdict({}, {{5: [1, 2, 3]}})>"
).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,
+ (
+ "<zoo must be typing.Mapping[str, str] "
+ "(got 123 that is a {})"
+ ).format(int),
+ ),
+ (
+ {1: "boo", 2: "zoo"},
+ CollectionsMutableMapping,
+ MutableMapping,
+ (
+ "<zoo must be typing.MutableMapping[str, str] "
+ "(got 1 that is a {})"
+ ).format(int),
+ ),
+ ],
+)
+def test_abc_mapping_types_throw_when_type_is_wrong(
+ data, type, validator_type, error_message
+):
+ class TestMapping(type):
+ def __init__(self, items):
+ self._data = items
+
+ def __getitem__(self, item):
+ return self._data[item]
+
+ def __len__(self):
+ return len(self._data)
+
+ def __iter__(self):
+ return iter(self._data)
+
+ def __delitem__(self, item):
+ pass
+
+ def __setitem__(self, item, value):
+ pass
+
+ validator = type_validator()
+
+ attr = MagicMock()
+ attr.name = "zoo"
+ attr.type = validator_type[str, str]
+
+ with pytest.raises(ValueError) as error:
+ validator(None, attr, TestMapping(data))
+
+ assert error_message in repr(error.value)
diff --git a/tests/test_list.py b/tests/test_list.py
index ea4cae6..44ab628 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -1,92 +1,94 @@
-from typing import Dict, List, Set, Tuple
+from typing import Any, Dict, List, Set, Tuple
import pytest
from attrs_strict import type_validator
try:
from unittest.mock import MagicMock
except ImportError:
from mock import Mock as MagicMock
@pytest.mark.parametrize(
"values, type_, error_message",
[
(
[1, 2, "a"],
List[int],
(
(
"numbers must be typing.List[int] "
"(got a that is a {}) in [1, 2, 'a']"
).format(str)
),
),
(
[[1, "a"]],
List[List[int]],
(
(
"numbers must be "
"typing.List[typing.List[int]] (got a that is a {}) "
"in [1, 'a'] in [[1, 'a']]"
).format(str)
),
),
(
[[1, 2, 3], ["a"]],
List[List[int]],
(
(
"numbers must be "
"typing.List[typing.List[int]] (got a that is a {}) "
"in ['a'] in [[1, 2, 3], ['a']]"
).format(str)
),
),
(
[(1, 2, "foo")],
List[Tuple[int, int, int]],
(
"numbers must be "
"typing.List[typing.Tuple[int, int, int]] (got foo "
"that is a {}) in (1, 2, 'foo') in "
"[(1, 2, 'foo')]"
).format(str),
),
],
)
def test_list_of_values_raise_value_error(values, type_, error_message):
validator = type_validator()
attrib = MagicMock()
attrib.name = "numbers"
attrib.type = type_
with pytest.raises(ValueError) as error:
validator(None, attrib, values)
# THEN
msg = "<{}>".format(error_message)
assert msg == repr(error.value)
@pytest.mark.parametrize(
"values, type_",
[
([1, 2, 3], List[int]),
([[1], [2], [3]], List[List[int]]),
({1, 2, 3}, Set[int]),
([{1: [1, 2, 3], 2: [3, 4, 5]}], List[Dict[int, List[int]]]),
+ ([1, 2, 3, 4], List[Any]),
+ ([1, 2, {"foo": "bar"}], List[Any]),
],
)
def test_list_of_valid_values_no_raise(values, type_):
validator = type_validator()
attrib = MagicMock()
attrib.name = "numbers"
attrib.type = type_
validator(None, attrib, values)
diff --git a/tests/test_newtype.py b/tests/test_newtype.py
new file mode 100644
index 0000000..4a652ad
--- /dev/null
+++ b/tests/test_newtype.py
@@ -0,0 +1,104 @@
+import typing
+
+import pytest
+
+from attrs_strict import AttributeTypeError, BadTypeError, type_validator
+
+try:
+ from unittest.mock import MagicMock
+except ImportError:
+ from mock import Mock as MagicMock
+
+
+@pytest.mark.parametrize("test_type, correct", [(str, "foo"), (int, 42),])
+def test_typing_newtype_single_validation_success(test_type, correct):
+ SomeNew = typing.NewType("SomeNew", test_type)
+
+ validator = type_validator()
+ attr = MagicMock()
+ attr.type = SomeNew
+
+ validator(None, attr, correct)
+ validator(None, attr, SomeNew(correct))
+
+
+@pytest.mark.parametrize(
+ "test_type, wrongs", [(str, [42, True]), (int, ["foo", ()]),]
+)
+def test_typing_newtype_single_validation_failure(test_type, wrongs):
+ SomeNew = typing.NewType("SomeNew", test_type)
+
+ validator = type_validator()
+ attr = MagicMock()
+ attr.type = SomeNew
+
+ for wrong in wrongs:
+ with pytest.raises(AttributeTypeError) as error:
+ validator(None, attr, wrong)
+
+ assert "must be NewType(SomeNew, {})".format(str(test_type)) in str(
+ error.value
+ )
+
+
+@pytest.mark.parametrize(
+ "container, test_type, correct",
+ [
+ (typing.List, str, ["foo", "bar"]),
+ (typing.Tuple, int, (0,)),
+ (typing.Optional, str, None),
+ ],
+)
+def test_typing_newtype_within_container_validation_success(
+ container, test_type, correct
+):
+ SomeNew = typing.NewType("SomeNew", test_type)
+
+ validator = type_validator()
+ attr = MagicMock()
+ attr.type = container[SomeNew]
+
+ validator(None, attr, correct)
+
+
+@pytest.mark.parametrize(
+ "container, test_type, wrongs",
+ [
+ (typing.List, str, [42, True, "foo", ("foo", "bar")]),
+ (typing.Tuple, int, ["foo", 42, [0, 1, "2"]]),
+ (typing.Optional, str, [42, (1, 2)]),
+ ],
+)
+def test_typing_newtype_within_container_validation_failure(
+ container, test_type, wrongs
+):
+ SomeNew = typing.NewType("SomeNew", test_type)
+
+ validator = type_validator()
+ attr = MagicMock()
+ attr.type = container[SomeNew]
+
+ for wrong in wrongs:
+ with pytest.raises(BadTypeError) as error:
+ validator(None, attr, wrong)
+
+ assert "must be {}".format(str(attr.type)) in str(
+ error.value
+ ) or "is not of type {}".format(str(attr.type)) in str(error.value)
+
+
+# -----------------------------------------------------------------------------
+# 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/tests/test_tuple.py b/tests/test_tuple.py
index a3428ab..765ac07 100644
--- a/tests/test_tuple.py
+++ b/tests/test_tuple.py
@@ -1,47 +1,89 @@
from typing import Tuple
import pytest
from attrs_strict import type_validator
try:
from unittest.mock import MagicMock
except ImportError:
from mock import Mock as MagicMock
def test_tuple_with_incorrect_number_of_arguments_raises():
element = (1, 2, 3, 4)
attr = MagicMock()
attr.name = "foo"
attr.type = Tuple[int, int, int]
validator = type_validator()
with pytest.raises(ValueError) as error:
validator(None, attr, element)
assert (
"<Element (1, 2, 3, 4) has more elements than types specified "
"in typing.Tuple[int, int, int]. Expected 3 received 4>"
) == 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 (
"<Element (3, 4, 5) has more elements than types specified "
"in typing.Tuple[typing.Tuple[int, int], typing.Tuple[int, int]]. "
"Expected 2 received 3 in ((1, 2), (3, 4, 5))>"
) == 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 (
+ "<foo must be typing.Tuple[int, ...] (got 4 that is a {}) "
+ "in (1, 2, 3, '4')>"
+ ).format(str) == repr(error.value)

File Metadata

Mime Type
text/x-diff
Expires
Jul 4 2025, 9:52 AM (5 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3310239

Event Timeline