├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Code-of-Conduct.md ├── LICENSE-APACHE-2.0 ├── LICENSE-MIT ├── README.md ├── abserde ├── __init__.py ├── __main__.py ├── config.py ├── gen_crate.py ├── gen_lib.py ├── main.py ├── py.typed └── template_crate │ ├── Cargo.toml.in │ └── src │ └── lib.rs ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── help.rst │ ├── index.rst │ ├── install.rst │ └── usage.rst ├── examples ├── forward.pyi ├── multiclass.pyi ├── nested.pyi ├── simple.pyi ├── twitter.pyi └── union.pyi ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── tests ├── __init__.py ├── large_bench.png ├── small_bench.png ├── test_benchmark.py ├── test_multiclass.py └── twitter.json └── tox.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | tags: ['*'] 6 | paths-ignore: 7 | - .gitignore 8 | - LICENSE 9 | - README.md 10 | - docs/ 11 | - tests/*.png 12 | pull_request: 13 | paths-ignore: 14 | - .gitignore 15 | - LICENSE 16 | - README.md 17 | 18 | jobs: 19 | tests: 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | name: [py36, py37, py38] 25 | os: [windows-latest, ubuntu-latest, macos-latest] 26 | include: 27 | - name: py36 28 | python-version: 3.6 29 | tox: py36 30 | - name: py37 31 | python-version: 3.7 32 | tox: py37 33 | - name: py38 34 | python-version: 3.8 35 | tox: py38 36 | 37 | steps: 38 | - uses: actions/checkout@v1 39 | - name: Set up Python ${{ matrix.python-version }} 40 | uses: actions/setup-python@v1 41 | with: 42 | python-version: ${{ matrix.python-version }} 43 | - name: Install latest nightly 44 | uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: nightly 47 | default: true 48 | - name: install abserde 49 | run: python -m pip install . 50 | - name: install tox 51 | run: python -m pip install tox 52 | - name: build multiclass for tests 53 | run: python -m abserde examples/multiclass.pyi 54 | - name: build twitter for tests 55 | run: python -m abserde examples/twitter.pyi 56 | - name: run tests 57 | run: python -m tox -e ${{ matrix.tox }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.7 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: debug-statements 9 | - id: check-ast 10 | - id: check-builtin-literals 11 | - id: check-byte-order-marker 12 | - id: check-case-conflict 13 | - id: check-docstring-first 14 | - id: check-merge-conflict 15 | - id: check-vcs-permalinks 16 | - id: debug-statements 17 | - id: detect-private-key 18 | - id: end-of-file-fixer 19 | - id: file-contents-sorter 20 | - id: mixed-line-ending 21 | - repo: https://gitlab.com/pycqa/flake8 22 | rev: 3.7.9 23 | hooks: 24 | - id: flake8 25 | - repo: https://github.com/asottile/reorder_python_imports 26 | rev: v1.9.0 27 | hooks: 28 | - id: reorder-python-imports 29 | - repo: https://github.com/asottile/pyupgrade 30 | rev: v1.26.2 31 | hooks: 32 | - id: pyupgrade 33 | args: ['--py36-plus'] 34 | - repo: https://github.com/Lucas-C/pre-commit-hooks 35 | rev: v1.1.7 36 | hooks: 37 | - id: remove-tabs 38 | -------------------------------------------------------------------------------- /Code-of-Conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at konstin@mailbox.org. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE-APACHE-2.0: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/ethanhs/abserde/workflows/CI/badge.svg)](https://github.com/ethanhs/abserde/actions) 2 | 3 | # Abserde 4 | 5 | Leveraging [serde](https://serde.rs/) to make fast JSON serializers/deserializers for Python. 6 | 7 | The main idea is you feed in a Python stub declaring the interface you want and get a fast JSON parser implemented in Rust. 8 | 9 | Note abserde is basically usable, but I have not stablized the API yet. 10 | 11 | 12 | # Roadmap 13 | 14 | 1. Design API and features ✅ 15 | 16 | 2. Write documentation ✅ 17 | 18 | 3. Write tests ✅ 19 | 20 | 4. Write mypy plugin 21 | 22 | # Usage 23 | 24 | You need [poetry](https://github.com/sdispater/poetry#installation) for now until packages are built. 25 | 26 | You need [rust nightly](https://rustup.rs/) to use this tool (but not for the final extension!) 27 | 28 | Then you can then 29 | ``` 30 | $ git clone https://github.com/ethanhs/abserde.git 31 | $ cd abserde 32 | $ poetry install 33 | ``` 34 | 35 | And then 36 | 37 | ``` 38 | $ poetry run abserde examples/multiclass.pyi 39 | ``` 40 | 41 | The multiclass stub file looks like this: 42 | 43 | ```python 44 | from abserde import abserde 45 | from typing import Any 46 | 47 | @abserde 48 | class Test: 49 | room: int 50 | floor: int 51 | 52 | 53 | @abserde 54 | class Test2: 55 | name: Any 56 | age: int 57 | foo: Test 58 | ``` 59 | 60 | You should find a wheel which you can install via: 61 | ``` 62 | $ poetry run pip install dist/multiclass-*.whl 63 | ``` 64 | 65 | And run Python in the environment with: 66 | ``` 67 | $ poetry run python 68 | ``` 69 | 70 | You should now be able to import the `multiclass` module, which you can use to serialize and deserialize with Python. 71 | 72 | ```python 73 | # modules are called the name of the stub 74 | >>> import multiclass 75 | # you can load objects from a string as you would expect 76 | >>> t = multiclass.Test.loads('{"room": 3, "floor": 9}') 77 | # and dump them 78 | >>> t.dumps() 79 | '{"room":3,"floor":9}' 80 | # they display nicely 81 | >>> t 82 | Test(room=3, floor=9) 83 | # members can be accessed as attributes 84 | >>> t.room 85 | 3 86 | # you can also set them 87 | >>> t.floor = 5 88 | >>> t 89 | Test(room=3, floor=5) 90 | # you can use subscripts if you prefer 91 | >>> t['floor'] 92 | 5 93 | # you can create instances the usual way 94 | >>> t2 = multiclass.Test2(name='Guido',age=63,foo=t) 95 | >>> t2 96 | Test2(age=39, name=6, foo=Test(room=3, floor=4)) 97 | # types annotated Any, such as "name" here, can be any JSON type 98 | # I am not a number, I'm a free man! 99 | >>> t2['name'] = "The Prisoner" 100 | ``` 101 | 102 | # Performance 103 | 104 | Initial rough benchmarks (see `tests/test_benchmark.py`) give the following results compared to `ujson`, `orjson` and the stdlib `json`. 105 | 106 | The first benchmark tests a small json blob loading/dumping: 107 | 108 | ![Small JSON benchmark](tests/small_bench.png) 109 | 110 | The next benchmark demonstrates that abserde is 2.4x faster at the standard twitter.json benchmark compared to `ujson`! 111 | 112 | ![Large JSON benchmark](tests/large_bench.png) 113 | 114 | # LICENSE 115 | 116 | Abserde is dual licensed Apache 2.0 and MIT. 117 | 118 | # Code of Conduct 119 | 120 | Abserde is under the Contributor Covenant Code of Conduct. I will enforce it. 121 | 122 | # Thank you 123 | 124 | Thanks to the amazing efforts of the developers of Rust, Serde, and PyO3. Without any of these, this project would not be possible. 125 | -------------------------------------------------------------------------------- /abserde/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | 3 | from typing import Type, TypeVar 4 | 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | def abserde(c: Type[T]) -> Type[T]: 10 | return c 11 | -------------------------------------------------------------------------------- /abserde/__main__.py: -------------------------------------------------------------------------------- 1 | from abserde.main import main 2 | 3 | 4 | main() 5 | -------------------------------------------------------------------------------- /abserde/config.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class Config: 6 | filename: str 7 | debug: bool 8 | name: str 9 | email: str 10 | -------------------------------------------------------------------------------- /abserde/gen_crate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | from distutils.dir_util import copy_tree 5 | from pathlib import Path 6 | 7 | from abserde.config import Config 8 | 9 | 10 | def generate_crate(mod: str, config: Config) -> None: 11 | dir = Path.cwd() / "build" / "abserde" 12 | template_dir = Path(__file__).parent / "template_crate" 13 | crate_name = config.filename 14 | crate_dir = dir / crate_name 15 | copy_tree(str(template_dir), str(crate_dir)) 16 | with open(crate_dir / "Cargo.toml.in") as c: 17 | src = c.read() 18 | formatted = src.format(file=crate_name, name=config.name, email=config.email) 19 | if config.debug: 20 | print("Cargo.toml:") 21 | print(formatted) 22 | with open(crate_dir / "Cargo.toml", "w") as w: 23 | w.write(formatted) 24 | 25 | with open(crate_dir / "src" / "lib.rs", "w+") as lib: 26 | lib.write(mod) 27 | cmd = ["maturin", "build", "-i", sys.executable, "--manylinux", "1-unchecked"] 28 | env = os.environ.copy() 29 | if not config.debug: 30 | cmd.append("--release") 31 | env['RUSTFLAGS'] = "-C target-cpu=native" 32 | p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=crate_dir, env=env) 33 | sys.stdout.buffer.write(p.stdout) 34 | sys.stdout.buffer.write(b'\n') 35 | sys.stderr.buffer.write(p.stderr) 36 | sys.stderr.buffer.write(b'\n') 37 | wheelhouse = crate_dir / "target" / "wheels" 38 | if config.debug: 39 | print("Generated wheel") 40 | print(*wheelhouse.iterdir()) 41 | built_wheel = Path.cwd() / "dist" 42 | copy_tree(str(wheelhouse), str(built_wheel)) 43 | -------------------------------------------------------------------------------- /abserde/gen_lib.py: -------------------------------------------------------------------------------- 1 | from ast import AnnAssign 2 | from ast import AST 3 | from ast import ClassDef 4 | from ast import Module 5 | from ast import Name 6 | from ast import NodeVisitor 7 | from ast import parse 8 | from ast import Subscript 9 | from typing import List 10 | from typing import NoReturn 11 | from typing import Optional 12 | from typing import Set 13 | from typing import Tuple 14 | 15 | from abserde.config import Config 16 | 17 | 18 | LIB_USES = """ 19 | #![feature(specialization)] 20 | #[allow(dead_code)] 21 | use pyo3::prelude::*; 22 | use pyo3::wrap_pyfunction; 23 | use pyo3::exceptions; 24 | use pyo3::PyObjectProtocol; 25 | use pyo3::types::{PyBytes, PyType, IntoPyDict, PyString, PyAny}; 26 | use pyo3::create_exception; 27 | use pyo3::PyMappingProtocol; 28 | use pyo3::ffi::Py_TYPE; 29 | use pyo3::AsPyPointer; 30 | use pyo3::types::PyDict; 31 | use pyo3::class::basic::CompareOp; 32 | 33 | use serde::{Deserialize, Serialize}; 34 | #[allow(unused_imports)] 35 | use std::ops::Deref; 36 | use std::fmt; 37 | """ 38 | 39 | STRUCT_PREFIX = """ 40 | #[pyclass(dict)] 41 | #[derive(Serialize, Deserialize, Clone, PartialEq)] 42 | pub struct {name} {{ 43 | """ 44 | 45 | PYCLASS_PREFIX = """ 46 | #[pymethods] 47 | impl {name} {{ 48 | fn dumps(&self) -> PyResult {{ 49 | dumps_impl(self) 50 | }} 51 | 52 | #[classmethod] 53 | fn loads(_cls: &PyType, s: &PyString) -> PyResult {{ 54 | match serde_json::from_str::<{name}>(&s.to_string_lossy()) {{ 55 | Ok(v) => Ok(v), 56 | Err(e) => Err(JSONParseError::py_err(e.to_string())), 57 | }} 58 | }} 59 | """ 60 | 61 | IMPL_NEW_PREFIX = """ 62 | #[new] 63 | fn new({args}) -> Self {{ 64 | {{ 65 | {name} {{ 66 | """ 67 | 68 | IMPL_NEW_SUFFIX = """ 69 | } 70 | } 71 | } 72 | """ 73 | 74 | OBJECT_PROTO = """ 75 | #[allow(unused)] 76 | #[pyproto] 77 | impl<'p> PyObjectProtocol<'p> for {name} {{ 78 | """ 79 | 80 | DUNDER_STR = """ 81 | fn __str__(&self) -> PyResult { 82 | match serde_json::to_string(&self) { 83 | Ok(v) => Ok(v), 84 | Err(e) => Err(exceptions::ValueError::py_err(e.to_string())) 85 | } 86 | } 87 | """ 88 | 89 | DUNDER_BYTES = """ 90 | fn __bytes__(&self) -> PyResult { 91 | let gil = GILGuard::acquire(); 92 | match serde_json::to_vec(&self) { 93 | Ok(v) => Ok(PyBytes::new(gil.python(), &v).into()), 94 | Err(e) => Err(exceptions::ValueError::py_err(e.to_string())) 95 | } 96 | } 97 | """ 98 | 99 | DUNDER_RICHCMP = """ 100 | fn __richcmp__(&self, other: Self, op: CompareOp) -> PyResult { 101 | Ok(match op { 102 | CompareOp::Eq => *self == other, 103 | CompareOp::Ne => *self != other, 104 | _ => false, 105 | }) 106 | } 107 | """ 108 | 109 | DUNDER_REPR = """ 110 | fn __repr__(&self) -> PyResult {{ 111 | let gil = GILGuard::acquire(); 112 | let py = gil.python(); 113 | Ok(format!("{name}({args})", {attrs})) 114 | }} 115 | """ 116 | 117 | DISPLAY_IMPL = """ 118 | #[allow(unused)] 119 | impl fmt::Debug for {name} {{ 120 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {{ 121 | let gil = GILGuard::acquire(); 122 | let py = gil.python(); 123 | write!(f, "{{}}", format!("{name}({args})", {attrs})) 124 | }} 125 | }} 126 | """ 127 | 128 | MAPPING_IMPL = """ 129 | #[pyproto] 130 | impl<'p> PyMappingProtocol<'p> for {name} {{ 131 | fn __len__(&'p self) -> PyResult {{ 132 | Ok({len}usize) 133 | }} 134 | fn __getitem__(&'p self, key: String) -> PyResult {{ 135 | let gil = GILGuard::acquire(); 136 | let py = gil.python(); 137 | match key.as_ref() {{ 138 | {getitems} 139 | &_ => Err(exceptions::AttributeError::py_err(format!("No such item {{}}", key))), 140 | }} 141 | }} 142 | fn __setitem__(&'p mut self, key: String, value: PyObject) -> PyResult<()> {{ 143 | let gil = GILGuard::acquire(); 144 | let py = gil.python(); 145 | match key.as_ref() {{ 146 | {setitems} 147 | &_ => Err(exceptions::AttributeError::py_err(format!("No such item {{}}", key))), 148 | }} 149 | }} 150 | }} 151 | """ 152 | 153 | ENUM_IMPL_PREFIX = """ 154 | impl IntoPy for {name} {{ 155 | fn into_py(self, py: Python) -> PyObject {{ 156 | match self {{ 157 | """ 158 | 159 | ENUM_IMPL_SUFFIX = """ 160 | }} 161 | }} 162 | }} 163 | 164 | impl<'source> pyo3::FromPyObject<'source> for {name} {{ 165 | fn extract(ob: &'source PyAny) -> pyo3::PyResult<{name}> {{ 166 | """ 167 | 168 | ENUM_DECL = """ 169 | {{Err(exceptions::ValueError::py_err("Could not convert object to any of {types}.")) }} 170 | }} 171 | }} 172 | 173 | 174 | #[derive(Serialize, Deserialize, Clone)] 175 | #[serde(untagged)] 176 | pub enum {name} {{ 177 | """ 178 | 179 | ENUM_IMPL_DEBUG_PREFIX = """ 180 | impl fmt::Debug for {name} {{ 181 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {{ 182 | match self {{ 183 | """ 184 | 185 | ENUM_IMPL_DEBUG_SUFFIX = """ 186 | } 187 | } 188 | } 189 | """ 190 | 191 | DUMPS_IMPL_PREFIX = """ 192 | fn dumps_impl(c: &T) -> PyResult 193 | where T: Serialize 194 | { 195 | match serde_json::to_string(c) { 196 | Ok(v) => Ok(v), 197 | Err(e) => Err(exceptions::ValueError::py_err(e.to_string())) 198 | } 199 | } 200 | 201 | /// dumps(s, /) 202 | /// -- 203 | /// 204 | /// Dump abserde class s into a str. 205 | /// For bytes, call bytes() on the object. 206 | #[pyfunction] 207 | pub fn dumps(c: PyObject, py: Python) -> PyResult { 208 | """ 209 | DUMPS_FOR_CLS = """ 210 | if let Ok(o) = c.extract::<{cls}>(py) {{ 211 | dumps_impl(&o) 212 | }}""" 213 | DUMPS_IMPL_SUFFIX = """ 214 | else { 215 | Err(exceptions::ValueError::py_err("Invalid type for dumps")) 216 | } 217 | } 218 | """ 219 | 220 | MODULE_PREFIX = """ 221 | /// loads(s, /) 222 | /// -- 223 | /// 224 | /// Parse s into an abserde class. 225 | /// s can be a str, byte, or bytearray. 226 | #[pyfunction] 227 | pub fn loads<'a>(s: PyObject, py: Python) -> PyResult {{ 228 | let bytes = if let Ok(string) = s.cast_as::(py) {{ 229 | string.as_bytes() 230 | }} else if let Ok(bytes) = s.cast_as::(py) {{ 231 | Ok(bytes.as_bytes()) 232 | }} else {{ 233 | let ty = unsafe {{ 234 | let p = s.as_ptr(); 235 | let tp = Py_TYPE(p); 236 | PyType::from_type_ptr(py, tp) 237 | }}; 238 | if ty.name() == "bytearray" {{ 239 | let locals = [("bytesobj", s)].into_py_dict(py); 240 | let bytes = py.eval("bytes(bytesobj)", None, Some(&locals))?.downcast::()?; 241 | Ok(bytes.as_bytes()) 242 | }} else {{ 243 | Err(exceptions::ValueError::py_err(format!("loads() takes only str, bytes, or bytearray, got {{}}", ty))) 244 | }} 245 | }}?; 246 | match serde_json::from_slice::(bytes) {{ 247 | Ok(v) => Ok(v), 248 | Err(e) => Err(JSONParseError::py_err(e.to_string())), 249 | }} 250 | }} 251 | 252 | #[derive(Serialize, Deserialize, Clone, PartialEq)] 253 | #[serde(transparent)] 254 | pub struct JsonValue(serde_json::Value); 255 | 256 | impl fmt::Debug for JsonValue {{ 257 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {{ 258 | write!(f, "{{}}", self.0) 259 | }} 260 | }} 261 | 262 | impl IntoPy for JsonValue {{ 263 | fn into_py(self, py: Python) -> PyObject {{ 264 | match self.0 {{ 265 | serde_json::Value::Null => py.None(), 266 | serde_json::Value::Bool(b) => b.into_py(py), 267 | serde_json::Value::Number(n) => {{ 268 | if n.is_i64() {{ 269 | // should never fail since we just checked it 270 | n.as_i64().unwrap().into_py(py) 271 | }} else {{ 272 | // should never fail since it isn't an integer? 273 | n.as_f64().unwrap().into_py(py) 274 | }} 275 | }}, 276 | serde_json::Value::String(s) => s.into_py(py), 277 | serde_json::Value::Array(v) => {{ 278 | let vec: Vec = v.iter().map(|i| JsonValue(i.clone()).into_py(py)).collect(); 279 | vec.into_py(py) 280 | }}, 281 | serde_json::Value::Object(m) => {{ 282 | let d = PyDict::new(py); 283 | for (k, v) in m.iter() {{ 284 | let key = PyString::new(py, &*k); 285 | let value: PyObject = JsonValue(v.clone()).into_py(py); 286 | d.set_item(key, value).unwrap(); 287 | }} 288 | d.into_py(py) 289 | }} 290 | }} 291 | }} 292 | }} 293 | 294 | impl<'source> pyo3::FromPyObject<'source> for JsonValue {{ 295 | fn extract(ob: &'source PyAny) -> pyo3::PyResult {{ 296 | if let Ok(s) = ob.extract::() {{ 297 | Ok(JsonValue(serde_json::Value::String(s))) 298 | }} else if let Ok(n) = ob.extract::() {{ 299 | Ok(JsonValue(serde_json::Value::Number(n.into()))) 300 | }} else if let Ok(f) = ob.extract::() {{ 301 | let flt = match serde_json::Number::from_f64(f) {{ 302 | Some(v) => Ok(v), 303 | None => Err(exceptions::ValueError::py_err("Cannot convert NaN or inf to JSON")), 304 | }}; 305 | Ok(JsonValue(serde_json::Value::Number(flt?))) 306 | }} else if let Ok(b) = ob.extract::() {{ 307 | Ok(JsonValue(serde_json::Value::Bool(b))) 308 | }} else if let Ok(l) = ob.extract::>() {{ 309 | Ok(JsonValue(serde_json::Value::Array(l.into_iter().map(|i| i.0).collect()))) 310 | }} else if let Ok(d) = ob.extract::<&PyDict>() {{ 311 | let mut m = serde_json::Map::with_capacity(d.len()); 312 | for (k, v) in d.iter() {{ 313 | let key: String = k.extract()?; 314 | let value: JsonValue = v.extract()?; 315 | m.insert(key, value.0); 316 | }} 317 | Ok(JsonValue(serde_json::Value::Object(m))) 318 | }} else if ob.extract::()?.is_none() {{ 319 | Ok(JsonValue(serde_json::Value::Null)) 320 | }} else {{ 321 | Err(exceptions::ValueError::py_err("Could not convert object to JSON")) 322 | }} 323 | }} 324 | }} 325 | 326 | create_exception!({module}, JSONParseError, exceptions::ValueError); 327 | 328 | #[pymodule] 329 | fn {module}(py: Python, m: &PyModule) -> PyResult<()> {{ 330 | """ # noqa 331 | 332 | MODULE_SUFFIX = """ 333 | m.add_wrapped(wrap_pyfunction!(loads))?; 334 | m.add_wrapped(wrap_pyfunction!(dumps))?; 335 | m.add("JSONParseError", py.get_type::())?; 336 | Ok(()) 337 | } 338 | """ 339 | 340 | SIMPLE_TYPE_MAP = { 341 | "str": "String", 342 | "int": "i64", 343 | "bool": "bool", 344 | "float": "f64", 345 | "Any": "JsonValue" 346 | } 347 | 348 | CONTAINER_TYPE_MAP = {"List": "Vec", "Optional": "Option"} 349 | 350 | 351 | RUST_KEYWORDS = [ 352 | 'abstract', 353 | 'become', 354 | 'box', 355 | 'do', 356 | 'final', 357 | 'macro', 358 | 'override', 359 | 'priv', 360 | 'typeof', 361 | 'unsized', 362 | 'virtual', 363 | 'yield', 364 | 'try', 365 | 'as', 366 | 'break', 367 | 'const', 368 | 'continue', 369 | 'crate', 370 | 'else', 371 | 'enum', 372 | 'extern', 373 | 'false', 374 | 'fn', 375 | 'for', 376 | 'if', 377 | 'impl', 378 | 'in', 379 | 'let', 380 | 'loop', 381 | 'match', 382 | 'mod', 383 | 'move', 384 | 'mut', 385 | 'pub', 386 | 'ref', 387 | 'return', 388 | 'self', 389 | 'Self', 390 | 'static', 391 | 'struct', 392 | 'super', 393 | 'trait', 394 | 'true', 395 | 'type', 396 | 'unsafe', 397 | 'use', 398 | 'where', 399 | 'while', 400 | 'async', 401 | 'await', 402 | 'dyn', 403 | ] 404 | 405 | 406 | class InvalidTypeError(Exception): 407 | pass 408 | 409 | 410 | def invalid_type(typ: str, container: Optional[str] = None) -> NoReturn: 411 | if container is not None: 412 | msg = f"The type {container}[{typ}] is not valid." 413 | else: 414 | msg = f"The type {typ} is not valid." 415 | raise InvalidTypeError(msg) 416 | 417 | 418 | class ClassGatherer(NodeVisitor): 419 | classes: List[str] 420 | unions: Set[Tuple[str, ...]] 421 | 422 | def __init__(self): 423 | self.classes = [] 424 | self.unions = set() 425 | 426 | def visit_ClassDef(self, n: ClassDef) -> None: 427 | self.classes.append(n.name) 428 | for item in n.body: 429 | if isinstance(item, AnnAssign): 430 | if isinstance(item.annotation, Subscript): 431 | typ = item.annotation 432 | if typ.value.id == "Union": 433 | self.unions.add(n for n in typ.slice.value.elts) 434 | 435 | def run(self, n) -> Tuple[List[str], Set[Tuple[str, ...]]]: 436 | self.visit(n) 437 | return self.classes, self.unions 438 | 439 | 440 | class StubVisitor(NodeVisitor): 441 | def __init__(self, config: Config, classes: List[str], unions: Set[Tuple[str, ...]]): 442 | self.config = config 443 | self.unions = unions 444 | self.classes = classes 445 | self.lib = "" 446 | 447 | def convert(self, n: AST) -> str: 448 | """Turn types like List[str] into types like Vec""" 449 | if isinstance(n, Name): 450 | return self._convert_simple(n.id) 451 | if isinstance(n, Subscript): 452 | return f'{CONTAINER_TYPE_MAP[n.value.id]}<{self.convert(n.slice.value)}>' 453 | 454 | def _convert_simple(self, typ: str) -> str: 455 | """Utility method to convert Python annotations to Rust types""" 456 | if typ in self.classes: 457 | return f"{typ}" 458 | try: 459 | return f"{SIMPLE_TYPE_MAP[typ]}" 460 | except KeyError: 461 | invalid_type(typ) 462 | 463 | def escape_keywords(self, name): 464 | if name in RUST_KEYWORDS: 465 | return f'r#{name}' 466 | else: 467 | return name 468 | 469 | def is_union(self, item: AST) -> bool: 470 | return isinstance(item.annotation, Subscript) and item.annotation.value.id == "Union" 471 | 472 | def generate_lib(self, n: Module) -> str: 473 | self.visit(n) 474 | return self.lib 475 | 476 | def write(self, s: str) -> None: 477 | self.lib += s 478 | 479 | def writeline(self, s: str) -> None: 480 | self.lib += s + "\n" 481 | 482 | def write_enum(self, name: str, members: List[str]) -> None: 483 | self.write(ENUM_IMPL_PREFIX.format(name=name)) 484 | for elt in members: 485 | typ = elt.replace('<', '').replace('>', '') 486 | self.writeline(" " * 12 + f"{name}::{typ}Type(v) => v.into_py(py),") 487 | self.write(ENUM_IMPL_SUFFIX.format(name=name)) 488 | for elt in members: 489 | typ = elt.replace('<', '').replace('>', '') 490 | self.writeline(f"""if let Ok(t) = ob.extract::<{elt}>() {{ 491 | Ok({name}::{typ}Type(t)) }} else """) 492 | types = " ".join(members) 493 | self.writeline(ENUM_DECL.format(name=name, types=types)) 494 | for elt in members: 495 | typ = elt.replace('<', '').replace('>', '') 496 | self.writeline("#[allow(non_camel_case_types)]\n" + " " * 4 + f"{typ}Type({elt}),") 497 | self.writeline("}") 498 | self.writeline(ENUM_IMPL_DEBUG_PREFIX.format(name=name)) 499 | for elt in members: 500 | typ = elt.replace('<', '').replace('>', '') 501 | self.writeline(" " * 12 + f'{name}::{typ}Type(v) => write!(f, "{{:?}}", v),') 502 | self.writeline(ENUM_IMPL_DEBUG_SUFFIX) 503 | 504 | def visit_Module(self, n: Module) -> None: 505 | self.write(LIB_USES) 506 | self.generic_visit(n) 507 | module = self.config.filename 508 | self.write_enum("Classes", self.classes) 509 | self.write(DUMPS_IMPL_PREFIX) 510 | self.write(" else ".join(DUMPS_FOR_CLS.format(cls=cls) for cls in self.classes)) 511 | self.write(DUMPS_IMPL_SUFFIX) 512 | self.write(MODULE_PREFIX.format(module=module)) 513 | for cls in self.classes: 514 | self.writeline(" " * 4 + f"m.add_class::<{cls}>()?;") 515 | self.write(MODULE_SUFFIX) 516 | if self.config.debug: 517 | print(f"Generated Rust for: {self.config.filename}") 518 | 519 | def visit_ClassDef(self, n: ClassDef) -> None: 520 | decorators = n.decorator_list 521 | if ( 522 | len(decorators) != 1 523 | or not isinstance(decorators[0], Name) 524 | or decorators[0].id != "abserde" 525 | ): 526 | if self.config.debug: 527 | print("Skipping class {n.name}") 528 | self.generic_visit(n) 529 | return 530 | # First, we write out the struct and its members 531 | self.write(STRUCT_PREFIX.format(name=n.name)) 532 | attributes: List[Tuple[str, str]] = [] 533 | # enums that need to be generated later 534 | enums: List[Tuple[str, Tuple[str, ...]]] = [] 535 | for item in n.body: 536 | if isinstance(item, AnnAssign): 537 | assert isinstance(item.target, Name) 538 | name = item.target.id 539 | if self.is_union(item): 540 | id = len(self.unions) 541 | annotation = f'Union{id}' 542 | members = (n for n in item.annotation.slice.value.elts) 543 | self.unions.discard(members) 544 | enums.append((annotation, members)) 545 | else: 546 | annotation = self.convert(item.annotation) 547 | assert annotation is not None, print(n.name, item.target.id) 548 | attributes.append((self.escape_keywords(name), annotation)) 549 | self.writeline(" " * 4 + "#[pyo3(get, set)]") 550 | self.writeline(" " * 4 + f"pub {self.escape_keywords(name)}: {annotation},") 551 | self.writeline("}") 552 | # Then we write out the class implementation. 553 | self.write(PYCLASS_PREFIX.format(name=n.name)) 554 | args = ", ".join(f"{name}: {typ}" for name, typ in attributes) 555 | self.write(IMPL_NEW_PREFIX.format(args=args, name=n.name)) 556 | for pair in attributes: 557 | name = pair[0] 558 | self.writeline(" " * 16 + f"{name}: {name},") 559 | self.write(IMPL_NEW_SUFFIX) 560 | self.writeline("}") 561 | # write out needed enum types 562 | for name, members in enums: 563 | self.write_enum(name, [self.convert(n) for n in members]) 564 | getitem = ("\n" + " " * 12).join( 565 | f'"{name}" => Ok(self.{name}.clone().into_py(py)),' for name, _ in attributes 566 | ) 567 | setitem = ("\n" + " " * 12).join( 568 | f'"{name}" => Ok(self.{name} = value.extract(py)?),' for name, _ in attributes 569 | ) 570 | self.write( 571 | MAPPING_IMPL.format( 572 | name=n.name, len=len(attributes), getitems=getitem, setitems=setitem 573 | ) 574 | ) 575 | self.write(OBJECT_PROTO.format(name=n.name)) 576 | self.write(DUNDER_STR) 577 | repr_args = ", ".join(f"{name}={{{name}:?}}" for name, _ in attributes) 578 | names = ", ".join(f"{name} = self.{name}" if not typ.startswith('Py<') 579 | else f"{name} = self.{name}.as_ref(py).deref()" 580 | for name, typ in attributes) 581 | self.write(DUNDER_REPR.format(name=n.name, args=repr_args, attrs=names)) 582 | self.write(DUNDER_RICHCMP) 583 | self.writeline("}") 584 | self.write(DISPLAY_IMPL.format(name=n.name, args=repr_args, attrs=names)) 585 | 586 | 587 | def gen_bindings(src: str, config: Config) -> str: 588 | mod = parse(src) 589 | assert isinstance(mod, Module) 590 | gatherer = ClassGatherer() 591 | classes, unions = gatherer.run(mod) 592 | visitor = StubVisitor(config, classes, unions) 593 | return visitor.generate_lib(mod) 594 | -------------------------------------------------------------------------------- /abserde/main.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import click 4 | 5 | from abserde.config import Config 6 | from abserde.gen_crate import generate_crate 7 | from abserde.gen_lib import gen_bindings 8 | 9 | 10 | @click.command() 11 | @click.argument("file") 12 | @click.option("-d", "--debug", "debug", is_flag=True, help="Print more output.") 13 | @click.option("-n", "--name", "name", help="Name for package.") 14 | @click.option("-e", "--email", "email", help="Email for package.") 15 | def main(file: str, debug: bool, name: str, email: str) -> None: 16 | file_name = Path(file).name.replace(".pyi", "").replace(".py", "") 17 | config = Config(file_name, debug, name, email) 18 | with open(file) as f: 19 | src = f.read() 20 | mod = gen_bindings(src, config) 21 | if config.debug: 22 | print(mod) 23 | generate_crate(mod, config) 24 | -------------------------------------------------------------------------------- /abserde/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmatyping/abserde/b448d2bb5a1a59e1ce65f62a3c20a7009bb77071/abserde/py.typed -------------------------------------------------------------------------------- /abserde/template_crate/Cargo.toml.in: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{file}" 3 | version = "0.1.0" 4 | authors = ["{name} <{email}>"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serde = {{ version = "1.0", features = ["derive"] }} 9 | serde_json = "1.0" 10 | libc = "0.2" 11 | 12 | [dependencies.pyo3] 13 | version = "0.9.2" 14 | features = ["extension-module"] 15 | 16 | [lib] 17 | name = "{file}" 18 | crate-type = ["cdylib"] 19 | 20 | [profile.release] 21 | lto = true 22 | codegen-units = 1 23 | opt-level = 3 24 | -------------------------------------------------------------------------------- /abserde/template_crate/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This space left intentionally blank (otherwise the file won't get copied...) 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | # -- Path setup -------------------------------------------------------------- 7 | # If extensions (or modules to document with autodoc) are in another directory, 8 | # add these directories to sys.path here. If the directory is relative to the 9 | # documentation root, use os.path.abspath to make it absolute, like shown here. 10 | # 11 | # import os 12 | # import sys 13 | # sys.path.insert(0, os.path.abspath('.')) 14 | # -- Project information ----------------------------------------------------- 15 | 16 | project = 'abserde' 17 | copyright = '2020, Ethan Smith' 18 | author = 'Ethan Smith' 19 | 20 | # The full version, including alpha/beta/rc tags 21 | release = '0.1.0' 22 | 23 | 24 | # -- General configuration --------------------------------------------------- 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = [ 30 | 'recommonmark', 31 | 'sphinxcontrib.programoutput' 32 | ] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | # This pattern also affects html_static_path and html_extra_path. 40 | exclude_patterns = [] 41 | 42 | 43 | # -- Options for HTML output ------------------------------------------------- 44 | 45 | # The theme to use for HTML and HTML Help pages. See the documentation for 46 | # a list of builtin themes. 47 | # 48 | html_theme = 'alabaster' 49 | 50 | # Add any paths that contain custom static files (such as style sheets) here, 51 | # relative to this directory. They are copied after the builtin static files, 52 | # so a file named "default.css" will overwrite the builtin "default.css". 53 | html_static_path = ['_static'] 54 | -------------------------------------------------------------------------------- /docs/source/help.rst: -------------------------------------------------------------------------------- 1 | Help output for abserde 2 | ======================= 3 | 4 | .. program-output:: abserde --help 5 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to abserde's documentation! 2 | =================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | install 9 | usage 10 | help 11 | 12 | Indices and tables 13 | ================== 14 | 15 | * :ref:`genindex` 16 | * :ref:`modindex` 17 | * :ref:`search` 18 | -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | .. _installing: 2 | 3 | Installing abserde 4 | ================== 5 | 6 | To install abserde, first install the latest nightly Rust via `rustup`_ and poetry via :code:`pip`. Then clone 7 | the repository and install it! 8 | 9 | .. code-block:: 10 | 11 | $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 12 | ... rust installs. Make sure to install the latest nightly for your platform! ... 13 | $ curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python 14 | ... poetry installs ... 15 | $ git clone https://github.com/ethanhs/abserde.git && cd abserde 16 | ... cloning etc ... 17 | $ python -m pip install . 18 | ... install abserde ... 19 | $ abserde --help 20 | ... help output here... 21 | 22 | 23 | .. _`rustup`: https://rustup.rs/ 24 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | Using abserde 2 | ============= 3 | 4 | Before using abserde, make sure it is installed (see :ref:`installing` if you haven't yet). 5 | 6 | You can pass :code:`--debug` if you want to check that abserde will compile the stub you 7 | provided. The output wheel can be found in :code:`dist/`. 8 | 9 | .. code-block:: 10 | 11 | $ abserde --debug my_input.pyi 12 | 13 | Once you are happy with the results, you can run without :code:`--debug` to build a fully optimized 14 | release wheel. 15 | 16 | .. code-block:: 17 | 18 | $ abserde my_input.pyi 19 | 20 | 21 | Abserde creates a Rust crate, which you can change the author name and email for via the 22 | :code:`--name` and :code:`--email` flags. 23 | 24 | .. code-block:: 25 | 26 | $ abserde --name "King Arthur" --email king.arthur@lancelot.email my_input.pyi 27 | -------------------------------------------------------------------------------- /examples/forward.pyi: -------------------------------------------------------------------------------- 1 | from abserde import abserde 2 | 3 | @abserde 4 | class Example: 5 | a: int 6 | b: Test 7 | 8 | @abserde 9 | class Test: 10 | c: str 11 | -------------------------------------------------------------------------------- /examples/multiclass.pyi: -------------------------------------------------------------------------------- 1 | from abserde import abserde 2 | from typing import Any 3 | 4 | @abserde 5 | class Test: 6 | room: int 7 | floor: int 8 | 9 | 10 | @abserde 11 | class Test2: 12 | name: Any 13 | age: int 14 | foo: Test 15 | -------------------------------------------------------------------------------- /examples/nested.pyi: -------------------------------------------------------------------------------- 1 | from abserde import abserde 2 | from typing import List 3 | 4 | @abserde 5 | class Nested: 6 | attr: List[List[str]] 7 | -------------------------------------------------------------------------------- /examples/simple.pyi: -------------------------------------------------------------------------------- 1 | from abserde import abserde 2 | 3 | @abserde 4 | class Example: 5 | a: int 6 | b: str 7 | -------------------------------------------------------------------------------- /examples/twitter.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Any, Union 2 | from abserde import abserde 3 | 4 | 5 | @abserde 6 | class SearchMetadata: 7 | completed_in: float 8 | max_id: float 9 | max_id_str: str 10 | next_results: str 11 | query: str 12 | refresh_url: str 13 | count: int 14 | since_id: int 15 | since_id_str: str 16 | 17 | 18 | @abserde 19 | class Hashtag: 20 | text: str 21 | indices: List[int] 22 | 23 | 24 | @abserde 25 | class Size: 26 | w: int 27 | h: int 28 | resize: str 29 | 30 | 31 | @abserde 32 | class Sizes: 33 | medium: Size 34 | small: Size 35 | thumb: Size 36 | large: Size 37 | 38 | 39 | @abserde 40 | class Media: 41 | id: float 42 | id_str: str 43 | indices: List[int] 44 | media_url: str 45 | media_url_https: str 46 | url: str 47 | display_url: str 48 | expanded_url: str 49 | typ: str 50 | sizes: Sizes 51 | source_status_id: Optional[float] 52 | source_status_id_str: Optional[str] 53 | 54 | 55 | @abserde 56 | class URL: 57 | url: str 58 | expanded_url: str 59 | display_url: str 60 | indices: List[int] 61 | 62 | 63 | @abserde 64 | class UserMention: 65 | screen_name: str 66 | name: str 67 | id: int 68 | id_str: str 69 | indices: List[int] 70 | 71 | 72 | @abserde 73 | class StatusEntities: 74 | hashtags: List[Hashtag] 75 | symbols: List[Any] 76 | urls: List[URL] 77 | user_mentions: List[UserMention] 78 | media: Optional[List[Media]] 79 | 80 | 81 | @abserde 82 | class Metadata: 83 | result_type: str 84 | iso_language_code: str 85 | 86 | 87 | @abserde 88 | class Description: 89 | urls: List[URL] 90 | 91 | 92 | @abserde 93 | class UserEntities: 94 | description: Description 95 | url: Optional[Description] 96 | 97 | 98 | @abserde 99 | class User: 100 | id: int 101 | id_str: str 102 | name: str 103 | screen_name: str 104 | location: str 105 | description: str 106 | entities: UserEntities 107 | protected: bool 108 | followers_count: int 109 | friends_count: int 110 | listed_count: int 111 | created_at: str 112 | favourites_count: int 113 | geo_enabled: bool 114 | verified: bool 115 | statuses_count: int 116 | lang: str 117 | contributors_enabled: bool 118 | is_translator: bool 119 | is_translation_enabled: bool 120 | profile_background_color: str 121 | profile_background_image_url: str 122 | profile_background_image_url_https: str 123 | profile_background_tile: bool 124 | profile_image_url: str 125 | profile_image_url_https: str 126 | profile_link_color: str 127 | profile_sidebar_border_color: str 128 | profile_sidebar_fill_color: str 129 | profile_text_color: str 130 | profile_use_background_image: bool 131 | default_profile: bool 132 | default_profile_image: bool 133 | following: bool 134 | follow_request_sent: bool 135 | notifications: bool 136 | url: Optional[str] 137 | utc_offset: Optional[int] 138 | time_zone: Optional[str] 139 | profile_banner_url: Optional[str] 140 | 141 | @abserde 142 | class InnerStatus: 143 | metadata: Metadata 144 | created_at: str 145 | id: float 146 | id_str: str 147 | text: str 148 | source: str 149 | truncated: bool 150 | user: User 151 | geo: Optional[Any] 152 | coordinates: Optional[Any] 153 | place: Optional[Any] 154 | contributors: Optional[Any] 155 | retweet_count: int 156 | favorite_count: int 157 | entities: StatusEntities 158 | favorited: bool 159 | retweeted: bool 160 | lang: str 161 | in_reply_to_status_id: Optional[float] 162 | in_reply_to_status_id_str: Optional[str] 163 | in_reply_to_user_id: Optional[int] 164 | in_reply_to_user_id_str: Optional[str] 165 | in_reply_to_screen_name: Optional[str] 166 | possibly_sensitive: Optional[bool] 167 | 168 | @abserde 169 | class Status: 170 | metadata: Metadata 171 | created_at: str 172 | id: float 173 | id_str: str 174 | text: str 175 | source: str 176 | truncated: bool 177 | user: User 178 | geo: Optional[Any] 179 | coordinates: Optional[Any] 180 | place: Optional[Any] 181 | contributors: Optional[Any] 182 | retweet_count: int 183 | favorite_count: int 184 | entities: StatusEntities 185 | favorited: bool 186 | retweeted: bool 187 | lang: str 188 | in_reply_to_status_id: Optional[float] 189 | in_reply_to_status_id_str: Optional[str] 190 | in_reply_to_user_id: Optional[int] 191 | in_reply_to_user_id_str: Optional[str] 192 | in_reply_to_screen_name: Optional[str] 193 | retweeted_status: Optional[InnerStatus] 194 | possibly_sensitive: Optional[bool] 195 | 196 | 197 | @abserde 198 | class File: 199 | statuses: List[Status] 200 | search_metadata: SearchMetadata 201 | -------------------------------------------------------------------------------- /examples/union.pyi: -------------------------------------------------------------------------------- 1 | from abserde import abserde 2 | 3 | @abserde 4 | class Test: 5 | room: int 6 | floor: int 7 | 8 | 9 | @abserde 10 | class Test2: 11 | age: int 12 | foo: Union[Test, List[int]] 13 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disallow_untyped_calls = True 3 | disallow_untyped_defs = True 4 | disallow_incomplete_defs = True 5 | check_untyped_defs = True 6 | disallow_subclassing_any = True 7 | warn_no_return = True 8 | strict_optional = True 9 | no_implicit_optional = True 10 | disallow_any_generics = True 11 | disallow_any_unimported = True 12 | warn_redundant_casts = True 13 | warn_unused_configs = True 14 | show_traceback = True 15 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 4 | name = "appdirs" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.4.3" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "A few extensions to pyyaml." 12 | name = "aspy.yaml" 13 | optional = false 14 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 15 | version = "1.3.0" 16 | 17 | [package.dependencies] 18 | pyyaml = "*" 19 | 20 | [[package]] 21 | category = "dev" 22 | description = "Atomic file writes." 23 | name = "atomicwrites" 24 | optional = false 25 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 26 | version = "1.3.0" 27 | 28 | [[package]] 29 | category = "dev" 30 | description = "Classes Without Boilerplate" 31 | name = "attrs" 32 | optional = false 33 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 34 | version = "19.3.0" 35 | 36 | [package.extras] 37 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 38 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 39 | docs = ["sphinx", "zope.interface"] 40 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 41 | 42 | [[package]] 43 | category = "dev" 44 | description = "Validate configuration and produce human readable error messages." 45 | name = "cfgv" 46 | optional = false 47 | python-versions = ">=3.6" 48 | version = "3.0.0" 49 | 50 | [[package]] 51 | category = "main" 52 | description = "Composable command line interface toolkit" 53 | name = "click" 54 | optional = false 55 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 56 | version = "7.1.1" 57 | 58 | [[package]] 59 | category = "dev" 60 | description = "Cross-platform colored terminal text." 61 | marker = "sys_platform == \"win32\" and python_version != \"3.4\" or platform_system == \"Windows\"" 62 | name = "colorama" 63 | optional = false 64 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 65 | version = "0.4.3" 66 | 67 | [[package]] 68 | category = "dev" 69 | description = "Distribution utilities" 70 | name = "distlib" 71 | optional = false 72 | python-versions = "*" 73 | version = "0.3.0" 74 | 75 | [[package]] 76 | category = "dev" 77 | description = "A platform independent file lock." 78 | name = "filelock" 79 | optional = false 80 | python-versions = "*" 81 | version = "3.0.12" 82 | 83 | [[package]] 84 | category = "dev" 85 | description = "File identification library for Python" 86 | name = "identify" 87 | optional = false 88 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 89 | version = "1.4.15" 90 | 91 | [package.extras] 92 | license = ["editdistance"] 93 | 94 | [[package]] 95 | category = "dev" 96 | description = "Read metadata from Python packages" 97 | marker = "python_version < \"3.8\"" 98 | name = "importlib-metadata" 99 | optional = false 100 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 101 | version = "1.6.0" 102 | 103 | [package.dependencies] 104 | zipp = ">=0.5" 105 | 106 | [package.extras] 107 | docs = ["sphinx", "rst.linker"] 108 | testing = ["packaging", "importlib-resources"] 109 | 110 | [[package]] 111 | category = "dev" 112 | description = "Read resources from Python packages" 113 | marker = "python_version < \"3.7\"" 114 | name = "importlib-resources" 115 | optional = false 116 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 117 | version = "1.4.0" 118 | 119 | [package.dependencies] 120 | [package.dependencies.importlib-metadata] 121 | python = "<3.8" 122 | version = "*" 123 | 124 | [package.dependencies.zipp] 125 | python = "<3.8" 126 | version = ">=0.4" 127 | 128 | [package.extras] 129 | docs = ["sphinx", "rst.linker", "jaraco.packaging"] 130 | 131 | [[package]] 132 | category = "main" 133 | description = "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" 134 | name = "maturin" 135 | optional = false 136 | python-versions = ">=3.5" 137 | version = "0.7.9" 138 | 139 | [package.dependencies] 140 | toml = ">=0.10.0,<0.11.0" 141 | 142 | [[package]] 143 | category = "dev" 144 | description = "More routines for operating on iterables, beyond itertools" 145 | marker = "python_version > \"2.7\"" 146 | name = "more-itertools" 147 | optional = false 148 | python-versions = ">=3.5" 149 | version = "8.2.0" 150 | 151 | [[package]] 152 | category = "dev" 153 | description = "Optional static typing for Python" 154 | name = "mypy" 155 | optional = false 156 | python-versions = ">=3.5" 157 | version = "0.761" 158 | 159 | [package.dependencies] 160 | mypy-extensions = ">=0.4.3,<0.5.0" 161 | typed-ast = ">=1.4.0,<1.5.0" 162 | typing-extensions = ">=3.7.4" 163 | 164 | [package.extras] 165 | dmypy = ["psutil (>=4.0)"] 166 | 167 | [[package]] 168 | category = "dev" 169 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 170 | name = "mypy-extensions" 171 | optional = false 172 | python-versions = "*" 173 | version = "0.4.3" 174 | 175 | [[package]] 176 | category = "dev" 177 | description = "Node.js virtual environment builder" 178 | name = "nodeenv" 179 | optional = false 180 | python-versions = "*" 181 | version = "1.3.5" 182 | 183 | [[package]] 184 | category = "dev" 185 | description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" 186 | name = "orjson" 187 | optional = false 188 | python-versions = ">=3.6" 189 | version = "2.6.6" 190 | 191 | [[package]] 192 | category = "dev" 193 | description = "Core utilities for Python packages" 194 | name = "packaging" 195 | optional = false 196 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 197 | version = "20.3" 198 | 199 | [package.dependencies] 200 | pyparsing = ">=2.0.2" 201 | six = "*" 202 | 203 | [[package]] 204 | category = "dev" 205 | description = "plugin and hook calling mechanisms for python" 206 | name = "pluggy" 207 | optional = false 208 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 209 | version = "0.13.1" 210 | 211 | [package.dependencies] 212 | [package.dependencies.importlib-metadata] 213 | python = "<3.8" 214 | version = ">=0.12" 215 | 216 | [package.extras] 217 | dev = ["pre-commit", "tox"] 218 | 219 | [[package]] 220 | category = "dev" 221 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 222 | name = "pre-commit" 223 | optional = false 224 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 225 | version = "1.21.0" 226 | 227 | [package.dependencies] 228 | "aspy.yaml" = "*" 229 | cfgv = ">=2.0.0" 230 | identify = ">=1.0.0" 231 | nodeenv = ">=0.11.1" 232 | pyyaml = "*" 233 | six = "*" 234 | toml = "*" 235 | virtualenv = ">=15.2" 236 | 237 | [package.dependencies.importlib-metadata] 238 | python = "<3.8" 239 | version = "*" 240 | 241 | [package.dependencies.importlib-resources] 242 | python = "<3.7" 243 | version = "*" 244 | 245 | [[package]] 246 | category = "dev" 247 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 248 | name = "py" 249 | optional = false 250 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 251 | version = "1.8.1" 252 | 253 | [[package]] 254 | category = "dev" 255 | description = "Get CPU info with pure Python 2 & 3" 256 | name = "py-cpuinfo" 257 | optional = false 258 | python-versions = "*" 259 | version = "5.0.0" 260 | 261 | [[package]] 262 | category = "dev" 263 | description = "Python parsing module" 264 | name = "pyparsing" 265 | optional = false 266 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 267 | version = "2.4.7" 268 | 269 | [[package]] 270 | category = "dev" 271 | description = "pytest: simple powerful testing with Python" 272 | name = "pytest" 273 | optional = false 274 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 275 | version = "4.6.9" 276 | 277 | [package.dependencies] 278 | atomicwrites = ">=1.0" 279 | attrs = ">=17.4.0" 280 | packaging = "*" 281 | pluggy = ">=0.12,<1.0" 282 | py = ">=1.5.0" 283 | six = ">=1.10.0" 284 | wcwidth = "*" 285 | 286 | [package.dependencies.colorama] 287 | python = "<3.4.0 || >=3.5.0" 288 | version = "*" 289 | 290 | [package.dependencies.importlib-metadata] 291 | python = "<3.8" 292 | version = ">=0.12" 293 | 294 | [package.dependencies.more-itertools] 295 | python = ">=2.8" 296 | version = ">=4.0.0" 297 | 298 | [package.extras] 299 | testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] 300 | 301 | [[package]] 302 | category = "dev" 303 | description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. See calibration_ and FAQ_." 304 | name = "pytest-benchmark" 305 | optional = false 306 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 307 | version = "3.2.3" 308 | 309 | [package.dependencies] 310 | py-cpuinfo = "*" 311 | pytest = ">=3.8" 312 | 313 | [package.extras] 314 | aspect = ["aspectlib"] 315 | elasticsearch = ["elasticsearch"] 316 | histogram = ["pygal", "pygaljs"] 317 | 318 | [[package]] 319 | category = "dev" 320 | description = "YAML parser and emitter for Python" 321 | name = "pyyaml" 322 | optional = false 323 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 324 | version = "5.3.1" 325 | 326 | [[package]] 327 | category = "dev" 328 | description = "Python 2 and 3 compatibility utilities" 329 | name = "six" 330 | optional = false 331 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 332 | version = "1.14.0" 333 | 334 | [[package]] 335 | category = "main" 336 | description = "Python Library for Tom's Obvious, Minimal Language" 337 | name = "toml" 338 | optional = false 339 | python-versions = "*" 340 | version = "0.10.0" 341 | 342 | [[package]] 343 | category = "dev" 344 | description = "tox is a generic virtualenv management and test command line tool" 345 | name = "tox" 346 | optional = false 347 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 348 | version = "3.14.6" 349 | 350 | [package.dependencies] 351 | colorama = ">=0.4.1" 352 | filelock = ">=3.0.0,<4" 353 | packaging = ">=14" 354 | pluggy = ">=0.12.0,<1" 355 | py = ">=1.4.17,<2" 356 | six = ">=1.14.0,<2" 357 | toml = ">=0.9.4" 358 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 359 | 360 | [package.dependencies.importlib-metadata] 361 | python = "<3.8" 362 | version = ">=0.12,<2" 363 | 364 | [package.extras] 365 | docs = ["sphinx (>=2.0.0,<3)", "towncrier (>=18.5.0)", "pygments-github-lexers (>=0.0.5)", "sphinxcontrib-autoprogram (>=0.1.5)"] 366 | testing = ["freezegun (>=0.3.11,<1)", "pathlib2 (>=2.3.3,<3)", "pytest (>=4.0.0,<6)", "pytest-cov (>=2.5.1,<3)", "pytest-mock (>=1.10.0,<2)", "pytest-xdist (>=1.22.2,<2)", "pytest-randomly (>=1.0.0,<4)", "flaky (>=3.4.0,<4)", "psutil (>=5.6.1,<6)"] 367 | 368 | [[package]] 369 | category = "dev" 370 | description = "a fork of Python 2 and 3 ast modules with type comment support" 371 | name = "typed-ast" 372 | optional = false 373 | python-versions = "*" 374 | version = "1.4.1" 375 | 376 | [[package]] 377 | category = "dev" 378 | description = "Backported and Experimental Type Hints for Python 3.5+" 379 | name = "typing-extensions" 380 | optional = false 381 | python-versions = "*" 382 | version = "3.7.4.2" 383 | 384 | [[package]] 385 | category = "dev" 386 | description = "Ultra fast JSON encoder and decoder for Python" 387 | name = "ujson" 388 | optional = false 389 | python-versions = "*" 390 | version = "1.35" 391 | 392 | [[package]] 393 | category = "dev" 394 | description = "Virtual Python Environment builder" 395 | name = "virtualenv" 396 | optional = false 397 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 398 | version = "20.0.18" 399 | 400 | [package.dependencies] 401 | appdirs = ">=1.4.3,<2" 402 | distlib = ">=0.3.0,<1" 403 | filelock = ">=3.0.0,<4" 404 | six = ">=1.9.0,<2" 405 | 406 | [package.dependencies.importlib-metadata] 407 | python = "<3.8" 408 | version = ">=0.12,<2" 409 | 410 | [package.dependencies.importlib-resources] 411 | python = "<3.7" 412 | version = ">=1.0,<2" 413 | 414 | [package.extras] 415 | docs = ["sphinx (>=2.0.0,<3)", "sphinx-argparse (>=0.2.5,<1)", "sphinx-rtd-theme (>=0.4.3,<1)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2,<1)"] 416 | testing = ["pytest (>=4.0.0,<6)", "coverage (>=4.5.1,<6)", "pytest-mock (>=2.0.0,<3)", "pytest-env (>=0.6.2,<1)", "pytest-timeout (>=1.3.4,<2)", "packaging (>=20.0)", "xonsh (>=0.9.16,<1)"] 417 | 418 | [[package]] 419 | category = "dev" 420 | description = "Measures number of Terminal column cells of wide-character codes" 421 | name = "wcwidth" 422 | optional = false 423 | python-versions = "*" 424 | version = "0.1.9" 425 | 426 | [[package]] 427 | category = "dev" 428 | description = "Backport of pathlib-compatible object wrapper for zip files" 429 | marker = "python_version < \"3.8\"" 430 | name = "zipp" 431 | optional = false 432 | python-versions = ">=3.6" 433 | version = "3.1.0" 434 | 435 | [package.extras] 436 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 437 | testing = ["jaraco.itertools", "func-timeout"] 438 | 439 | [metadata] 440 | content-hash = "9b9e77e24c5f46fd72ccd991c15f52af6c065946addba1132efa72f1dc0168f0" 441 | python-versions = ">3.6" 442 | 443 | [metadata.files] 444 | appdirs = [ 445 | {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, 446 | {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, 447 | ] 448 | "aspy.yaml" = [ 449 | {file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"}, 450 | {file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"}, 451 | ] 452 | atomicwrites = [ 453 | {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, 454 | {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, 455 | ] 456 | attrs = [ 457 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 458 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 459 | ] 460 | cfgv = [ 461 | {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, 462 | {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, 463 | ] 464 | click = [ 465 | {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, 466 | {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, 467 | ] 468 | colorama = [ 469 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 470 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 471 | ] 472 | distlib = [ 473 | {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, 474 | ] 475 | filelock = [ 476 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 477 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 478 | ] 479 | identify = [ 480 | {file = "identify-1.4.15-py2.py3-none-any.whl", hash = "sha256:88ed90632023e52a6495749c6732e61e08ec9f4f04e95484a5c37b9caf40283c"}, 481 | {file = "identify-1.4.15.tar.gz", hash = "sha256:23c18d97bb50e05be1a54917ee45cc61d57cb96aedc06aabb2b02331edf0dbf0"}, 482 | ] 483 | importlib-metadata = [ 484 | {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, 485 | {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, 486 | ] 487 | importlib-resources = [ 488 | {file = "importlib_resources-1.4.0-py2.py3-none-any.whl", hash = "sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8"}, 489 | {file = "importlib_resources-1.4.0.tar.gz", hash = "sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2"}, 490 | ] 491 | maturin = [ 492 | {file = "maturin-0.7.9-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:e3bc882f7728273880c8b1589933ccdfbb0fdbd9fa0f57958ba69811b9bbccee"}, 493 | {file = "maturin-0.7.9-py3-none-manylinux1_i686.whl", hash = "sha256:8f1595336be0958b281f36716f423115d5233a17137a01aecc20baff1007a9d9"}, 494 | {file = "maturin-0.7.9-py3-none-manylinux1_x86_64.whl", hash = "sha256:df1358a298e30dee41c16e51b77c931f01017ba703103bb6764372f500dac453"}, 495 | {file = "maturin-0.7.9-py3-none-win32.whl", hash = "sha256:cc1c7bb83caad728be484de9aa952a71c1f6bdd386ff36a6a4d0c4b99410951e"}, 496 | {file = "maturin-0.7.9-py3-none-win_amd64.whl", hash = "sha256:7d8fbf2132e98c35aa665a50a44764e4c0340d70795b05cebfb875ebbb512e23"}, 497 | {file = "maturin-0.7.9.tar.gz", hash = "sha256:13403587b64eb9571fc8886608fac6a301b791d0670ef2fb79e60add39ac8aec"}, 498 | ] 499 | more-itertools = [ 500 | {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, 501 | {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, 502 | ] 503 | mypy = [ 504 | {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, 505 | {file = "mypy-0.761-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36"}, 506 | {file = "mypy-0.761-cp35-cp35m-win_amd64.whl", hash = "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72"}, 507 | {file = "mypy-0.761-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2"}, 508 | {file = "mypy-0.761-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0"}, 509 | {file = "mypy-0.761-cp36-cp36m-win_amd64.whl", hash = "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474"}, 510 | {file = "mypy-0.761-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a"}, 511 | {file = "mypy-0.761-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749"}, 512 | {file = "mypy-0.761-cp37-cp37m-win_amd64.whl", hash = "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1"}, 513 | {file = "mypy-0.761-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7"}, 514 | {file = "mypy-0.761-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"}, 515 | {file = "mypy-0.761-cp38-cp38-win_amd64.whl", hash = "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b"}, 516 | {file = "mypy-0.761-py3-none-any.whl", hash = "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217"}, 517 | {file = "mypy-0.761.tar.gz", hash = "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf"}, 518 | ] 519 | mypy-extensions = [ 520 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 521 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 522 | ] 523 | nodeenv = [ 524 | {file = "nodeenv-1.3.5-py2.py3-none-any.whl", hash = "sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212"}, 525 | ] 526 | orjson = [ 527 | {file = "orjson-2.6.6-cp36-cp36m-macosx_10_7_x86_64.whl", hash = "sha256:5ec9fd99a5ea2fcd02e603bf45f59537699eed3c8e80931f4046dbeb3c0b04d0"}, 528 | {file = "orjson-2.6.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5ce6ec523cc2c86dbe67212ae5457b367cc9e987dbb6c3205ef40d56288c877a"}, 529 | {file = "orjson-2.6.6-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:58940fdb556813495c2be8f49243a5909094375a38e70cced9792562141c67a4"}, 530 | {file = "orjson-2.6.6-cp36-none-win_amd64.whl", hash = "sha256:bca81abab141089e83aa1bba2c01d5c761ff62c84a46f0358b1287647adcb288"}, 531 | {file = "orjson-2.6.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:1f4dd2ea6a34061b61fb0933ab5ff7b1f7192a46ee06f322df3783fbda5c9d80"}, 532 | {file = "orjson-2.6.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1422b34d7d087e3a13eab823783942fc94a2ba6b8947697a5eb740bfe4325759"}, 533 | {file = "orjson-2.6.6-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a9a9eef9ffd096951a63615c0a28e1d19baefdb05f184479d5a421a94ae9c69b"}, 534 | {file = "orjson-2.6.6-cp37-none-win_amd64.whl", hash = "sha256:56c8fec78f3780c3654d794262836497579bbf65a14a170ef004c3d801ff4a54"}, 535 | {file = "orjson-2.6.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:862ab639ba644e8c287d1a5512d21a4dfb9f8b82611836ab6fdfd9232412890d"}, 536 | {file = "orjson-2.6.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2f5b6c6d6c3e3346f93ed7e77cb0c1e917ffa2ef75f584a472e5d455fbfa567e"}, 537 | {file = "orjson-2.6.6-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:abdd23e11fa72a38618b64157e4e3c3b7dde1beaf53d3e222dbd531c5809cd06"}, 538 | {file = "orjson-2.6.6-cp38-none-win_amd64.whl", hash = "sha256:fd57f3860e23f09ab861674ce56488e4f78ff65ec0bac1ecddb8875135abe3cc"}, 539 | {file = "orjson-2.6.6-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9d6ebb0865f0d99b76330712280129d5f5f7fd4025082bddeaba87873d9e6a19"}, 540 | {file = "orjson-2.6.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:93ab5b6eca95264d1a1c81644e54f4fe2fdadf39907f5856dbf27a6187a49776"}, 541 | {file = "orjson-2.6.6.tar.gz", hash = "sha256:910796567d46223a7f4eaea0cc2cb4d3d3adb7a6bccc5ffa8fa2f95b4296d8d9"}, 542 | ] 543 | packaging = [ 544 | {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, 545 | {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, 546 | ] 547 | pluggy = [ 548 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 549 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 550 | ] 551 | pre-commit = [ 552 | {file = "pre_commit-1.21.0-py2.py3-none-any.whl", hash = "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"}, 553 | {file = "pre_commit-1.21.0.tar.gz", hash = "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850"}, 554 | ] 555 | py = [ 556 | {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, 557 | {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, 558 | ] 559 | py-cpuinfo = [ 560 | {file = "py-cpuinfo-5.0.0.tar.gz", hash = "sha256:2cf6426f776625b21d1db8397d3297ef7acfa59018f02a8779123f3190f18500"}, 561 | ] 562 | pyparsing = [ 563 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 564 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 565 | ] 566 | pytest = [ 567 | {file = "pytest-4.6.9-py2.py3-none-any.whl", hash = "sha256:c77a5f30a90e0ce24db9eaa14ddfd38d4afb5ea159309bdd2dae55b931bc9324"}, 568 | {file = "pytest-4.6.9.tar.gz", hash = "sha256:19e8f75eac01dd3f211edd465b39efbcbdc8fc5f7866d7dd49fedb30d8adf339"}, 569 | ] 570 | pytest-benchmark = [ 571 | {file = "pytest-benchmark-3.2.3.tar.gz", hash = "sha256:ad4314d093a3089701b24c80a05121994c7765ce373478c8f4ba8d23c9ba9528"}, 572 | {file = "pytest_benchmark-3.2.3-py2.py3-none-any.whl", hash = "sha256:01f79d38d506f5a3a0a9ada22ded714537bbdfc8147a881a35c1655db07289d9"}, 573 | ] 574 | pyyaml = [ 575 | {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, 576 | {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, 577 | {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, 578 | {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, 579 | {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, 580 | {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, 581 | {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, 582 | {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, 583 | {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, 584 | {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, 585 | {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, 586 | ] 587 | six = [ 588 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 589 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 590 | ] 591 | toml = [ 592 | {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, 593 | {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, 594 | {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, 595 | ] 596 | tox = [ 597 | {file = "tox-3.14.6-py2.py3-none-any.whl", hash = "sha256:b2c4b91c975ea5c11463d9ca00bebf82654439c5df0f614807b9bdec62cc9471"}, 598 | {file = "tox-3.14.6.tar.gz", hash = "sha256:a4a6689045d93c208d77230853b28058b7513f5123647b67bf012f82fa168303"}, 599 | ] 600 | typed-ast = [ 601 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 602 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 603 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 604 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 605 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 606 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 607 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 608 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 609 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 610 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 611 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 612 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 613 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 614 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 615 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 616 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 617 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 618 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 619 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 620 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 621 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 622 | ] 623 | typing-extensions = [ 624 | {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, 625 | {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, 626 | {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, 627 | ] 628 | ujson = [ 629 | {file = "ujson-1.35.tar.gz", hash = "sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86"}, 630 | ] 631 | virtualenv = [ 632 | {file = "virtualenv-20.0.18-py2.py3-none-any.whl", hash = "sha256:5021396e8f03d0d002a770da90e31e61159684db2859d0ba4850fbea752aa675"}, 633 | {file = "virtualenv-20.0.18.tar.gz", hash = "sha256:ac53ade75ca189bc97b6c1d9ec0f1a50efe33cbf178ae09452dcd9fd309013c1"}, 634 | ] 635 | wcwidth = [ 636 | {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, 637 | {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, 638 | ] 639 | zipp = [ 640 | {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, 641 | {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, 642 | ] 643 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "abserde" 3 | version = "0.1.0" 4 | description = "Generate fast JSON parsers based on type stubs" 5 | authors = ["Ethan Smith "] 6 | include = ["abserde/template_crate/**/*.*", "abserde/template_crate/src"] 7 | license = "Apache-2.0 OR MIT" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">3.6" 11 | click = "^7.0" 12 | maturin = "^0.7" 13 | dataclasses = { version = "^0.7", python = "3.6" } 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^4.0" 17 | pytest-benchmark="^3.2" 18 | mypy = "0.770" 19 | pre-commit = "^1.16" 20 | tox = "^3.10" 21 | orjson = "^2.6" 22 | ujson = "1.35" 23 | 24 | [tool.poetry.plugins."console_scripts"] 25 | "abserde" = "abserde.main:main" 26 | 27 | [build-system] 28 | requires = ["poetry>=0.12"] 29 | build-backend = "poetry.masonry.api" 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmatyping/abserde/b448d2bb5a1a59e1ce65f62a3c20a7009bb77071/tests/__init__.py -------------------------------------------------------------------------------- /tests/large_bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmatyping/abserde/b448d2bb5a1a59e1ce65f62a3c20a7009bb77071/tests/large_bench.png -------------------------------------------------------------------------------- /tests/small_bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmatyping/abserde/b448d2bb5a1a59e1ce65f62a3c20a7009bb77071/tests/small_bench.png -------------------------------------------------------------------------------- /tests/test_benchmark.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | import orjson 5 | import pytest 6 | import ujson 7 | 8 | try: 9 | import multiclass 10 | import twitter 11 | except ImportError as e: 12 | print("You must run the tests from the environment with all of the examples built.") 13 | raise e 14 | 15 | 16 | @pytest.mark.benchmark( 17 | group="dumps", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 18 | ) 19 | def test_abserde_dumps_speed(benchmark): 20 | n = multiclass.Test(4211, 4) 21 | benchmark(n.dumps) 22 | 23 | 24 | @pytest.mark.benchmark( 25 | group="loads", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 26 | ) 27 | def test_abserde_loads_speed(benchmark): 28 | n = multiclass.Test(4211, 4) 29 | s = multiclass.dumps(n) 30 | benchmark(multiclass.Test.loads, s) 31 | 32 | 33 | @pytest.mark.benchmark( 34 | group="loads", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 35 | ) 36 | def test_orjson_loads_speed(benchmark): 37 | n = multiclass.Test(4211, 4) 38 | s = multiclass.dumps(n) 39 | benchmark(orjson.loads, s) 40 | 41 | 42 | @pytest.mark.benchmark( 43 | group="dumps", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 44 | ) 45 | def test_orjson_dumps_speed(benchmark): 46 | n = multiclass.Test(4211, 4) 47 | d = orjson.loads(multiclass.dumps(n)) 48 | benchmark(orjson.dumps, d) 49 | 50 | 51 | @pytest.mark.benchmark( 52 | group="loads", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 53 | ) 54 | def test_ujson_loads_speed(benchmark): 55 | n = multiclass.Test(4211, 4) 56 | s = multiclass.dumps(n) 57 | benchmark(ujson.loads, s) 58 | 59 | 60 | @pytest.mark.benchmark( 61 | group="dumps", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 62 | ) 63 | def test_ujson_dumps_speed(benchmark): 64 | n = multiclass.Test(4211, 4) 65 | d = ujson.loads(multiclass.dumps(n)) 66 | benchmark(ujson.dumps, d) 67 | 68 | 69 | @pytest.mark.benchmark( 70 | group="loads", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 71 | ) 72 | def test_json_loads_speed(benchmark): 73 | n = multiclass.Test(4211, 4) 74 | s = multiclass.dumps(n) 75 | benchmark(json.loads, s) 76 | 77 | 78 | @pytest.mark.benchmark( 79 | group="dumps", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 80 | ) 81 | def test_json_dumps_speed(benchmark): 82 | n = multiclass.Test(4211, 4) 83 | d = json.loads(multiclass.dumps(n)) 84 | benchmark(json.dumps, d) 85 | 86 | 87 | @pytest.mark.benchmark( 88 | group="twitter_loads", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 89 | ) 90 | def test_abserde_twitter_loads_speed(benchmark): 91 | with open('tests/twitter.json', encoding='utf-8') as f: 92 | j = f.read() 93 | benchmark(twitter.File.loads, j) 94 | 95 | 96 | @pytest.mark.benchmark( 97 | group="twitter_loads", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 98 | ) 99 | def test_ujson_twitter_loads_speed(benchmark): 100 | with open('tests/twitter.json', encoding='utf-8') as f: 101 | j = f.read() 102 | benchmark(ujson.loads, j) 103 | 104 | 105 | @pytest.mark.benchmark( 106 | group="twitter_dumps", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 107 | ) 108 | def test_abserde_twitter_dumps_speed(benchmark): 109 | with open('tests/twitter.json', encoding='utf-8') as f: 110 | j = f.read() 111 | data = twitter.File.loads(j) 112 | benchmark(data.dumps) 113 | 114 | 115 | @pytest.mark.benchmark( 116 | group="twitter_dumps", max_time=5.0, timer=time.perf_counter, disable_gc=True, warmup=False, 117 | ) 118 | def test_ujson_twitter_dumps_speed(benchmark): 119 | with open('tests/twitter.json', encoding='utf-8') as f: 120 | j = f.read() 121 | data = ujson.loads(j) 122 | benchmark(ujson.dumps, data) 123 | -------------------------------------------------------------------------------- /tests/test_multiclass.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | try: 4 | import multiclass 5 | except ImportError as e: 6 | print("You must run the tests from the environment with all of the examples built.") 7 | raise e 8 | 9 | 10 | def test_init(): 11 | t = multiclass.Test(5, 2) 12 | assert t.room == 5 13 | assert t.floor == 2 14 | with pytest.raises(TypeError): 15 | tt = multiclass.Test('fail!', 5.5) 16 | t2 = multiclass.Test2(8.8, 30, t) 17 | assert t2.name == 8.8 18 | assert t2.age == 30 19 | assert t2.foo == t 20 | with pytest.raises(TypeError): 21 | tt2 = multiclass.Test2(6, 6, 6) 22 | 23 | 24 | def test_loads(): 25 | t = multiclass.Test.loads('{"room": 4, "floor": 10}') 26 | assert t.room == 4 27 | assert t.floor == 10 28 | with pytest.raises(multiclass.JSONParseError): 29 | tt = multiclass.Test.loads('{"invalid":') 30 | with pytest.raises(multiclass.JSONParseError): 31 | tt = multiclass.Test.loads('{"room": "invalid", "floor": 5}') 32 | t2 = multiclass.Test2.loads('{"age": 4, "name": "Will", "foo": {"room": 4, "floor": 10}}') 33 | assert t2.name == "Will" 34 | assert t2.age == 4 35 | assert t2.foo == t 36 | with pytest.raises(multiclass.JSONParseError): 37 | tt2 = multiclass.Test2.loads('{"age": 4, "name": 5, "foo": {"room": None, "floor": 10}}') 38 | 39 | 40 | def test_dumps(): 41 | t = multiclass.Test(5, 2) 42 | assert t.dumps() == '{"room":5,"floor":2}' 43 | t2 = multiclass.Test2(8.8, 30, t) 44 | assert t2.dumps() == '{"name":8.8,"age":30,"foo":{"room":5,"floor":2}}' 45 | 46 | 47 | def test_access(): 48 | t = multiclass.Test(5, 2) 49 | assert t['room'] == 5 50 | t['floor'] = 10 51 | assert t.floor == 10 52 | t.room = 100 53 | assert t['room'] == 100 54 | 55 | 56 | def test_str(): 57 | t = multiclass.Test(5, 2) 58 | assert(str(t) == t.dumps()) 59 | 60 | 61 | def test_any_mutation(): 62 | t = multiclass.Test(5, 2) 63 | t2 = multiclass.Test2(8.8, 30, t) 64 | s = "I can be a string!" 65 | t2.name = s 66 | assert t2.name == s 67 | lst = [1, ''] 68 | t2.name = lst 69 | assert t2.name == lst 70 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = lint,py36,py37,py38 3 | isolated_build = True 4 | 5 | [testenv] 6 | setenv = 7 | VIRTUALENV_NO_DOWNLOAD=1 8 | deps = 9 | pytest >= 4.0 10 | pytest-benchmark 11 | mypy 12 | pre-commit 13 | orjson 14 | ujson 15 | 16 | [testenv:py36] 17 | commands = python -m pytest {posargs} 18 | commands_pre = 19 | python -m pip install --no-index --find-links=dist multiclass twitter -U 20 | 21 | 22 | [testenv:py37] 23 | # Python 3.6+ has a number of compile-time warnings on invalid string escapes. 24 | # PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run. 25 | install_command = pip install --no-compile {opts} {packages} 26 | setenv = 27 | PYTHONWARNINGS=d 28 | commands = python -m pytest {posargs} 29 | commands_pre = 30 | python -m pip install --no-index --find-links=dist multiclass twitter -U 31 | 32 | [testenv:py38] 33 | # Python 3.6+ has a number of compile-time warnings on invalid string escapes. 34 | # PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run. 35 | install_command = pip install --no-compile {opts} {packages} 36 | setenv = 37 | PYTHONWARNINGS=d 38 | commands = python -m pytest {posargs} 39 | commands_pre = 40 | python -m pip install --no-index --find-links=dist multiclass twitter -U 41 | 42 | [testenv:lint] 43 | basepython = python3.7 44 | skip_install = true 45 | deps = 46 | pre-commit 47 | mypy 48 | passenv = HOMEPATH # needed on Windows 49 | commands = pre-commit run --all-files 50 | 51 | 52 | [flake8] 53 | max-line-length = 99 54 | ignore = F841, W503 55 | --------------------------------------------------------------------------------