├── .github ├── CODE_OF_CONDUCT.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── SECURITY.md └── workflows │ ├── main.yml │ ├── pypi-package.yml │ └── zizmor.yml ├── .gitignore ├── .readthedocs.yml ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── bench ├── __init__.py ├── test_attrs_collections.py ├── test_attrs_nested.py ├── test_attrs_primitives.py └── test_primitives.py ├── docs ├── Makefile ├── _static │ ├── custom.css │ └── fonts │ │ ├── ubuntu-mono-v15-latin-700.woff │ │ ├── ubuntu-mono-v15-latin-700.woff2 │ │ ├── ubuntu-mono-v15-latin-700italic.woff │ │ ├── ubuntu-mono-v15-latin-700italic.woff2 │ │ ├── ubuntu-mono-v15-latin-italic.woff │ │ ├── ubuntu-mono-v15-latin-italic.woff2 │ │ ├── ubuntu-mono-v15-latin-regular.woff │ │ └── ubuntu-mono-v15-latin-regular.woff2 ├── basics.md ├── benchmarking.md ├── cattrs.gen.rst ├── cattrs.preconf.rst ├── cattrs.rst ├── cattrs.strategies.rst ├── conf.py ├── contributing.md ├── customizing.md ├── defaulthooks.md ├── history.md ├── indepth.md ├── index.md ├── make.bat ├── migrations.md ├── modules.rst ├── preconf.md ├── recipes.md ├── strategies.md ├── unions.md ├── usage.md ├── validation.md └── why.md ├── pdm.lock ├── pyproject.toml ├── src ├── cattr │ ├── __init__.py │ ├── converters.py │ ├── disambiguators.py │ ├── dispatch.py │ ├── errors.py │ ├── gen.py │ ├── preconf │ │ ├── __init__.py │ │ ├── bson.py │ │ ├── json.py │ │ ├── msgpack.py │ │ ├── orjson.py │ │ ├── pyyaml.py │ │ ├── tomlkit.py │ │ └── ujson.py │ └── py.typed └── cattrs │ ├── __init__.py │ ├── _compat.py │ ├── _generics.py │ ├── cols.py │ ├── converters.py │ ├── disambiguators.py │ ├── dispatch.py │ ├── errors.py │ ├── fns.py │ ├── gen │ ├── __init__.py │ ├── _consts.py │ ├── _generics.py │ ├── _lc.py │ ├── _shared.py │ └── typeddicts.py │ ├── literals.py │ ├── preconf │ ├── __init__.py │ ├── bson.py │ ├── cbor2.py │ ├── json.py │ ├── msgpack.py │ ├── msgspec.py │ ├── orjson.py │ ├── pyyaml.py │ ├── tomlkit.py │ └── ujson.py │ ├── py.typed │ ├── strategies │ ├── __init__.py │ ├── _class_methods.py │ ├── _subclasses.py │ └── _unions.py │ ├── typealiases.py │ ├── types.py │ └── v.py ├── tests ├── __init__.py ├── _compat.py ├── conftest.py ├── preconf │ ├── __init__.py │ ├── test_msgspec_cpython.py │ └── test_pyyaml.py ├── strategies │ ├── __init__.py │ ├── test_class_methods.py │ ├── test_include_subclasses.py │ ├── test_native_unions.py │ ├── test_tagged_unions.py │ └── test_tagged_unions_695.py ├── test_any.py ├── test_baseconverter.py ├── test_cols.py ├── test_converter.py ├── test_converter_inheritance.py ├── test_copy.py ├── test_dataclasses.py ├── test_defaultdicts.py ├── test_dicts.py ├── test_disambiguators.py ├── test_enums.py ├── test_factory_hooks.py ├── test_final.py ├── test_function_dispatch.py ├── test_gen.py ├── test_gen_collections.py ├── test_gen_dict.py ├── test_gen_dict_563.py ├── test_generics.py ├── test_generics_604.py ├── test_generics_695.py ├── test_generics_696.py ├── test_literals.py ├── test_multistrategy_dispatch.py ├── test_newtypes.py ├── test_optionals.py ├── test_preconf.py ├── test_recursive.py ├── test_self.py ├── test_structure.py ├── test_structure_attrs.py ├── test_tests.py ├── test_tuples.py ├── test_tuples_563.py ├── test_typeddicts.py ├── test_unions.py ├── test_unstructure.py ├── test_unstructure_collections.py ├── test_v.py ├── test_validation.py ├── typed.py ├── typeddicts.py └── untyped.py └── tox.ini /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socioeconomic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | . 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | tidelift: "pypi/cattrs" 3 | github: Tinche 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * cattrs version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | 4 | ## Versioning and Backwards Compatibility 5 | 6 | _cattrs_ follows [*CalVer*](https://calver.org) and we only support the latest PyPI version, due to limited resources. 7 | We aim to support all Python versions that are not end-of-life; older versions will be dropped to ease the maintenance burden. 8 | 9 | Our goal is to never undertake major breaking changes (the kinds that would necessitate a v2 if we were following SemVer). 10 | Minor breaking changes may be undertaken to improve the developer experience and robustness of the library. 11 | All breaking changes are prominently called out in the changelog, alongside any customization steps that may be used to restore previous behavior, when applicable. 12 | 13 | APIs may be marked as provisional. 14 | These are not guaranteed to be stable and may change or be removed without prior notice. 15 | 16 | ## Security Contact Information 17 | 18 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). 19 | Tidelift will coordinate the fix and disclosure. 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | permissions: {} 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | tests: 14 | name: "Python ${{ matrix.python-version }}" 15 | runs-on: "ubuntu-latest" 16 | 17 | strategy: 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.10"] 20 | fail-fast: false 21 | 22 | steps: 23 | - uses: "actions/checkout@v4" 24 | with: 25 | persist-credentials: false 26 | 27 | - uses: "pdm-project/setup-pdm@v4" 28 | with: 29 | python-version: "${{ matrix.python-version }}" 30 | allow-python-prereleases: true 31 | cache: true 32 | version: "2.21.0" 33 | 34 | - name: "Run Tox" 35 | run: | 36 | python -Im pip install --upgrade tox tox-gh-actions 37 | 38 | python -Im tox 39 | 40 | - name: Upload coverage data 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: coverage-data-${{ matrix.python-version }} 44 | path: .coverage.* 45 | if-no-files-found: ignore 46 | include-hidden-files: true 47 | 48 | coverage: 49 | name: "Combine & check coverage." 50 | needs: "tests" 51 | runs-on: "ubuntu-latest" 52 | 53 | steps: 54 | - uses: "actions/checkout@v4" 55 | with: 56 | persist-credentials: false 57 | 58 | - uses: "actions/setup-python@v5" 59 | with: 60 | cache: "pip" 61 | python-version: "3.12" 62 | 63 | - run: "python -Im pip install --upgrade coverage[toml]" 64 | 65 | - name: Download coverage data 66 | uses: actions/download-artifact@v4 67 | with: 68 | pattern: coverage-data-* 69 | merge-multiple: true 70 | 71 | - name: "Combine coverage" 72 | run: | 73 | python -Im coverage combine 74 | python -Im coverage html 75 | python -Im coverage json 76 | 77 | # Report and write to summary. 78 | python -Im coverage report --format=markdown >> $GITHUB_STEP_SUMMARY 79 | 80 | export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") 81 | echo "total=$TOTAL" >> $GITHUB_ENV 82 | 83 | # Report again and fail if under the threshold. 84 | python -Im coverage report --fail-under=100 85 | 86 | - name: "Upload HTML report." 87 | uses: "actions/upload-artifact@v4" 88 | with: 89 | name: "html-report" 90 | path: "htmlcov" 91 | if: always() 92 | 93 | - name: "Make badge" 94 | if: github.ref == 'refs/heads/main' 95 | uses: "schneegans/dynamic-badges-action@v1.4.0" 96 | with: 97 | # GIST_TOKEN is a GitHub personal access token with scope "gist". 98 | auth: ${{ secrets.GIST_TOKEN }} 99 | gistID: 22405310d6a663164d894a2beab4d44d 100 | filename: covbadge.json 101 | label: Coverage 102 | message: ${{ env.total }}% 103 | minColorRange: 50 104 | maxColorRange: 90 105 | valColorRange: ${{ env.total }} 106 | 107 | package: 108 | name: "Build & verify package" 109 | runs-on: "ubuntu-latest" 110 | 111 | steps: 112 | - uses: "actions/checkout@v4" 113 | with: 114 | persist-credentials: false 115 | - uses: "pdm-project/setup-pdm@v4" 116 | with: 117 | python-version: "3.12" 118 | version: "2.21.0" 119 | 120 | - name: "Install check-wheel-content and twine" 121 | run: "python -m pip install twine check-wheel-contents" 122 | - name: "Build package" 123 | run: "pdm build" 124 | - name: "List result" 125 | run: "ls -l dist" 126 | - name: "Check wheel contents" 127 | run: "check-wheel-contents --toplevel cattr,cattrs dist/*.whl" 128 | - name: "Check long_description" 129 | run: "python -m twine check dist/*" 130 | -------------------------------------------------------------------------------- /.github/workflows/pypi-package.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build & maybe upload PyPI package 3 | 4 | permissions: {} 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | tags: ["*"] 10 | release: 11 | types: 12 | - published 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build-package: 17 | name: Build & verify package 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | persist-credentials: false 25 | 26 | - uses: hynek/build-and-inspect-python-package@v2 27 | 28 | # Upload to Test PyPI on every commit on main. 29 | release-test-pypi: 30 | name: Publish in-dev package to test.pypi.org 31 | environment: release-test-pypi 32 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 33 | runs-on: ubuntu-latest 34 | needs: build-package 35 | permissions: 36 | id-token: write 37 | 38 | steps: 39 | - name: Download packages built by build-and-inspect-python-package 40 | uses: actions/download-artifact@v4 41 | with: 42 | name: Packages 43 | path: dist 44 | 45 | - name: Upload package to Test PyPI 46 | uses: pypa/gh-action-pypi-publish@release/v1 47 | with: 48 | repository-url: https://test.pypi.org/legacy/ 49 | 50 | # Upload to real PyPI on GitHub Releases. 51 | release-pypi: 52 | name: Publish released package to pypi.org 53 | environment: release-pypi 54 | if: github.event.action == 'published' 55 | runs-on: ubuntu-latest 56 | needs: build-package 57 | permissions: 58 | id-token: write 59 | 60 | steps: 61 | - name: Download packages built by build-and-inspect-python-package 62 | uses: actions/download-artifact@v4 63 | with: 64 | name: Packages 65 | path: dist 66 | 67 | - name: Upload package to PyPI 68 | uses: pypa/gh-action-pypi-publish@release/v1 69 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: Zizmor 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["*"] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | zizmor: 14 | name: Zizmor latest via uv 15 | runs-on: ubuntu-latest 16 | permissions: 17 | security-events: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | persist-credentials: false 24 | - name: Install the latest version of uv 25 | uses: astral-sh/setup-uv@v4 26 | with: 27 | version: "latest" 28 | - name: Run zizmor 🌈 29 | run: uvx zizmor --format sarif . > results.sarif 30 | env: 31 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | - name: Upload SARIF file 33 | uses: github/codeql-action/upload-sarif@v3 34 | with: 35 | # Path to SARIF file relative to the root of the repository 36 | sarif_file: results.sarif 37 | # Optional category for the results 38 | # Used to differentiate multiple results for one commit 39 | category: zizmor 40 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | README.html 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # Visual Studio Code 63 | launch.json 64 | settings.json 65 | 66 | .venv* 67 | .editorconfig 68 | .mypy_cache 69 | .pytest_cache 70 | .pdm-python 71 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | build: 7 | os: ubuntu-20.04 8 | tools: 9 | # Keep version in sync with tox.ini (docs and gh-actions). 10 | python: "3.11" 11 | jobs: 12 | # Need the tags to calculate the version 13 | post_checkout: 14 | - git fetch --tags 15 | post_create_environment: 16 | - "curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -" 17 | post_install: 18 | - "VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH ~/.local/bin/pdm sync -dG :all,docs" 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! 4 | Every little bit helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | ## Types of Contributions 9 | 10 | ### Report Bugs 11 | 12 | Report bugs at [https://github.com/python-attrs/cattrs/issues](https://github.com/python-attrs/cattrs/issues). 13 | 14 | If you are reporting a bug, please include: 15 | 16 | - Your operating system name and version. 17 | - Any details about your local setup that might be helpful in troubleshooting. 18 | - Detailed steps to reproduce the bug. 19 | 20 | ### Fix Bugs 21 | 22 | Look through the GitHub issues for bugs. Anything tagged with "bug" 23 | and "help wanted" is open to whoever wants to implement it. 24 | 25 | ### Implement Features 26 | 27 | Look through the GitHub issues for features. 28 | Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. 29 | 30 | ### Write Documentation 31 | 32 | _cattrs_ could always use more documentation, whether as part of the 33 | official cattrs docs, in docstrings, or even on the web in blog posts, 34 | articles, and such. 35 | 36 | ### Submit Feedback 37 | 38 | The best way to send feedback is to file an issue at [https://github.com/python-attrs/cattrs/issues](https://github.com/python-attrs/cattrs/issues). 39 | 40 | If you are proposing a feature: 41 | 42 | - Explain in detail how it would work. 43 | - Keep the scope as narrow as possible, to make it easier to implement. 44 | - Remember that this is a volunteer-driven project, and that contributions 45 | are welcome :) 46 | 47 | ## Get Started! 48 | 49 | Ready to contribute? Here's how to set up _cattrs_ for local development. 50 | 51 | 1. Fork the `cattrs` repo on GitHub. 52 | 2. Clone your fork locally:: 53 | 54 | ```shell 55 | $ git clone git@github.com:your_name_here/cattrs.git 56 | ``` 57 | 58 | 3. Install your local copy into a virtualenv. Assuming you have [PDM](https://pdm.fming.dev/latest/) installed, this is how you set up your fork for local development:: 59 | 60 | ```shell 61 | $ cd cattrs/ 62 | $ pdm install -d -G :all 63 | ``` 64 | 65 | 4. Create a branch for local development:: 66 | 67 | ```shell 68 | $ git checkout -b name-of-your-bugfix-or-feature 69 | ``` 70 | 71 | Now you can make your changes locally. 72 | 73 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 74 | 75 | ```shell 76 | $ make lint 77 | $ make test 78 | $ tox 79 | ``` 80 | 81 | 6. Commit your changes and push your branch to GitHub:: 82 | 83 | ```shell 84 | $ git add . 85 | $ git commit -m "Your detailed description of your changes." 86 | $ git push origin name-of-your-bugfix-or-feature 87 | ``` 88 | 89 | 7. Submit a pull request through the GitHub website. 90 | 91 | ## Pull Request Guidelines 92 | 93 | Before you submit a pull request, check that it meets these guidelines: 94 | 95 | 1. The pull request should include tests. 96 | 2. If the pull request adds functionality, the docs should be updated. Put 97 | your new functionality into a function with a docstring, and add the 98 | feature to the list in README.rst. 99 | 3. The pull request should work for all supported Python versions. Check 100 | [https://github.com/python-attrs/cattrs/actions](https://github.com/python-attrs/cattrs/actions) 101 | and make sure that the tests pass for all supported Python versions. 102 | 4. Don't forget to add a line to HISTORY.md. 103 | 104 | ## Tips 105 | 106 | To run a subset of tests: 107 | 108 | ```shell 109 | $ pdm run pytest tests.test_unstructure 110 | ``` 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2016, Tin Tvrtković 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | include src/cattr/py.typed 7 | 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help bench bench-cmp test 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | 32 | clean-build: ## remove build artifacts 33 | rm -fr build/ 34 | rm -fr dist/ 35 | rm -fr .eggs/ 36 | find . -name '*.egg-info' -exec rm -fr {} + 37 | find . -name '*.egg' -exec rm -f {} + 38 | 39 | clean-pyc: ## remove Python file artifacts 40 | find . -name '*.pyc' -exec rm -f {} + 41 | find . -name '*.pyo' -exec rm -f {} + 42 | find . -name '*~' -exec rm -f {} + 43 | find . -name '__pycache__' -exec rm -fr {} + 44 | 45 | clean-test: ## remove test and coverage artifacts 46 | rm -fr .tox/ 47 | rm -f .coverage 48 | rm -fr htmlcov/ 49 | 50 | lint: ## check style with ruff and black 51 | pdm run ruff check src/ tests bench 52 | pdm run black --check src tests docs/conf.py 53 | 54 | test: ## run tests quickly with the default Python 55 | pdm run pytest -x --ff -n auto tests 56 | 57 | 58 | test-all: ## run tests on every Python version with tox 59 | tox 60 | 61 | coverage: ## check code coverage quickly with the default Python 62 | pdm run coverage run --source cattrs -m pytest -n auto tests 63 | 64 | pdm run coverage report -m 65 | pdm run coverage html 66 | $(BROWSER) htmlcov/index.html 67 | 68 | docs: ## generate Sphinx HTML documentation, including API docs 69 | $(MAKE) -C docs clean 70 | $(MAKE) -C docs doctest 71 | $(MAKE) -C docs html 72 | 73 | htmllive: docs ## compile the docs watching for changes 74 | $(MAKE) -C docs htmllive 75 | 76 | bench-cmp: 77 | pytest bench --benchmark-compare 78 | 79 | bench: 80 | pytest bench --benchmark-save base 81 | -------------------------------------------------------------------------------- /bench/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-attrs/cattrs/98940957f32196c1dc7f40762518451e342dd156/bench/__init__.py -------------------------------------------------------------------------------- /bench/test_attrs_collections.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from typing import Dict, List, Mapping, MutableMapping 3 | 4 | import attr 5 | import pytest 6 | 7 | from cattr import BaseConverter, Converter, UnstructureStrategy 8 | 9 | 10 | @pytest.mark.parametrize("converter_cls", [BaseConverter, Converter]) 11 | @pytest.mark.parametrize( 12 | "unstructure_strat", [UnstructureStrategy.AS_DICT, UnstructureStrategy.AS_TUPLE] 13 | ) 14 | def test_unstructure_attrs_lists(benchmark, converter_cls, unstructure_strat): 15 | """ 16 | Benchmark a large (30 attributes) attrs class containing lists of 17 | primitives. 18 | """ 19 | 20 | class E(IntEnum): 21 | ONE = 1 22 | TWO = 2 23 | 24 | @attr.define 25 | class C: 26 | a: List[int] 27 | b: List[float] 28 | c: List[str] 29 | d: List[bytes] 30 | e: List[E] 31 | f: List[int] 32 | g: List[float] 33 | h: List[str] 34 | i: List[bytes] 35 | j: List[E] 36 | k: List[int] 37 | l: List[float] # noqa: E741 38 | m: List[str] 39 | n: List[bytes] 40 | o: List[E] 41 | p: List[int] 42 | q: List[float] 43 | r: List[str] 44 | s: List[bytes] 45 | t: List[E] 46 | u: List[int] 47 | v: List[float] 48 | w: List[str] 49 | x: List[bytes] 50 | y: List[E] 51 | z: List[int] 52 | aa: List[float] 53 | ab: List[str] 54 | ac: List[bytes] 55 | ad: List[E] 56 | 57 | c = converter_cls(unstruct_strat=unstructure_strat) 58 | 59 | benchmark( 60 | c.unstructure, 61 | C( 62 | [1] * 3, 63 | [1.0] * 3, 64 | ["a small string"] * 3, 65 | [b"test"] * 3, 66 | [E.ONE] * 3, 67 | [2] * 3, 68 | [2.0] * 3, 69 | ["a small string"] * 3, 70 | [b"test"] * 3, 71 | [E.TWO] * 3, 72 | [3] * 3, 73 | [3.0] * 3, 74 | ["a small string"] * 3, 75 | [b"test"] * 3, 76 | [E.ONE] * 3, 77 | [4] * 3, 78 | [4.0] * 3, 79 | ["a small string"] * 3, 80 | [b"test"] * 3, 81 | [E.TWO] * 3, 82 | [5] * 3, 83 | [5.0] * 3, 84 | ["a small string"] * 3, 85 | [b"test"] * 3, 86 | [E.ONE] * 3, 87 | [6] * 3, 88 | [6.0] * 3, 89 | ["a small string"] * 3, 90 | [b"test"] * 3, 91 | [E.TWO] * 3, 92 | ), 93 | ) 94 | 95 | 96 | @pytest.mark.parametrize("converter_cls", [BaseConverter, Converter]) 97 | @pytest.mark.parametrize( 98 | "unstructure_strat", [UnstructureStrategy.AS_DICT, UnstructureStrategy.AS_TUPLE] 99 | ) 100 | def test_unstructure_attrs_mappings(benchmark, converter_cls, unstructure_strat): 101 | """ 102 | Benchmark an attrs class containing mappings. 103 | """ 104 | 105 | @attr.frozen 106 | class FrozenCls: 107 | a: int 108 | 109 | @attr.define 110 | class C: 111 | a: Mapping[int, str] 112 | b: Dict[float, bytes] 113 | c: MutableMapping[int, FrozenCls] 114 | 115 | c = converter_cls(unstruct_strat=unstructure_strat) 116 | 117 | benchmark( 118 | c.unstructure, 119 | C( 120 | {i: str(i) for i in range(30)}, 121 | {float(i): bytes(i) for i in range(30)}, 122 | {i: FrozenCls(i) for i in range(30)}, 123 | ), 124 | ) 125 | 126 | 127 | @pytest.mark.parametrize("converter_cls", [BaseConverter, Converter]) 128 | def test_structure_attrs_mappings(benchmark, converter_cls): 129 | """ 130 | Benchmark an attrs class containing mappings. 131 | """ 132 | 133 | @attr.frozen 134 | class FrozenCls: 135 | a: int 136 | 137 | @attr.define 138 | class C: 139 | a: Mapping[int, str] 140 | b: Dict[float, bytes] 141 | c: MutableMapping[int, FrozenCls] 142 | 143 | c = converter_cls() 144 | 145 | inst = C( 146 | {i: str(i) for i in range(30)}, 147 | {float(i): bytes(i) for i in range(30)}, 148 | {i: FrozenCls(i) for i in range(30)}, 149 | ) 150 | raw = c.unstructure(inst) 151 | 152 | benchmark(c.structure, raw, C) 153 | -------------------------------------------------------------------------------- /bench/test_attrs_nested.py: -------------------------------------------------------------------------------- 1 | """Benchmark attrs containing other attrs classes.""" 2 | 3 | import pytest 4 | from attrs import define 5 | 6 | from cattr import BaseConverter, Converter, UnstructureStrategy 7 | 8 | 9 | @pytest.mark.parametrize("converter_cls", [BaseConverter, Converter]) 10 | @pytest.mark.parametrize( 11 | "unstructure_strat", [UnstructureStrategy.AS_DICT, UnstructureStrategy.AS_TUPLE] 12 | ) 13 | def test_unstructure_attrs_nested(benchmark, converter_cls, unstructure_strat): 14 | c = converter_cls(unstruct_strat=unstructure_strat) 15 | 16 | @define 17 | class InnerA: 18 | a: int 19 | b: float 20 | c: str 21 | d: bytes 22 | 23 | @define 24 | class InnerB: 25 | a: int 26 | b: float 27 | c: str 28 | d: bytes 29 | 30 | @define 31 | class InnerC: 32 | a: int 33 | b: float 34 | c: str 35 | d: bytes 36 | 37 | @define 38 | class InnerD: 39 | a: int 40 | b: float 41 | c: str 42 | d: bytes 43 | 44 | @define 45 | class InnerE: 46 | a: int 47 | b: float 48 | c: str 49 | d: bytes 50 | 51 | @define 52 | class Outer: 53 | a: InnerA 54 | b: InnerB 55 | c: InnerC 56 | d: InnerD 57 | e: InnerE 58 | 59 | inst = Outer( 60 | InnerA(1, 1.0, "one", b"one"), 61 | InnerB(2, 2.0, "two", b"two"), 62 | InnerC(3, 3.0, "three", b"three"), 63 | InnerD(4, 4.0, "four", b"four"), 64 | InnerE(5, 5.0, "five", b"five"), 65 | ) 66 | 67 | benchmark(c.unstructure, inst) 68 | 69 | 70 | @pytest.mark.parametrize("converter_cls", [BaseConverter, Converter]) 71 | @pytest.mark.parametrize( 72 | "unstructure_strat", [UnstructureStrategy.AS_DICT, UnstructureStrategy.AS_TUPLE] 73 | ) 74 | def test_unstruct_attrs_deep_nest(benchmark, converter_cls, unstructure_strat): 75 | c = converter_cls(unstruct_strat=unstructure_strat) 76 | 77 | @define 78 | class InnerA: 79 | a: int 80 | b: float 81 | c: str 82 | d: bytes 83 | 84 | @define 85 | class InnerB: 86 | a: InnerA 87 | b: InnerA 88 | c: InnerA 89 | d: InnerA 90 | 91 | @define 92 | class InnerC: 93 | a: InnerB 94 | b: InnerB 95 | c: InnerB 96 | d: InnerB 97 | 98 | @define 99 | class InnerD: 100 | a: InnerC 101 | b: InnerC 102 | c: InnerC 103 | d: InnerC 104 | 105 | @define 106 | class InnerE: 107 | a: InnerD 108 | b: InnerD 109 | c: InnerD 110 | d: InnerD 111 | 112 | @define 113 | class Outer: 114 | a: InnerE 115 | b: InnerE 116 | c: InnerE 117 | d: InnerE 118 | 119 | def make_inner_a(): 120 | return InnerA(1, 1.0, "one", b"one") 121 | 122 | def make_inner_b(): 123 | return InnerB(*[make_inner_a() for _ in range(4)]) 124 | 125 | def make_inner_c(): 126 | return InnerC(*[make_inner_b() for _ in range(4)]) 127 | 128 | def make_inner_d(): 129 | return InnerD(*[make_inner_c() for _ in range(4)]) 130 | 131 | def make_inner_e(): 132 | return InnerE(*[make_inner_d() for _ in range(4)]) 133 | 134 | inst = Outer(*[make_inner_e() for _ in range(4)]) 135 | 136 | benchmark(c.unstructure, inst) 137 | -------------------------------------------------------------------------------- /bench/test_attrs_primitives.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | import attr 4 | import pytest 5 | 6 | from cattr import BaseConverter, Converter, UnstructureStrategy 7 | 8 | 9 | class E(IntEnum): 10 | ONE = 1 11 | TWO = 2 12 | 13 | 14 | @attr.define 15 | class C: 16 | a: int 17 | b: float 18 | c: str 19 | d: bytes 20 | e: E 21 | f: int 22 | g: float 23 | h: str 24 | i: bytes 25 | j: E 26 | k: int 27 | l: float # noqa: E741 28 | m: str 29 | n: bytes 30 | o: E 31 | p: int 32 | q: float 33 | r: str 34 | s: bytes 35 | t: E 36 | u: int 37 | v: float 38 | w: str 39 | x: bytes 40 | y: E 41 | z: int 42 | aa: float 43 | ab: str 44 | ac: bytes 45 | ad: E 46 | 47 | 48 | @pytest.mark.parametrize("converter_cls", [BaseConverter, Converter]) 49 | @pytest.mark.parametrize( 50 | "unstructure_strat", [UnstructureStrategy.AS_DICT, UnstructureStrategy.AS_TUPLE] 51 | ) 52 | def test_unstructure_attrs_primitives(benchmark, converter_cls, unstructure_strat): 53 | """Benchmark a large (30 attributes) attrs class containing primitives.""" 54 | 55 | c = converter_cls(unstruct_strat=unstructure_strat) 56 | 57 | benchmark( 58 | c.unstructure, 59 | C( 60 | 1, 61 | 1.0, 62 | "a small string", 63 | b"test", 64 | E.ONE, 65 | 2, 66 | 2.0, 67 | "a small string", 68 | b"test", 69 | E.TWO, 70 | 3, 71 | 3.0, 72 | "a small string", 73 | b"test", 74 | E.ONE, 75 | 4, 76 | 4.0, 77 | "a small string", 78 | b"test", 79 | E.TWO, 80 | 5, 81 | 5.0, 82 | "a small string", 83 | b"test", 84 | E.ONE, 85 | 6, 86 | 6.0, 87 | "a small string", 88 | b"test", 89 | E.TWO, 90 | ), 91 | ) 92 | 93 | 94 | @pytest.mark.parametrize("converter_cls", [BaseConverter, Converter]) 95 | @pytest.mark.parametrize( 96 | "unstructure_strat", [UnstructureStrategy.AS_DICT, UnstructureStrategy.AS_TUPLE] 97 | ) 98 | def test_structure_attrs_primitives(benchmark, converter_cls, unstructure_strat): 99 | """Benchmark a large (30 attributes) attrs class containing primitives.""" 100 | 101 | c = converter_cls(unstruct_strat=unstructure_strat) 102 | 103 | inst = C( 104 | 1, 105 | 1.0, 106 | "a small string", 107 | b"test", 108 | E.ONE, 109 | 2, 110 | 2.0, 111 | "a small string", 112 | b"test", 113 | E.TWO, 114 | 3, 115 | 3.0, 116 | "a small string", 117 | b"test", 118 | E.ONE, 119 | 4, 120 | 4.0, 121 | "a small string", 122 | b"test", 123 | E.TWO, 124 | 5, 125 | 5.0, 126 | "a small string", 127 | b"test", 128 | E.ONE, 129 | 6, 130 | 6.0, 131 | "a small string", 132 | b"test", 133 | E.TWO, 134 | ) 135 | 136 | raw = c.unstructure(inst) 137 | 138 | benchmark(c.structure, raw, C) 139 | -------------------------------------------------------------------------------- /bench/test_primitives.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cattr import BaseConverter, Converter 4 | 5 | 6 | @pytest.mark.parametrize("converter_cls", [BaseConverter, Converter]) 7 | def test_unstructure_int(benchmark, converter_cls): 8 | c = converter_cls() 9 | 10 | benchmark(c.unstructure, 5) 11 | 12 | 13 | @pytest.mark.parametrize("converter_cls", [BaseConverter, Converter]) 14 | def test_unstructure_float(benchmark, converter_cls): 15 | c = converter_cls() 16 | 17 | benchmark(c.unstructure, 15.0) 18 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | 3 | :root { 4 | font-feature-settings: 'liga' 1, 'calt' 1; /* fix for Chrome */ 5 | } 6 | 7 | @supports (font-variation-settings: normal) { 8 | :root { font-family: InterVariable, sans-serif; } 9 | } 10 | 11 | /* ubuntu-mono-regular - latin */ 12 | @font-face { 13 | font-family: "Ubuntu Mono"; 14 | font-style: normal; 15 | font-weight: 400; 16 | src: local(""), 17 | url("./fonts/ubuntu-mono-v15-latin-regular.woff2") format("woff2"), 18 | /* Chrome 26+, Opera 23+, Firefox 39+ */ 19 | url("./fonts/ubuntu-mono-v15-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ 20 | } 21 | 22 | /* ubuntu-mono-italic - latin */ 23 | @font-face { 24 | font-family: "Ubuntu Mono"; 25 | font-style: italic; 26 | font-weight: 400; 27 | src: local(""), 28 | url("./fonts/ubuntu-mono-v15-latin-italic.woff2") format("woff2"), 29 | /* Chrome 26+, Opera 23+, Firefox 39+ */ 30 | url("./fonts/ubuntu-mono-v15-latin-italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ 31 | } 32 | 33 | /* ubuntu-mono-700 - latin */ 34 | @font-face { 35 | font-family: "Ubuntu Mono"; 36 | font-style: normal; 37 | font-weight: 700; 38 | src: local(""), url("./fonts/ubuntu-mono-v15-latin-700.woff2") format("woff2"), 39 | /* Chrome 26+, Opera 23+, Firefox 39+ */ 40 | url("./fonts/ubuntu-mono-v15-latin-700.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ 41 | } 42 | 43 | /* ubuntu-mono-700italic - latin */ 44 | @font-face { 45 | font-family: "Ubuntu Mono"; 46 | font-style: italic; 47 | font-weight: 700; 48 | src: local(""), 49 | url("./fonts/ubuntu-mono-v15-latin-700italic.woff2") format("woff2"), 50 | /* Chrome 26+, Opera 23+, Firefox 39+ */ 51 | url("./fonts/ubuntu-mono-v15-latin-700italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ 52 | } 53 | 54 | h2, 55 | h3 { 56 | margin-bottom: 0.5em; 57 | margin-top: 2rem; 58 | } 59 | 60 | :target > h1:first-of-type, 61 | :target > h2:first-of-type, 62 | :target > h3:first-of-type, 63 | span:target ~ h1:first-of-type, 64 | span:target ~ h2:first-of-type, 65 | span:target ~ h3:first-of-type, 66 | span:target ~ h4:first-of-type, 67 | span:target ~ h5:first-of-type, 68 | span:target ~ h6:first-of-type { 69 | text-decoration: underline dashed; 70 | text-decoration-thickness: 1px; 71 | } 72 | 73 | div.article-container > article { 74 | font-size: 17px; 75 | line-height: 29px; 76 | } 77 | 78 | div.admonition { 79 | font-size: 15px; 80 | line-height: 27px; 81 | margin-top: 2em; 82 | margin-bottom: 2em; 83 | } 84 | 85 | p.admonition-title { 86 | font-size: 15px !important; 87 | line-height: 20px !important; 88 | } 89 | 90 | article > li > a { 91 | font-size: 19px; 92 | line-height: 29px; 93 | } 94 | 95 | div.tab-set { 96 | margin-top: 1em; 97 | margin-bottom: 2em; 98 | } 99 | 100 | div.tab-set pre { 101 | padding: 1.25em; 102 | } 103 | 104 | body .highlight.only_dark { 105 | background: #18181a; 106 | } 107 | -------------------------------------------------------------------------------- /docs/_static/fonts/ubuntu-mono-v15-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-attrs/cattrs/98940957f32196c1dc7f40762518451e342dd156/docs/_static/fonts/ubuntu-mono-v15-latin-700.woff -------------------------------------------------------------------------------- /docs/_static/fonts/ubuntu-mono-v15-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-attrs/cattrs/98940957f32196c1dc7f40762518451e342dd156/docs/_static/fonts/ubuntu-mono-v15-latin-700.woff2 -------------------------------------------------------------------------------- /docs/_static/fonts/ubuntu-mono-v15-latin-700italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-attrs/cattrs/98940957f32196c1dc7f40762518451e342dd156/docs/_static/fonts/ubuntu-mono-v15-latin-700italic.woff -------------------------------------------------------------------------------- /docs/_static/fonts/ubuntu-mono-v15-latin-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-attrs/cattrs/98940957f32196c1dc7f40762518451e342dd156/docs/_static/fonts/ubuntu-mono-v15-latin-700italic.woff2 -------------------------------------------------------------------------------- /docs/_static/fonts/ubuntu-mono-v15-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-attrs/cattrs/98940957f32196c1dc7f40762518451e342dd156/docs/_static/fonts/ubuntu-mono-v15-latin-italic.woff -------------------------------------------------------------------------------- /docs/_static/fonts/ubuntu-mono-v15-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-attrs/cattrs/98940957f32196c1dc7f40762518451e342dd156/docs/_static/fonts/ubuntu-mono-v15-latin-italic.woff2 -------------------------------------------------------------------------------- /docs/_static/fonts/ubuntu-mono-v15-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-attrs/cattrs/98940957f32196c1dc7f40762518451e342dd156/docs/_static/fonts/ubuntu-mono-v15-latin-regular.woff -------------------------------------------------------------------------------- /docs/_static/fonts/ubuntu-mono-v15-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-attrs/cattrs/98940957f32196c1dc7f40762518451e342dd156/docs/_static/fonts/ubuntu-mono-v15-latin-regular.woff2 -------------------------------------------------------------------------------- /docs/basics.md: -------------------------------------------------------------------------------- 1 | # The Basics 2 | ```{currentmodule} cattrs 3 | ``` 4 | 5 | All _cattrs_ functionality is exposed through a {class}`cattrs.Converter` object. 6 | A global converter is provided for convenience as {data}`cattrs.global_converter` 7 | but more complex customizations should be performed on private instances, any number of which can be made. 8 | 9 | 10 | ## Converters and Hooks 11 | 12 | The core functionality of a converter is structuring and unstructuring data by composing [provided](defaulthooks.md) and [custom handling functions](customizing.md), called _hooks_. 13 | 14 | To create a private converter, instantiate a {class}`cattrs.Converter`. Converters are relatively cheap; users are encouraged to have as many as they need. 15 | 16 | The two main methods, {meth}`structure ` and {meth}`unstructure `, are used to convert between _structured_ and _unstructured_ data. 17 | 18 | ```{doctest} basics 19 | >>> from cattrs import structure, unstructure 20 | >>> from attrs import define 21 | 22 | >>> @define 23 | ... class Model: 24 | ... a: int 25 | 26 | >>> unstructure(Model(1)) 27 | {'a': 1} 28 | >>> structure({"a": 1}, Model) 29 | Model(a=1) 30 | ``` 31 | 32 | _cattrs_ comes with a rich library of un/structuring hooks by default but it excels at composing custom hooks with built-in ones. 33 | 34 | The simplest approach to customization is writing a new hook from scratch. 35 | For example, we can write our own hook for the `int` class and register it to a converter. 36 | 37 | ```{doctest} basics 38 | >>> from cattrs import Converter 39 | 40 | >>> converter = Converter() 41 | 42 | >>> @converter.register_structure_hook 43 | ... def int_hook(value, type) -> int: 44 | ... if not isinstance(value, int): 45 | ... raise ValueError('not an int!') 46 | ... return value 47 | ``` 48 | 49 | Now, any other hook converting an `int` will use it. 50 | 51 | Another approach to customization is wrapping (composing) an existing hook with your own function. 52 | A base hook can be obtained from a converter and then be subjected to the very rich machinery of function composition that Python offers. 53 | 54 | 55 | ```{doctest} basics 56 | >>> base_hook = converter.get_structure_hook(Model) 57 | 58 | >>> @converter.register_structure_hook 59 | ... def my_model_hook(value, type) -> Model: 60 | ... # Apply any preprocessing to the value. 61 | ... result = base_hook(value, type) 62 | ... # Apply any postprocessing to the model. 63 | ... return result 64 | ``` 65 | 66 | (`cattrs.structure({}, Model)` is equivalent to `cattrs.get_structure_hook(Model)({}, Model)`.) 67 | 68 | Now if we use this hook to structure a `Model`, through ✨the magic of function composition✨ that hook will use our old `int_hook`. 69 | 70 | ```python 71 | >>> converter.structure({"a": "1"}, Model) 72 | + Exception Group Traceback (most recent call last): 73 | | File "...", line 22, in 74 | | base_hook({"a": "1"}, Model) 75 | | File "", line 9, in structure_Model 76 | | cattrs.errors.ClassValidationError: While structuring Model (1 sub-exception) 77 | +-+---------------- 1 ---------------- 78 | | Traceback (most recent call last): 79 | | File "", line 5, in structure_Model 80 | | File "...", line 15, in my_int_hook 81 | | raise ValueError("not an int!") 82 | | ValueError: not an int! 83 | | Structuring class Model @ attribute a 84 | +------------------------------------ 85 | ``` 86 | 87 | To continue reading about customizing _cattrs_, see [](customizing.md). 88 | More advanced structuring customizations are commonly called [](strategies.md). 89 | 90 | ## Global Converter 91 | 92 | Global _cattrs_ functions, such as {meth}`cattrs.structure`, use a single {data}`global converter `. 93 | Changes done to this global converter, such as registering new structure and unstructure hooks, affect all code using the global functions. 94 | 95 | The following functions implicitly use this global converter: 96 | 97 | - {meth}`cattrs.structure` 98 | - {meth}`cattrs.unstructure` 99 | - {meth}`cattrs.get_structure_hook` 100 | - {meth}`cattrs.get_unstructure_hook` 101 | - {meth}`cattrs.structure_attrs_fromtuple` 102 | - {meth}`cattrs.structure_attrs_fromdict` 103 | 104 | Changes made to the global converter will affect the behavior of these functions. 105 | 106 | Larger applications are strongly encouraged to create and customize different, private instances of {class}`cattrs.Converter`. 107 | -------------------------------------------------------------------------------- /docs/benchmarking.md: -------------------------------------------------------------------------------- 1 | # Benchmarking 2 | 3 | cattrs includes a benchmarking suite to help detect performance regressions and 4 | guide performance optimizations. 5 | 6 | The suite is based on pytest and pytest-benchmark. Benchmarks are similar to 7 | tests, with the exception of being stored in the `bench/` directory and being 8 | used to verify performance instead of correctness. 9 | 10 | ## A Sample Workflow 11 | 12 | First, ensure the system you're benchmarking on is as stable as possible. For 13 | example, the pyperf library has a `system tune` command that can tweak 14 | CPU frequency governors. You also might want to quit as many applications as 15 | possible and run the benchmark suite on isolated CPU cores (`taskset` can be 16 | used for this purpose on Linux). 17 | 18 | Then, generate a baseline using `make bench`. This will run the benchmark suite 19 | and save it into a file. 20 | 21 | Following that, implement the changes you have in mind. Run the test suite to 22 | ensure correctness. Then, compare the performance of the new code to the saved 23 | baseline using `make bench-cmp`. If the code is still correct but faster, 24 | congratulations! 25 | -------------------------------------------------------------------------------- /docs/cattrs.gen.rst: -------------------------------------------------------------------------------- 1 | cattrs.gen package 2 | ================== 3 | 4 | .. automodule:: cattrs.gen 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cattrs.gen.typeddicts module 13 | ---------------------------- 14 | 15 | .. automodule:: cattrs.gen.typeddicts 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/cattrs.preconf.rst: -------------------------------------------------------------------------------- 1 | cattrs.preconf package 2 | ====================== 3 | 4 | .. automodule:: cattrs.preconf 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cattrs.preconf.bson module 13 | -------------------------- 14 | 15 | .. automodule:: cattrs.preconf.bson 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cattrs.preconf.cbor2 module 21 | --------------------------- 22 | 23 | .. automodule:: cattrs.preconf.cbor2 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | cattrs.preconf.json module 29 | -------------------------- 30 | 31 | .. automodule:: cattrs.preconf.json 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | cattrs.preconf.msgpack module 37 | ----------------------------- 38 | 39 | .. automodule:: cattrs.preconf.msgpack 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | cattrs.preconf.msgspec module 45 | ----------------------------- 46 | 47 | .. automodule:: cattrs.preconf.msgspec 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | cattrs.preconf.orjson module 53 | ---------------------------- 54 | 55 | .. automodule:: cattrs.preconf.orjson 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | cattrs.preconf.pyyaml module 61 | ---------------------------- 62 | 63 | .. automodule:: cattrs.preconf.pyyaml 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | cattrs.preconf.tomlkit module 69 | ----------------------------- 70 | 71 | .. automodule:: cattrs.preconf.tomlkit 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | 76 | cattrs.preconf.ujson module 77 | --------------------------- 78 | 79 | .. automodule:: cattrs.preconf.ujson 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | -------------------------------------------------------------------------------- /docs/cattrs.rst: -------------------------------------------------------------------------------- 1 | cattrs package 2 | ============== 3 | 4 | .. automodule:: cattrs 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | cattrs.gen 16 | cattrs.preconf 17 | cattrs.strategies 18 | 19 | Submodules 20 | ---------- 21 | 22 | cattrs.cols module 23 | ------------------ 24 | 25 | .. automodule:: cattrs.cols 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | cattrs.disambiguators module 31 | ---------------------------- 32 | 33 | .. automodule:: cattrs.disambiguators 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | cattrs.dispatch module 39 | ---------------------- 40 | 41 | .. automodule:: cattrs.dispatch 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | cattrs.errors module 47 | -------------------- 48 | 49 | .. automodule:: cattrs.errors 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | cattrs.fns module 55 | ----------------- 56 | 57 | .. automodule:: cattrs.fns 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | cattrs.v module 63 | --------------- 64 | 65 | .. automodule:: cattrs.v 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | -------------------------------------------------------------------------------- /docs/cattrs.strategies.rst: -------------------------------------------------------------------------------- 1 | cattrs.strategies package 2 | ========================= 3 | 4 | .. automodule:: cattrs.strategies 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | ```{include} ../CONTRIBUTING.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /docs/history.md: -------------------------------------------------------------------------------- 1 | ```{include} ../HISTORY.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # *cattrs*: Flexible Object Serialization and Validation 2 | 3 | *Because validation belongs to the edges.* 4 | 5 | --- 6 | 7 | ```{include} ../README.md 8 | :start-after: "begin-teaser -->" 9 | :end-before: "" 14 | :end-before: "" 20 | :end-before: "" 95 | :end-before: "