diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a09f1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# python +*.pyc +*.pyo +*.swp +__pycache__ + +# packaging folders +/build/ +/dist/ +*egg-info +*_version.py + +# tox working folder +/.tox + +# IDEs +/.vscode + +# tools +/.*_cache + +pip-wheel-metadata diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..75923ef --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,41 @@ +repos: +- repo: https://github.com/psf/black + rev: 19.3b0 + hooks: + - id: black + args: [--safe] + language_version: python3.7 +- repo: https://github.com/asottile/blacken-docs + rev: v1.3.0 + hooks: + - id: blacken-docs + args: [--skip-errors] + additional_dependencies: [black==19.3b0] + language_version: python3.7 +- repo: https://github.com/asottile/seed-isort-config + rev: v1.9.2 + hooks: + - id: seed-isort-config + args: [--application-directories, "src:."] +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: debug-statements + - id: flake8 + additional_dependencies: ["flake8-bugbear == 19.8.0"] + language_version: python3.7 +- repo: https://github.com/asottile/pyupgrade + rev: v1.24.0 + hooks: + - id: pyupgrade +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.4.1 + hooks: + - id: rst-backticks diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..ff20712 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,13 @@ +version: 2 +sphinx: + builder: html + configuration: doc/conf.py + fail_on_warning: true +formats: all +python: + version: 3.7 + install: + - method: pip + path: . +build: + image: latest diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a78940a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +dist: xenial +language: python +python: + - "2.7" + - "3.6" + - "3.7" + - "3.8" + - "pypy" + - "pypy3" +install: + - pip install tox-travis +script: + - tox + +deploy: + - provider: script + script: python -m pip install tox && tox -vve pypi_publish + on: + tags: true + python: "3.8" + condition: -n "$TWINE_PASSWORD" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8ed7f79 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 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. diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..9c85866 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,123 @@ +Metadata-Version: 2.1 +Name: attrs-strict +Version: 0.0.5.1 +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. + + `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 new file mode 100644 index 0000000..f9bcbbc --- /dev/null +++ b/README.md @@ -0,0 +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. + +`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 new file mode 100644 index 0000000..9c85866 --- /dev/null +++ b/attrs_strict.egg-info/PKG-INFO @@ -0,0 +1,123 @@ +Metadata-Version: 2.1 +Name: attrs-strict +Version: 0.0.5.1 +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. + + `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 new file mode 100644 index 0000000..1a4b8cf --- /dev/null +++ b/attrs_strict.egg-info/SOURCES.txt @@ -0,0 +1,30 @@ +.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/_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_tuple.py +tests/test_union.py \ No newline at end of file diff --git a/attrs_strict.egg-info/dependency_links.txt b/attrs_strict.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/attrs_strict.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/attrs_strict.egg-info/requires.txt b/attrs_strict.egg-info/requires.txt new file mode 100644 index 0000000..a58a71f --- /dev/null +++ b/attrs_strict.egg-info/requires.txt @@ -0,0 +1,4 @@ +attrs + +[:python_version < "3.5"] +typing diff --git a/attrs_strict.egg-info/top_level.txt b/attrs_strict.egg-info/top_level.txt new file mode 100644 index 0000000..f65f1c7 --- /dev/null +++ b/attrs_strict.egg-info/top_level.txt @@ -0,0 +1 @@ +attrs_strict diff --git a/attrs_strict/__init__.py b/attrs_strict/__init__.py new file mode 100755 index 0000000..3bfa08f --- /dev/null +++ b/attrs_strict/__init__.py @@ -0,0 +1,38 @@ +"""Runtime validation library for attrs data classes. + +""" + +from ._error import ( + AttributeTypeError, + BadTypeError, + TupleError, + TypeValidationError, + UnionError, +) +from ._type_validation import type_validator +from ._version import __version__ # noqa + +__all__ = [ + "type_validator", + "AttributeTypeError", + "BadTypeError", + "UnionError", + "TupleError", + "TypeValidationError", +] + +# ----------------------------------------------------------------------------- +# 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/_error.py b/attrs_strict/_error.py new file mode 100644 index 0000000..de8e013 --- /dev/null +++ b/attrs_strict/_error.py @@ -0,0 +1,93 @@ +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, + 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, + 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 new file mode 100644 index 0000000..735822f --- /dev/null +++ b/attrs_strict/_type_validation.py @@ -0,0 +1,124 @@ +import collections +import typing + +from ._error import AttributeTypeError, BadTypeError, EmptyError, TupleError, UnionError + + +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__") + and expected_type.__origin__ is not None + else expected_type + ) + + if base_type is None: + 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}: + _handle_set_or_list(attribute, value, expected_type) + elif base_type in { + dict, + collections.OrderedDict, + collections.defaultdict, + typing.Dict, + typing.DefaultDict, + }: + _handle_dict(attribute, value, expected_type) + elif base_type in {tuple, typing.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__ + + 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(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 new file mode 100644 index 0000000..4263f04 --- /dev/null +++ b/attrs_strict/_version.py @@ -0,0 +1,16 @@ +__version__ = '0.0.5.1' +# -------------------------------------------------------------------------- +# 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/api.rst b/doc/api.rst new file mode 100644 index 0000000..4ad4a9b --- /dev/null +++ b/doc/api.rst @@ -0,0 +1,5 @@ +API Documentation +----------------- + +.. automodule:: attrs_strict + :members: diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..75e6e08 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,35 @@ +import os +import sys + +from attrs_strict import __version__ + +sys.path.insert(0, os.path.abspath("../..")) + +project = "" +copyright = "2019, Bloomberg" +author = "Erik-Cristian Seulean" + +release = "2019" + +extensions = [ + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", +] + +templates_path = ["_templates"] +language = "python" + +exclude_patterns = [] + +html_theme = "alabaster" +html_static_path = [] + +master_doc = "index" + +version = u".".join(__version__.split(".")[:2]) +release = __version__ diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..e754aff --- /dev/null +++ b/doc/index.rst @@ -0,0 +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:`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/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7529136 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = [ + "setuptools >= 40.0.4", + "setuptools_scm >= 2.0.0, <4", + "wheel >= 0.29.0", +] +build-backend = 'setuptools.build_meta' + +[tool.black] +line-length = 80 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7c6c116 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[bdist_wheel] +universal = true + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..8c45026 --- /dev/null +++ b/setup.py @@ -0,0 +1,93 @@ +import textwrap + +from setuptools import setup + +with open("README.md") as fp: + readme = fp.read() + +long_description = "".join( + [ + section.split("")[0] + for section in readme.split("") + if "" in section + ] +) + +setup( + name="attrs-strict", + description="Runtime validators for attrs", + long_description=long_description, + long_description_content_type="text/markdown", + author="Erik-Cristian Seulean", + author_email="eseulean@bloomberg.net", + license="Apache 2.0", + packages=["attrs_strict"], + install_requires=["attrs", "typing; python_version<'3.5'"], + tests_require=["mock", "pytest"], + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4", + url="https://github.com/bloomberg/attrs-strict", + project_urls={ + "Source": "https://github.com/bloomberg/attrs-strict", + "Tracker": "https://github.com/bloomberg/attrs-strict/issues", + "Documentation": "https://github.com/bloomberg/attrs-strict/blob/" + "master/README.md#attrs-runtime-validation", + }, + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + ], + use_scm_version={ + "write_to": "attrs_strict/_version.py", + "write_to_template": textwrap.dedent( + """ + __version__ = {version!r} + # -------------------------------------------------------------------------- + # 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 -------------------------------- + """ + ).lstrip(), + }, +) + +# ----------------------------------------------------------------------------- +# 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/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_base_types.py b/tests/test_base_types.py new file mode 100644 index 0000000..7c3839c --- /dev/null +++ b/tests/test_base_types.py @@ -0,0 +1,43 @@ +import attr +import pytest + +from attrs_strict import type_validator + + +@pytest.mark.parametrize( + "value, expected, actual", + [ + (3, str, int), + ("five", int, str), + (None, str, type(None)), + (2.3, int, float), + ], +) +def test_primitive_types(value, expected, actual): + @attr.s + class Something(object): + number = attr.ib(validator=type_validator(), type=expected) + + with pytest.raises(ValueError) as error: + Something(number=value) + + assert repr( + error.value + ) == "".format( + expected, value, actual + ) + + +def test_reassign_evaluate(): + @attr.s + class Something(object): + number = attr.ib(validator=type_validator(), type=str) + + x = Something(number="foo") + with pytest.raises(ValueError) as error: + x.number = 5 + attr.validate(x) + + assert repr( + error.value + ) == "".format(str, int) diff --git a/tests/test_container.py b/tests/test_container.py new file mode 100644 index 0000000..9390b2e --- /dev/null +++ b/tests/test_container.py @@ -0,0 +1,105 @@ +import typing + +import attr +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( + "items, types, message", + [ + ( + [1, 2, 3], + typing.Set, + "Smth must be {} (got [1, 2, 3] that is a {})".format( + typing.Set, list + ), + ), + ( + [1, 2, 3], + typing.Dict, + "Smth must be {} (got [1, 2, 3] that is a {})".format( + typing.Dict, list + ), + ), + ( + [1, 2, 3], + typing.Tuple, + "Smth must be {} (got [1, 2, 3] that is a {})".format( + typing.Tuple, list + ), + ), + ], +) +def test_container_is_not_of_expected_type_raises_TypeError( + items, types, message +): + validator = type_validator() + + attr = MagicMock() + attr.name = "Smth" + attr.type = types + + with pytest.raises(ValueError) as error: + validator(None, attr, items) + + repr_msg = "<{}>".format(message) + assert repr_msg == repr(error.value) + + +def test_does_not_raise_when_container_is_empty_and_allowed(): + items = [] + + validator = type_validator(True) + attr = MagicMock() + attr.type = typing.List[int] + + validator(None, attr, items) + + +def test_raises_when_container_is_empty_and_empty_ok_is_false(): + # GIVEN + items = [] + validator = type_validator(empty_ok=False) + attr = MagicMock() + attr.name = "Smth" + attr.type = str + + # WHEN + with pytest.raises(ValueError) as error: + validator(None, attr, items) + + # THEN + assert "Smth can not be empty and must be str" + "(got None)" == str(error.value) + + +def test_no_type_specified_is_fine(): + @attr.s + class Something(object): + numbers = attr.ib(validator=type_validator()) + + Something([1, 2, 3, 4]) + + +# ----------------------------------------------------------------------------- +# 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_dict.py b/tests/test_dict.py new file mode 100644 index 0000000..55c68ed --- /dev/null +++ b/tests/test_dict.py @@ -0,0 +1,45 @@ +import collections +from typing import DefaultDict, List + +import pytest + +from attrs_strict import type_validator + +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) diff --git a/tests/test_list.py b/tests/test_list.py new file mode 100644 index 0000000..ea4cae6 --- /dev/null +++ b/tests/test_list.py @@ -0,0 +1,92 @@ +from typing import 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]]]), + ], +) +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_module.py b/tests/test_module.py new file mode 100644 index 0000000..dd3b2ac --- /dev/null +++ b/tests/test_module.py @@ -0,0 +1,28 @@ +import attrs_strict + + +def test_version(): + assert hasattr(attrs_strict, "__version__") + assert hasattr(attrs_strict, "type_validator") + assert hasattr(attrs_strict, "AttributeTypeError") + assert hasattr(attrs_strict, "BadTypeError") + assert hasattr(attrs_strict, "UnionError") + assert hasattr(attrs_strict, "TupleError") + assert hasattr(attrs_strict, "TypeValidationError") + + +# ----------------------------------------------------------------------------- +# 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 new file mode 100644 index 0000000..a3428ab --- /dev/null +++ b/tests/test_tuple.py @@ -0,0 +1,47 @@ +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 ( + "" + ) == 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) diff --git a/tests/test_union.py b/tests/test_union.py new file mode 100644 index 0000000..bfbc73d --- /dev/null +++ b/tests/test_union.py @@ -0,0 +1,61 @@ +from typing import List, Union + +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( + "element, type_, error_message", + [ + ( + 2.0, + Union[int, str], + "Value of foo 2.0 is not of type typing.Union[int, str]", + ), + ( + [1, 2, "p"], + List[Union[None, int]], + ( + "Value of foo p is not of type typing.Union[NoneType, int] " + "in [1, 2, 'p']" + ), + ), + ], +) +def test_union_when_type_is_not_specified_raises(element, type_, error_message): + + validator = type_validator() + + attr = MagicMock() + attr.name = "foo" + attr.type = type_ + + with pytest.raises(ValueError) as error: + validator(None, attr, element) + + repr_msg = "<{}>".format(error_message) + assert repr_msg == repr(error.value) + + +@pytest.mark.parametrize( + "element, type_,", + [ + (2.0, Union[int, float]), + ([1, 2, None, 4, 5], List[Union[None, int]]), + (None, Union[int, None]), + ], +) +def test_union_not_raise_for_correct_values(element, type_): + validator = type_validator() + + attr = MagicMock() + attr.name = "foo" + attr.type = type_ + + validator(None, attr, element) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..05a81f0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,113 @@ +[tox] +envlist = + py27, + py36, + py37, + py38, + pypy, + pypy3, + coverage, + fix_lint, + docs, + package_description, +minversion = 3.7 +isolated_build = true +skip_missing_interpreters = true + +[testenv] +description = run the tests with pytest under {basepython} +setenv = PIP_DISABLE_VERSION_CHECK = 1 + COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} +passenv = http_proxy https_proxy no_proxy SSL_CERT_FILE PYTEST_* +deps = + pytest + pytest-cov + py{27,py}: mock + py{27,py}: typing + +changedir = tests +commands = python -m pytest \ + --cov attrs_strict \ + --cov-config "{toxinidir}/tox.ini" \ + --junitxml {toxworkdir}/junit.{envname}.xml \ + {posargs:.} + +[testenv:docs] +description = invoke sphinx-build to build the HTML docs +basepython = python3.7 +deps = + sphinx +commands = sphinx-build -d "{toxworkdir}/docs_doctree" "{toxinidir}/doc" "{toxworkdir}/docs_out" --color -W -bhtml {posargs} + python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' + +[testenv:package_description] +changedir = {toxinidir} +description = check that the long description is valid +basepython = python3.7 +deps = twine >= 1.12.1 + readme-renderer[md] >= 24.0 + pep517 +skip_install = true +commands = python -m pep517.build -s -b . -o {envtmpdir} + twine check {envtmpdir}/* + +[testenv:pypi_publish] +changedir = {toxinidir} +description = Upload a new package to pypi +deps = twine >= 1.12.1 + pep517 +passenv = TWINE_PASSWORD https_proxy no_proxy +skip_install = true +commands = python -m pep517.build -s -b . -o {envtmpdir} + twine upload --verbose -u __token__ {envtmpdir}/* + +[testenv:fix_lint] +description = format the code base to adhere to our styles, and complain about what we cannot do automatically +basepython = python3.7 +changedir = {toxinidir} +passenv = {[testenv]passenv} + # without PROGRAMDATA cloning using git for Windows will fail with an + # `error setting certificate verify locations` error + PROGRAMDATA +deps = pre-commit >= 1.14.4, < 2 +skip_install = True +commands = pre-commit run --all-files --show-diff-on-failure + python -c 'import pathlib; print("hint: run \{\} install to add checks as pre-commit hook".format(pathlib.Path(r"{envdir}") / "bin" / "pre-commit"))' + +[isort] +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +line_length = 80 +known_first_party = attrs_strict,tests +known_third_party = attr,pytest,setuptools + +[flake8] +max-complexity = 22 +max-line-length = 80 + +[pep8] +max-line-length = 80 + +[coverage:run] +branch = true +parallel = true + +[coverage:report] +skip_covered = True +show_missing = True +exclude_lines = + \#\s*pragma: no cover + ^\s*raise AssertionError\b + ^\s*raise NotImplementedError\b + ^\s*return NotImplemented\b + ^\s*raise$ + ^if __name__ == ['"]__main__['"]:$ + +[coverage:paths] +source = attrs_strict + */.tox/*/lib/python*/site-packages/attrs_strict + */.tox/pypy*/site-packages/attrs_strict + */.tox\*\Lib\site-packages\attrs_strict + */attrs_strict + *\attrs_strict