From c49868a095abe60d320621726df21a0922381798 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:33:04 +0000 Subject: [PATCH] Fetch upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit source /home/aleksul/projects/pydantic/venv/bin/activatebuild(deps): bump actions/setup-python from 2 to 3 (#3868) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 3. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump actions/checkout from 2 to 3 (#3869) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump mkdocs-material from 8.1.3 to 8.2.3 (#3865) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.1.3 to 8.2.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.1.3...8.2.3) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump coverage from 6.2 to 6.3.2 (#3839) Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.2 to 6.3.2. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/6.2...6.3.2) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Fixed a typo in decimal_encoder's doc. (#3820) build(deps): bump pre-commit from 2.16.0 to 2.17.0 (#3731) Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.16.0 to 2.17.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/master/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.16.0...v2.17.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump mypy from 0.930 to 0.931 (#3656) Bumps [mypy](https://github.com/python/mypy) from 0.930 to 0.931. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.930...v0.931) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> docs: fix typo in settings management page (#3781) build(deps): bump black from 21.12b0 to 22.3.0 (#3950) * build(deps): bump black from 21.12b0 to 22.3.0 Bumps [black](https://github.com/psf/black) from 21.12b0 to 22.3.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/commits/22.3.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production ... Signed-off-by: dependabot[bot] * apply new black styles, fix docs * try upgrading pip before fastapi tests Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Samuel Colvin build(deps): bump python-dotenv from 0.19.2 to 0.20.0 (#3963) Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 0.19.2 to 0.20.0. - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/master/CHANGELOG.md) - [Commits](https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0) --- updated-dependencies: - dependency-name: python-dotenv dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump hypothesis from 6.31.6 to 6.41.0 (#3964) Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.31.6 to 6.41.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.31.6...hypothesis-python-6.41.0) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump pytest from 6.2.5 to 7.1.1 (#3926) Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.1.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.5...7.1.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump twine from 3.7.1 to 4.0.0 (#3965) Bumps [twine](https://github.com/pypa/twine) from 3.7.1 to 4.0.0. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/3.7.1...4.0.0) --- updated-dependencies: - dependency-name: twine dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump cython from 0.29.26 to 0.29.28 (#3871) Bumps [cython](https://github.com/cython/cython) from 0.29.26 to 0.29.28. - [Release notes](https://github.com/cython/cython/releases) - [Changelog](https://github.com/cython/cython/blob/master/CHANGES.rst) - [Commits](https://github.com/cython/cython/compare/0.29.26...0.29.28) --- updated-dependencies: - dependency-name: cython dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump typing-extensions from 4.0.1 to 4.1.1 (#3874) Bumps [typing-extensions](https://github.com/python/typing) from 4.0.1 to 4.1.1. - [Release notes](https://github.com/python/typing/releases) - [Changelog](https://github.com/python/typing/blob/master/typing_extensions/CHANGELOG) - [Commits](https://github.com/python/typing/compare/4.0.1...4.1.1) --- updated-dependencies: - dependency-name: typing-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump pytest-mock from 3.6.1 to 3.7.0 (#3967) Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.6.1 to 3.7.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.6.1...v3.7.0) --- updated-dependencies: - dependency-name: pytest-mock dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Combine dependabot prs (#3969) * allow combining of dependabot PRs * add combine-dependabot.yml CentOS 7: `read_text(encoding='utf-8')` (#3625) With CentOS 7 Python 3.6, running install from source with pip failed: sudo podman run -ti --rm centos:7 yum -y update yum -y install epel-release yum -y install git python3 python3-devel python3-pip python3-setuptools python3-wheel git clone https://github.com/samuelcolvin/pydantic.git cd pydantic pip3 install . With following error message: [root@c99d0585636c pydantic]# pip3 install . Processing /pydantic Complete output from command python setup.py egg_info: Traceback (most recent call last): File "", line 1, in File "/tmp/pip-91v_ixvz-build/setup.py", line 62, in history = (THIS_DIR / 'HISTORY.md').read_text() File "/usr/lib64/python3.6/pathlib.py", line 1197, in read_text return f.read() File "/usr/lib64/python3.6/encodings/ascii.py", line 26, in decode return codecs.ascii_decode(input, self.errors)[0] UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 14648: ordinal not in range(128) ---------------------------------------- Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-91v_ixvz-build/ This PR add the required `read_text(encoding='utf-8')` for `setup.py`. Signed-off-by: Wong Hoi Sing Edison fix: clarify that discriminated unions do not support singletons (#3639) Add Robusta.dev to list of Pydantic users (#3715) * add robusta.dev to pydantic users * update robusta.dev description and fix typo Prevent subclasses of bytes being converted to bytes (#3707) * adding a test * fix and add change description build(deps): bump mypy from 0.931 to 0.942 (#3968) * build(deps): bump mypy from 0.931 to 0.942 Bumps [mypy](https://github.com/python/mypy) from 0.931 to 0.942. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.931...v0.942) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * build(deps): bump mypy from 0.931 to 0.942 Bumps [mypy](https://github.com/python/mypy) from 0.931 to 0.942. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.931...v0.942) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * fix mypy Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Samuel Colvin Fix pytest crashes with hypothesis and pydantic (#3727) Pytest (sometimes?) crashes when it is invoked with `-vv` and pydantic and hypthesis are installed. This is because `_registered(typ)` modifies `_DEFINED_TYPES` while it is being iterated: ``` INTERNALERROR> File ".../lib/python3.9/site-packages/pydantic/_hypothesis_plugin.py", line 361, in INTERNALERROR> for typ in pydantic.types._DEFINED_TYPES: INTERNALERROR> File ".../lib/python3.9/_weakrefset.py", line 65, in __iter__ INTERNALERROR> for itemref in self.data: INTERNALERROR> RuntimeError: Set changed size during iteration ``` Remove incorrect comment about lazy evaluation of setting sources (#3806) * Remove incorrect comment about lazy evaluation of setting sources It looks like the current implementation always evaluates every source (https://github.com/samuelcolvin/pydantic/blob/9d631a3429a66f30742c1a52c94ac18ec6ba848d/pydantic/env_settings.py#L73) before coalescing them into a single dictionary to pass to `BaseModel`. So the comment about lazy evaluation is incorrect and should be removed. * Add changelog [no ci] correct name of change file Remove benchmarks completely (#3973) * removing benchmarks completely * [no ci] add change fix: `Config.copy_on_model_validation` does a deep copy and not a shallow one (#3642) * fix: `Config.copy_on_model_validation` does a deep copy and not a shallow one closes #3641 * fix: typo * use python 3.10 to run fastapi tests * fix fastapi test call Co-authored-by: Samuel Colvin build(deps): bump typing-extensions from 4.1.1 to 4.2.0 (#4040) Bumps [typing-extensions](https://github.com/python/typing) from 4.1.1 to 4.2.0. - [Release notes](https://github.com/python/typing/releases) - [Changelog](https://github.com/python/typing/blob/master/typing_extensions/CHANGELOG.md) - [Commits](https://github.com/python/typing/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: typing-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump mkdocs-material from 8.2.8 to 8.2.12 (#4038) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.2.8 to 8.2.12. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.2.8...8.2.12) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump pytest from 7.1.1 to 7.1.2 (#4037) Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.1.1...7.1.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump actions/upload-artifact from 2 to 3 (#4035) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump actions/download-artifact from 2 to 3 (#4034) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2 to 3. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump email-validator from 1.1.3 to 1.2.1 (#4060) Bumps [email-validator](https://github.com/JoshData/python-email-validator) from 1.1.3 to 1.2.1. - [Release notes](https://github.com/JoshData/python-email-validator/releases) - [Commits](https://github.com/JoshData/python-email-validator/compare/v1.1.3...v1.2.1) --- updated-dependencies: - dependency-name: email-validator dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump hypothesis from 6.41.0 to 6.46.3 (#4059) Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.41.0 to 6.46.3. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.41.0...hypothesis-python-6.46.3) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> build(deps): bump pre-commit from 2.17.0 to 2.19.0 (#4061) Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.17.0 to 2.19.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.17.0...v2.19.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> test pyright with pydantic (#3972) * test pyright with pydantic * rename file to avoid pytest running it * try another name 😴 * add docs about BaseSettings and Field * add change Fix regression in handling of nested dataclasses in `get_flat_models_from_field` (#3819) * add test for nested python dataclass schema generation * fix handling of dataclasses in `get_flat_models_from_field` * add change note Fix issue with self-referencing dataclass (#3713) * Fix issue with self-referencing dataclass * Fix mypy issue guard against ClassVar in fields (#4064) * guard against ClassVar in fields, fix #3679 * fix linting * skipif for test_class_var_forward_ref Fix issue with in-place modification of FieldInfo (#4067) * Fix info with in-place modification of field info * add changes * add test for 3714 * Update changes/4067-adriangb.md Co-authored-by: Samuel Colvin Co-authored-by: Samuel Colvin build(deps): bump mkdocs-material from 8.2.8 to 8.2.14 (#4063) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.2.8 to 8.2.14. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.2.8...8.2.14) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/dependabot.yml | 2 +- .github/workflows/ci.yml | 88 +++---- .github/workflows/combine-dependabot.yml | 137 +++++++++++ .github/workflows/upload-previews.yml | 2 +- .gitignore | 5 +- Makefile | 20 +- benchmarks/profile.py | 39 --- benchmarks/requirements.txt | 14 -- benchmarks/run.py | 294 ----------------------- benchmarks/test_cattrs.py | 99 -------- benchmarks/test_cerberus.py | 49 ---- benchmarks/test_drf.py | 53 ---- benchmarks/test_marshmallow.py | 45 ---- benchmarks/test_pydantic.py | 52 ---- benchmarks/test_schematics.py | 50 ---- benchmarks/test_trafaret.py | 46 ---- benchmarks/test_valideer.py | 47 ---- benchmarks/test_voluptuous.py | 51 ---- changes/3625-hswong3i.md | 1 + changes/3636-tommilligan.md | 1 + changes/3641-PrettyWood.md | 1 + changes/3675-uriyyo.md | 1 + changes/3679-samuelcolvin.md | 1 + changes/3706-samuelcolvin.md | 1 + changes/3806-garyd203.md | 1 + changes/3819-himbeles.md | 1 + changes/3972-samuelcolvin.md | 1 + changes/3973-samuelcolvin.md | 1 + changes/4067-adriangb.md | 1 + docs/benchmarks.md | 8 - docs/extra/redirects.js | 2 - docs/index.md | 8 +- docs/requirements.txt | 6 +- docs/usage/settings.md | 5 +- docs/usage/types.md | 6 + docs/usage/validation_decorator.md | 2 +- docs/visual_studio_code.md | 48 +++- mkdocs.yml | 1 - pydantic/_hypothesis_plugin.py | 2 +- pydantic/dataclasses.py | 9 +- pydantic/fields.py | 7 +- pydantic/generics.py | 1 + pydantic/json.py | 2 +- pydantic/main.py | 18 +- pydantic/networks.py | 10 +- pydantic/schema.py | 5 +- pydantic/types.py | 24 +- pydantic/validators.py | 2 +- requirements.txt | 8 +- setup.py | 4 +- tests/pyright/pyproject.toml | 4 + tests/pyright/pyright_example.py | 38 +++ tests/requirements-linting.txt | 10 +- tests/requirements-testing.txt | 10 +- tests/test_annotated.py | 20 ++ tests/test_construction.py | 4 +- tests/test_dataclasses.py | 8 + tests/test_discrimated_union.py | 13 +- tests/test_edge_cases.py | 26 ++ tests/test_forward_ref.py | 16 ++ tests/test_main.py | 22 +- tests/test_networks_ipaddress.py | 30 +-- tests/test_schema.py | 33 ++- tests/test_types.py | 8 +- 64 files changed, 519 insertions(+), 1005 deletions(-) create mode 100644 .github/workflows/combine-dependabot.yml delete mode 100644 benchmarks/profile.py delete mode 100644 benchmarks/requirements.txt delete mode 100644 benchmarks/run.py delete mode 100644 benchmarks/test_cattrs.py delete mode 100644 benchmarks/test_cerberus.py delete mode 100644 benchmarks/test_drf.py delete mode 100644 benchmarks/test_marshmallow.py delete mode 100644 benchmarks/test_pydantic.py delete mode 100644 benchmarks/test_schematics.py delete mode 100644 benchmarks/test_trafaret.py delete mode 100644 benchmarks/test_valideer.py delete mode 100644 benchmarks/test_voluptuous.py create mode 100644 changes/3625-hswong3i.md create mode 100644 changes/3636-tommilligan.md create mode 100644 changes/3641-PrettyWood.md create mode 100644 changes/3675-uriyyo.md create mode 100644 changes/3679-samuelcolvin.md create mode 100644 changes/3706-samuelcolvin.md create mode 100644 changes/3806-garyd203.md create mode 100644 changes/3819-himbeles.md create mode 100644 changes/3972-samuelcolvin.md create mode 100644 changes/3973-samuelcolvin.md create mode 100644 changes/4067-adriangb.md delete mode 100644 docs/benchmarks.md create mode 100644 tests/pyright/pyproject.toml create mode 100644 tests/pyright/pyright_example.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3acffa71cec..7a42e082063 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ updates: - package-ecosystem: pip directory: / schedule: - interval: weekly + interval: monthly - package-ecosystem: github-actions directory: / diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc81d4bfc6d..eeb7556de15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: 3.9 @@ -37,13 +37,22 @@ jobs: - name: check dist run: make check-dist + - name: install node for pyright + uses: actions/setup-node@v3 + with: + node-version: '14' + + - run: npm install -g pyright + + - run: make pyright + docs-build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v1 + uses: actions/setup-python@v3 with: python-version: 3.8 @@ -54,7 +63,7 @@ jobs: run: make docs - name: Store docs site - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: docs path: site @@ -71,10 +80,10 @@ jobs: OS: ubuntu steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} @@ -120,7 +129,7 @@ jobs: CONTEXT: linux-py${{ matrix.python-version }}-compiled-no-deps-no - name: store coverage files - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: coverage path: coverage @@ -140,10 +149,10 @@ jobs: runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} @@ -159,7 +168,7 @@ jobs: CONTEXT: ${{ matrix.os }}-py${{ matrix.python-version }} - name: store coverage files - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: coverage path: coverage @@ -173,10 +182,10 @@ jobs: mypy-version: ['0.910', '0.920', '0.921'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.10' @@ -200,7 +209,7 @@ jobs: CONTEXT: linux-py3.10-mypy${{ matrix.mypy-version }} - name: store coverage files - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: coverage path: coverage @@ -210,14 +219,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions/setup-python@v1 + - uses: actions/setup-python@v3 with: python-version: '3.8' - name: get coverage files - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: coverage path: coverage @@ -230,7 +239,7 @@ jobs: - run: coverage html --show-contexts - name: Store coverage html - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: coverage-html path: htmlcov @@ -239,10 +248,10 @@ jobs: name: test fastAPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.10' @@ -252,32 +261,9 @@ jobs: - name: test run: make test-fastapi - benchmark: - name: run benchmarks - runs-on: ubuntu-latest - env: - BENCHMARK_REPEATS: 1 - - steps: - - uses: actions/checkout@v2 - - - name: set up python - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - - name: install and build - run: | - make build - make install-benchmarks - - - run: make benchmark-pydantic - - run: make benchmark-all - - run: make benchmark-json - build: name: build py3.${{ matrix.python-version }} on ${{ matrix.platform || matrix.os }} - needs: [lint, test-linux, test-windows-mac, test-old-mypy, test-fastapi, benchmark] + needs: [lint, test-linux, test-windows-mac, test-old-mypy, test-fastapi] if: "success() && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master')" strategy: fail-fast: false @@ -292,10 +278,10 @@ jobs: runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.8' @@ -331,7 +317,7 @@ jobs: twine check dist/* - name: Store dist artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pypi_files path: dist @@ -343,21 +329,21 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.8' - name: get dist artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: pypi_files path: dist - name: get docs - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: docs path: site diff --git a/.github/workflows/combine-dependabot.yml b/.github/workflows/combine-dependabot.yml new file mode 100644 index 00000000000..1b141a75f0b --- /dev/null +++ b/.github/workflows/combine-dependabot.yml @@ -0,0 +1,137 @@ +# from https://github.com/hrvey/combine-prs-workflow/blob/master/combine-prs.yml +name: 'Combine Dependabot PRs' + +on: + workflow_dispatch: + inputs: + branchPrefix: + description: 'Branch prefix to find combinable PRs based on' + required: true + default: 'dependabot/' + mustBeGreen: + description: 'Only combine PRs that are green' + required: true + default: true + combineBranchName: + description: 'Name of the branch to combine PRs into' + required: true + default: 'combine-dependabot-bumps' + ignoreLabel: + description: 'Exclude PRs with this label' + required: true + default: 'nocombine' + +jobs: + combine-prs: + runs-on: ubuntu-latest + + steps: + - uses: actions/github-script@v6 + id: fetch-branch-names + name: Fetch branch names + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', { + owner: context.repo.owner, + repo: context.repo.repo + }); + branches = []; + prs = []; + base_branch = null; + for (const pull of pulls) { + const branch = pull['head']['ref']; + console.log('Pull for branch: ' + branch); + if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) { + console.log('Branch matched: ' + branch); + statusOK = true; + if(${{ github.event.inputs.mustBeGreen }}) { + console.log('Checking green status: ' + branch); + const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{ref}/status', { + owner: context.repo.owner, + repo: context.repo.repo, + ref: branch + }); + if(statuses.length > 0) { + const latest_status = statuses[0]['state']; + console.log('Validating status: ' + latest_status); + if(latest_status != 'success') { + console.log('Discarding ' + branch + ' with status ' + latest_status); + statusOK = false; + } + } + } + console.log('Checking labels: ' + branch); + const labels = pull['labels']; + for(const label of labels) { + const labelName = label['name']; + console.log('Checking label: ' + labelName); + if(labelName == '${{ github.event.inputs.ignoreLabel }}') { + console.log('Discarding ' + branch + ' with label ' + labelName); + statusOK = false; + } + } + if (statusOK) { + console.log('Adding branch to array: ' + branch); + branches.push(branch); + prs.push('#' + pull['number'] + ' ' + pull['title']); + base_branch = pull['base']['ref']; + } + } + } + if (branches.length == 0) { + core.setFailed('No PRs/branches matched criteria'); + return; + } + core.setOutput('base-branch', base_branch); + core.setOutput('prs-string', prs.join('\n')); + + combined = branches.join(' ') + console.log('Combined: ' + combined); + return combined + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # Creates a branch with other PR branches merged together + - name: Created combined branch + env: + BASE_BRANCH: ${{ steps.fetch-branch-names.outputs.base-branch }} + BRANCHES_TO_COMBINE: ${{ steps.fetch-branch-names.outputs.result }} + COMBINE_BRANCH_NAME: ${{ github.event.inputs.combineBranchName }} + run: | + echo "$BRANCHES_TO_COMBINE" + sourcebranches="${BRANCHES_TO_COMBINE%\"}" + sourcebranches="${sourcebranches#\"}" + + basebranch="${BASE_BRANCH%\"}" + basebranch="${basebranch#\"}" + + git config pull.rebase false + git config user.name github-actions + git config user.email github-actions@github.com + + git branch $COMBINE_BRANCH_NAME $basebranch + git checkout $COMBINE_BRANCH_NAME + git pull origin $sourcebranches --no-edit + git push origin $COMBINE_BRANCH_NAME + + # Creates a PR with the new combined branch + - uses: actions/github-script@v6 + name: Create Combined Pull Request + env: + PRS_STRING: ${{ steps.fetch-branch-names.outputs.prs-string }} + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const prString = process.env.PRS_STRING; + const body = 'This PR was created by the Combine PRs action by combining the following PRs:\n' + prString; + await github.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Combined Dependabot Bumps', + head: '${{ github.event.inputs.combineBranchName }}', + base: '${{ steps.fetch-branch-names.outputs.base-branch }}', + body: body + }); diff --git a/.github/workflows/upload-previews.yml b/.github/workflows/upload-previews.yml index 5325c4939db..0bec37843b5 100644 --- a/.github/workflows/upload-previews.yml +++ b/.github/workflows/upload-previews.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v1 + - uses: actions/setup-python@v3 with: python-version: '3.8' diff --git a/.gitignore b/.gitignore index e0c46182de1..55c22395276 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,7 @@ .idea/ env/ venv/ -env36/ -env37/ -env38/ -env39/ +env3*/ Pipfile *.lock *.py[cod] diff --git a/Makefile b/Makefile index 89bef90a605..cf846064eaf 100644 --- a/Makefile +++ b/Makefile @@ -21,10 +21,6 @@ install-testing: install-pydantic install-docs: install-pydantic pip install -U -r docs/requirements.txt -.PHONY: install-benchmarks -install-benchmarks: install-pydantic - pip install -U -r benchmarks/requirements.txt - .PHONY: install install: install-testing install-linting install-docs @echo 'installed development requirements' @@ -58,6 +54,10 @@ check-dist: mypy: mypy pydantic +.PHONY: pyright +pyright: + cd tests/pyright && pyright + .PHONY: test test: pytest --cov=pydantic @@ -85,18 +85,6 @@ test-fastapi: .PHONY: all all: lint mypy testcov -.PHONY: benchmark-all -benchmark-all: - python benchmarks/run.py - -.PHONY: benchmark-pydantic -benchmark-pydantic: - python benchmarks/run.py pydantic-only - -.PHONY: benchmark-json -benchmark-json: - TEST_JSON=1 python benchmarks/run.py - .PHONY: clean clean: rm -rf `find . -name __pycache__` diff --git a/benchmarks/profile.py b/benchmarks/profile.py deleted file mode 100644 index 53e4ab43eec..00000000000 --- a/benchmarks/profile.py +++ /dev/null @@ -1,39 +0,0 @@ -import json - -from line_profiler import LineProfiler - -import pydantic.datetime_parse -import pydantic.validators -from pydantic import validate_model -from pydantic.fields import ModelField -from test_pydantic import TestPydantic - -with open('./benchmarks/cases.json') as f: - cases = json.load(f) - - -def run(): - count, pass_count = 0, 0 - test = TestPydantic(False) - for case in cases: - passed, result = test.validate(case) - count += 1 - pass_count += passed - print('success percentage:', pass_count / count * 100) - - -funcs_to_profile = [validate_model, ModelField.validate, ModelField._validate_singleton, ModelField._apply_validators] -module_objects = {**vars(pydantic.validators), **vars(pydantic.datetime_parse), **vars(ModelField)} -funcs_to_profile += [v for k, v in module_objects.items() if not k.startswith('_') and str(v).startswith('{lpad}} ({i+1:>{len(str(repeats))}}/{repeats}) time={time:0.3f}s, success={success:0.2f}%') - times.append(time) - print(f'{p:>{lpad}} best={min(times):0.3f}s, avg={mean(times):0.3f}s, stdev={stdev(times):0.3f}s') - model_count = 3 * len(cases) - avg = mean(times) / model_count * 1e6 - sd = stdev(times) / model_count * 1e6 - results.append(f'{p:>{lpad}} best={min(times) / model_count * 1e6:0.3f}μs/iter ' - f'avg={avg:0.3f}μs/iter stdev={sd:0.3f}μs/iter version={test_class.version}') - csv_results.append([p, test_class.version, avg]) - print() - - return results, csv_results - -def main(): - json_path = THIS_DIR / 'cases.json' - if not json_path.exists(): - print('generating test cases...') - cases = [generate_case() for _ in range(2000)] - with json_path.open('w') as f: - json.dump(cases, f, indent=2, sort_keys=True) - else: - with json_path.open() as f: - cases = json.load(f) - - tests = [TestPydantic] - if 'pydantic-only' not in sys.argv: - tests += active_other_tests - - repeats = int(os.getenv('BENCHMARK_REPEATS', '5')) - test_json = 'TEST_JSON' in os.environ - results, csv_results = run_tests(tests, cases, repeats, test_json) - - for r in results: - print(r) - - if 'SAVE' in os.environ: - save_md(csv_results) - - -def save_md(data): - headings = 'Package', 'Version', 'Relative Performance', 'Mean validation time' - rows = [headings, ['---' for _ in headings]] - - first_avg = None - for package, version, avg in sorted(data, key=itemgetter(2)): - if first_avg: - relative = f'{avg / first_avg:0.1f}x slower' - else: - relative = '' - first_avg = avg - rows.append([package, f'`{version}`', relative, f'{avg:0.1f}μs']) - - table = '\n'.join(' | '.join(row) for row in rows) - text = f"""\ -[//]: <> (Generated with benchmarks/run.py, DO NOT EDIT THIS FILE DIRECTLY, instead run `SAVE=1 python ./run.py`.) - -{table} -""" - (Path(__file__).parent / '..' / 'docs' / '.benchmarks_table.md').write_text(text) - - -def diff(): - json_path = THIS_DIR / 'cases.json' - with json_path.open() as f: - cases = json.load(f) - - allow_extra = True - pydantic = TestPydantic(allow_extra) - others = [t(allow_extra) for t in active_other_tests] - - for case in cases: - pydantic_passed, pydantic_result = pydantic.validate(case) - for other in others: - other_passed, other_result = other.validate(case) - if other_passed != pydantic_passed: - print(f'⨯ pydantic {pydantic_passed} != {other.package} {other_passed}') - debug(case, pydantic_result, other_result) - return - print('✓ data passes match for all packages') - - -if __name__ == '__main__': - if 'diff' in sys.argv: - diff() - else: - main() - - # if None in other_tests: - # print('not all libraries could be imported!') - # sys.exit(1) diff --git a/benchmarks/test_cattrs.py b/benchmarks/test_cattrs.py deleted file mode 100644 index 617c68dfa9f..00000000000 --- a/benchmarks/test_cattrs.py +++ /dev/null @@ -1,99 +0,0 @@ -from datetime import datetime -from typing import List, Optional - -import attr -import cattr -from dateutil.parser import parse - - -class TestCAttrs: - package = 'attrs + cattrs' - version = attr.__version__ - - def __init__(self, allow_extra): - # cf. https://github.com/Tinche/cattrs/issues/26 why at least structure_str is needed - def structure_str(s, _): - if not isinstance(s, str): - raise ValueError() - return s - - def structure_int(i, _): - if not isinstance(i, int): - raise ValueError() - return i - - class PositiveInt(int): - ... - - def structure_posint(i, x): - i = PositiveInt(i) - if not isinstance(i, PositiveInt): - raise ValueError() - if i <= 0: - raise ValueError() - return i - - cattr.register_structure_hook(datetime, lambda isostring, _: parse(isostring)) - cattr.register_structure_hook(str, structure_str) - cattr.register_structure_hook(int, structure_int) - cattr.register_structure_hook(PositiveInt, structure_posint) - - def str_len_val(max_len: int, min_len: int = 0, required: bool = False): - # validate the max len of a string and optionally its min len and whether None is - # an acceptable value - def _check_str_len(self, attribute, value): - if value is None: - if required: - raise ValueError("") - else: - return - if len(value) > max_len: - raise ValueError("") - if min_len and len(value) < min_len: - raise ValueError("") - - return _check_str_len - - def pos_int(self, attribute, value): - # Validate that value is a positive >0 integer; None is allowed - if value is None: - return - if value <= 0: - raise ValueError("") - - @attr.s(auto_attribs=True, frozen=True, kw_only=True) - class Skill: - subject: str - subject_id: int - category: str - qual_level: str - qual_level_id: int - qual_level_ranking: float = 0 - - @attr.s(auto_attribs=True, frozen=True, kw_only=True) - class Location: - latitude: float = None - longitude: float = None - - @attr.s(auto_attribs=True, frozen=True, kw_only=True) - class Model: - id: int - sort_index: float - client_name: str = attr.ib(validator=str_len_val(255)) - # client_email: EmailStr = None - client_phone: Optional[str] = attr.ib(default=None, validator=str_len_val(255)) - location: Optional[Location] = None - - contractor: Optional[PositiveInt] - upstream_http_referrer: Optional[str] = attr.ib(default=None, validator=str_len_val(1023)) - grecaptcha_response: str = attr.ib(validator=str_len_val(1000, 20, required=True)) - last_updated: Optional[datetime] = None - skills: List[Skill] = [] - - self.model = Model - - def validate(self, data): - try: - return True, cattr.structure(data, self.model) - except (ValueError, TypeError, KeyError) as e: - return False, str(e) diff --git a/benchmarks/test_cerberus.py b/benchmarks/test_cerberus.py deleted file mode 100644 index 966d069cce3..00000000000 --- a/benchmarks/test_cerberus.py +++ /dev/null @@ -1,49 +0,0 @@ -from cerberus import Validator, __version__ -from dateutil.parser import parse as datetime_parse - - -class TestCerberus: - package = 'cerberus' - version = str(__version__) - - def __init__(self, allow_extra): - schema = { - 'id': {'type': 'integer', 'required': True}, - 'client_name': {'type': 'string', 'maxlength': 255, 'required': True}, - 'sort_index': {'type': 'float', 'required': True}, - 'client_phone': {'type': 'string', 'maxlength': 255, 'nullable': True}, - 'location': { - 'type': 'dict', - 'schema': {'latitude': {'type': 'float'}, 'longitude': {'type': 'float'}}, - 'nullable': True, - }, - 'contractor': {'type': 'integer', 'min': 0, 'nullable': True, 'coerce': int}, - 'upstream_http_referrer': {'type': 'string', 'maxlength': 1023, 'nullable': True}, - 'grecaptcha_response': {'type': 'string', 'minlength': 20, 'maxlength': 1000, 'required': True}, - 'last_updated': {'type': 'datetime', 'nullable': True, 'coerce': datetime_parse}, - 'skills': { - 'type': 'list', - 'default': [], - 'schema': { - 'type': 'dict', - 'schema': { - 'subject': {'type': 'string', 'required': True}, - 'subject_id': {'type': 'integer', 'required': True}, - 'category': {'type': 'string', 'required': True}, - 'qual_level': {'type': 'string', 'required': True}, - 'qual_level_id': {'type': 'integer', 'required': True}, - 'qual_level_ranking': {'type': 'float', 'default': 0, 'required': True}, - }, - }, - }, - } - - self.v = Validator(schema) - self.v.allow_unknown = allow_extra - - def validate(self, data): - validated = self.v.validated(data) - if validated is None: - return False, self.v.errors - else: - return True, validated diff --git a/benchmarks/test_drf.py b/benchmarks/test_drf.py deleted file mode 100644 index 638a0dfaeea..00000000000 --- a/benchmarks/test_drf.py +++ /dev/null @@ -1,53 +0,0 @@ -import django -from django.conf import settings - -settings.configure( - INSTALLED_APPS=['django.contrib.auth', 'django.contrib.contenttypes'] -) -django.setup() - -from rest_framework import __version__, serializers - - -class TestDRF: - package = 'django-rest-framework' - version = __version__ - - def __init__(self, allow_extra): - class Model(serializers.Serializer): - id = serializers.IntegerField() - client_name = serializers.CharField(max_length=255, trim_whitespace=False) - sort_index = serializers.FloatField() - # client_email = serializers.EmailField(required=False, allow_null=True) - client_phone = serializers.CharField(max_length=255, trim_whitespace=False, required=False, allow_null=True) - - class Location(serializers.Serializer): - latitude = serializers.FloatField(required=False, allow_null=True) - longitude = serializers.FloatField(required=False, allow_null=True) - location = Location(required=False, allow_null=True) - - contractor = serializers.IntegerField(required=False, allow_null=True, min_value=0) - upstream_http_referrer = serializers.CharField( - max_length=1023, trim_whitespace=False, required=False, allow_null=True - ) - grecaptcha_response = serializers.CharField(min_length=20, max_length=1000, trim_whitespace=False) - last_updated = serializers.DateTimeField(required=False, allow_null=True) - - class Skill(serializers.Serializer): - subject = serializers.CharField() - subject_id = serializers.IntegerField() - category = serializers.CharField() - qual_level = serializers.CharField() - qual_level_id = serializers.IntegerField() - qual_level_ranking = serializers.FloatField(default=0) - skills = serializers.ListField(child=Skill()) - - self.allow_extra = allow_extra # unused - self.serializer = Model - - def validate(self, data): - s = self.serializer(data=data) - if s.is_valid(): - return True, dict(s.data) - else: - return False, dict(s.errors) diff --git a/benchmarks/test_marshmallow.py b/benchmarks/test_marshmallow.py deleted file mode 100644 index 8ebc0355c27..00000000000 --- a/benchmarks/test_marshmallow.py +++ /dev/null @@ -1,45 +0,0 @@ -from marshmallow import Schema, ValidationError, __version__, fields, validate - - -class TestMarshmallow: - package = 'marshmallow' - version = __version__ - - def __init__(self, allow_extra): - class LocationSchema(Schema): - latitude = fields.Float(allow_none=True) - longitude = fields.Float(allow_none=True) - - class SkillSchema(Schema): - subject = fields.Str(required=True) - subject_id = fields.Integer(required=True) - category = fields.Str(required=True) - qual_level = fields.Str(required=True) - qual_level_id = fields.Integer(required=True) - qual_level_ranking = fields.Float(default=0) - - class Model(Schema): - id = fields.Integer(required=True) - client_name = fields.Str(validate=validate.Length(max=255), required=True) - sort_index = fields.Float(required=True) - # client_email = fields.Email() - client_phone = fields.Str(validate=validate.Length(max=255), allow_none=True) - - location = fields.Nested(LocationSchema) - - contractor = fields.Integer(validate=validate.Range(min=0), allow_none=True) - upstream_http_referrer = fields.Str(validate=validate.Length(max=1023), allow_none=True) - grecaptcha_response = fields.Str(validate=validate.Length(min=20, max=1000), required=True) - last_updated = fields.DateTime(allow_none=True) - skills = fields.Nested(SkillSchema, many=True) - - self.allow_extra = allow_extra # unused - self.schema = Model() - - def validate(self, data): - try: - result = self.schema.load(data) - except ValidationError as e: - return False, e.normalized_messages() - else: - return True, result diff --git a/benchmarks/test_pydantic.py b/benchmarks/test_pydantic.py deleted file mode 100644 index ff99dceb69c..00000000000 --- a/benchmarks/test_pydantic.py +++ /dev/null @@ -1,52 +0,0 @@ -from datetime import datetime -from typing import List - -from pydantic import VERSION, BaseModel, Extra, PositiveInt, ValidationError, constr - - -class TestPydantic: - package = 'pydantic' - version = str(VERSION) - - def __init__(self, allow_extra): - class Model(BaseModel): - id: int - client_name: constr(max_length=255) - sort_index: float - # client_email: EmailStr = None - client_phone: constr(max_length=255) = None - - class Location(BaseModel): - latitude: float = None - longitude: float = None - - location: Location = None - - contractor: PositiveInt = None - upstream_http_referrer: constr(max_length=1023) = None - grecaptcha_response: constr(min_length=20, max_length=1000) - last_updated: datetime = None - - class Skill(BaseModel): - subject: str - subject_id: int - category: str - qual_level: str - qual_level_id: int - qual_level_ranking: float = 0 - - skills: List[Skill] = [] - - class Config: - extra = Extra.allow if allow_extra else Extra.forbid - - self.model = Model - - def validate(self, data): - try: - return True, self.model(**data) - except ValidationError as e: - return False, e.errors() - - def to_json(self, model): - return model.json() diff --git a/benchmarks/test_schematics.py b/benchmarks/test_schematics.py deleted file mode 100644 index 94af84637d0..00000000000 --- a/benchmarks/test_schematics.py +++ /dev/null @@ -1,50 +0,0 @@ -from schematics import __version__ -from schematics.exceptions import DataError, ValidationError -from schematics.models import Model as PModel -from schematics.types import IntType, StringType -from schematics.types.base import DateType, FloatType -from schematics.types.compound import ListType, ModelType - - -class TestSchematics: - package = 'schematics' - version = __version__ - - def __init__(self, allow_extra): - class Model(PModel): - id = IntType(required=True) - client_name = StringType(max_length=255, required=True) - sort_index = FloatType(required=True) - client_phone = StringType(max_length=255, default=None) - - class Location(PModel): - latitude = FloatType(default=None) - longitude = FloatType(default=None) - - location = ModelType(model_spec=Location, default=None) - - contractor = IntType(min_value=1, default=None) - upstream_http_referrer = StringType(max_length=1023, default=None) - grecaptcha_response = StringType(min_length=20, max_length=1000, required=True) - last_updated = DateType(formats='%Y-%m-%dT%H:%M:%S') - - class Skill(PModel): - subject = StringType(required=True) - subject_id = IntType(required=True) - category = StringType(required=True) - qual_level = StringType(required=True) - qual_level_id = IntType(required=True) - qual_level_ranking = FloatType(default=0, required=True) - - skills = ListType(ModelType(Skill), default=[]) - - self.model = Model - - def validate(self, data): - try: - obj = self.model(data) - return True, obj.validate() - except DataError as e: - return False, e - except ValidationError as e: - return False, e diff --git a/benchmarks/test_trafaret.py b/benchmarks/test_trafaret.py deleted file mode 100644 index 546c165d1f2..00000000000 --- a/benchmarks/test_trafaret.py +++ /dev/null @@ -1,46 +0,0 @@ -from dateutil.parser import parse -import trafaret as t - - -class TestTrafaret: - package = 'trafaret' - version = '.'.join(map(str, t.__VERSION__)) - - def __init__(self, allow_extra): - self.schema = t.Dict({ - 'id': t.Int(), - 'client_name': t.String(max_length=255), - 'sort_index': t.Float, - # t.Key('client_email', optional=True): t.Or(t.Null | t.Email()), - t.Key('client_phone', optional=True): t.Or(t.Null | t.String(max_length=255)), - - t.Key('location', optional=True): t.Or(t.Null | t.Dict({ - 'latitude': t.Or(t.Float | t.Null), - 'longitude': t.Or(t.Float | t.Null), - })), - - t.Key('contractor', optional=True): t.Or(t.Null | t.Int(gt=0)), - t.Key('upstream_http_referrer', optional=True): t.Or(t.Null | t.String(max_length=1023)), - t.Key('grecaptcha_response'): t.String(min_length=20, max_length=1000), - - t.Key('last_updated', optional=True): t.Or(t.Null | t.String >> parse), - - t.Key('skills', default=[]): t.List(t.Dict({ - 'subject': t.String, - 'subject_id': t.Int, - 'category': t.String, - 'qual_level': t.String, - 'qual_level_id': t.Int, - t.Key('qual_level_ranking', default=0): t.Float, - })), - }) - if allow_extra: - self.schema.allow_extra('*') - - def validate(self, data): - try: - return True, self.schema.check(data) - except t.DataError: - return False, None - except ValueError: - return False, None diff --git a/benchmarks/test_valideer.py b/benchmarks/test_valideer.py deleted file mode 100644 index 353122acec4..00000000000 --- a/benchmarks/test_valideer.py +++ /dev/null @@ -1,47 +0,0 @@ -import re -import subprocess - -import dateutil.parser -import valideer as V - -# valideer appears to provide no way of getting the installed version -p = subprocess.run(['pip', 'freeze'], stdout=subprocess.PIPE, encoding='utf8', check=True) -valideer_version = re.search(r'valideer==(.+)', p.stdout).group(1) - - -class TestValideer: - package = 'valideer' - version = valideer_version - - def __init__(self, allow_extra): - schema = { - '+id': int, - '+client_name': V.String(max_length=255), - '+sort_index': float, - 'client_phone': V.Nullable(V.String(max_length=255)), - 'location': {'latitude': float, 'longitude': float}, - 'contractor': V.Range(V.AdaptTo(int), min_value=1), - 'upstream_http_referrer': V.Nullable(V.String(max_length=1023)), - '+grecaptcha_response': V.String(min_length=20, max_length=1000), - 'last_updated': V.AdaptBy(dateutil.parser.parse), - 'skills': V.Nullable( - [ - { - '+subject': str, - '+subject_id': int, - '+category': str, - '+qual_level': str, - '+qual_level_id': int, - 'qual_level_ranking': V.Nullable(float, default=0), - } - ], - default=[], - ), - } - self.validator = V.parse(schema, additional_properties=allow_extra) - - def validate(self, data): - try: - return True, self.validator.validate(data) - except V.ValidationError as e: - return False, str(e) diff --git a/benchmarks/test_voluptuous.py b/benchmarks/test_voluptuous.py deleted file mode 100644 index 0ac46f5d653..00000000000 --- a/benchmarks/test_voluptuous.py +++ /dev/null @@ -1,51 +0,0 @@ -from dateutil.parser import parse as parse_datetime -import voluptuous as v -from voluptuous.humanize import humanize_error - - -class TestVoluptuous: - package = 'voluptuous' - version = v.__version__ - - def __init__(self, allow_extra): - self.schema = v.Schema( - { - v.Required('id'): int, - v.Required('client_name'): v.All(str, v.Length(max=255)), - v.Required('sort_index'): float, - # v.Optional('client_email'): v.Maybe(v.Email), - v.Optional('client_phone'): v.Maybe(v.All(str, v.Length(max=255))), - v.Optional('location'): v.Maybe( - v.Schema( - { - 'latitude': v.Maybe(float), - 'longitude': v.Maybe(float) - }, - required=True - ) - ), - v.Optional('contractor'): v.Maybe(v.All(v.Coerce(int), v.Range(min=1))), - v.Optional('upstream_http_referrer'): v.Maybe(v.All(str, v.Length(max=1023))), - v.Required('grecaptcha_response'): v.All(str, v.Length(min=20, max=1000)), - v.Optional('last_updated'): v.Maybe(parse_datetime), - v.Required('skills', default=[]): [ - v.Schema( - { - v.Required('subject'): str, - v.Required('subject_id'): int, - v.Required('category'): str, - v.Required('qual_level'): str, - v.Required('qual_level_id'): int, - v.Required('qual_level_ranking', default=0): float, - } - ) - ], - }, - extra=allow_extra, - ) - - def validate(self, data): - try: - return True, self.schema(data) - except v.MultipleInvalid as e: - return False, humanize_error(data, e) diff --git a/changes/3625-hswong3i.md b/changes/3625-hswong3i.md new file mode 100644 index 00000000000..a9365e5206d --- /dev/null +++ b/changes/3625-hswong3i.md @@ -0,0 +1 @@ +Add `read_text(encoding='utf-8')` for `setup.py` diff --git a/changes/3636-tommilligan.md b/changes/3636-tommilligan.md new file mode 100644 index 00000000000..ec10fce7ea4 --- /dev/null +++ b/changes/3636-tommilligan.md @@ -0,0 +1 @@ +fix: clarify that discriminated unions do not support singletons diff --git a/changes/3641-PrettyWood.md b/changes/3641-PrettyWood.md new file mode 100644 index 00000000000..d0338c66369 --- /dev/null +++ b/changes/3641-PrettyWood.md @@ -0,0 +1 @@ +`Config.copy_on_model_validation` does a deep copy and not a shallow one \ No newline at end of file diff --git a/changes/3675-uriyyo.md b/changes/3675-uriyyo.md new file mode 100644 index 00000000000..7a34d4d2064 --- /dev/null +++ b/changes/3675-uriyyo.md @@ -0,0 +1 @@ +Fix issue with self-referencing dataclass diff --git a/changes/3679-samuelcolvin.md b/changes/3679-samuelcolvin.md new file mode 100644 index 00000000000..ac589451f5c --- /dev/null +++ b/changes/3679-samuelcolvin.md @@ -0,0 +1 @@ +Allow self referencing `ClassVar`s in models but checking for class vars after forward refs are resolved. diff --git a/changes/3706-samuelcolvin.md b/changes/3706-samuelcolvin.md new file mode 100644 index 00000000000..3a22afee678 --- /dev/null +++ b/changes/3706-samuelcolvin.md @@ -0,0 +1 @@ +Prevent subclasses of bytes being converted to bytes diff --git a/changes/3806-garyd203.md b/changes/3806-garyd203.md new file mode 100644 index 00000000000..25b2217bb47 --- /dev/null +++ b/changes/3806-garyd203.md @@ -0,0 +1 @@ +Update documentation about lazy evaluation of sources for Settings (it's not actually done). diff --git a/changes/3819-himbeles.md b/changes/3819-himbeles.md new file mode 100644 index 00000000000..7845d7b0f93 --- /dev/null +++ b/changes/3819-himbeles.md @@ -0,0 +1 @@ +Fix nested Python dataclass schema regression in version 1.9 diff --git a/changes/3972-samuelcolvin.md b/changes/3972-samuelcolvin.md new file mode 100644 index 00000000000..42f24982775 --- /dev/null +++ b/changes/3972-samuelcolvin.md @@ -0,0 +1 @@ +Typing checking with pyright in CI, improve docs on vscode/pylance/pyright. diff --git a/changes/3973-samuelcolvin.md b/changes/3973-samuelcolvin.md new file mode 100644 index 00000000000..a6838a23e1e --- /dev/null +++ b/changes/3973-samuelcolvin.md @@ -0,0 +1 @@ +Remove benchmarks from codebase and docs. diff --git a/changes/4067-adriangb.md b/changes/4067-adriangb.md new file mode 100644 index 00000000000..4c6689ebe5e --- /dev/null +++ b/changes/4067-adriangb.md @@ -0,0 +1 @@ +Fix in-place modification of `FieldInfo` that caused problems with PEP 593 type aliases diff --git a/docs/benchmarks.md b/docs/benchmarks.md deleted file mode 100644 index 94364bf3470..00000000000 --- a/docs/benchmarks.md +++ /dev/null @@ -1,8 +0,0 @@ -Below are the results of crude benchmarks comparing *pydantic* to other validation libraries. - -{!.benchmarks_table.md!} - -See [the benchmarks code](https://github.com/samuelcolvin/pydantic/tree/master/benchmarks) -for more details on the test case. Feel free to suggest more packages to benchmark or improve an existing one. - -Benchmarks were run with Python 3.8.6 and the package versions listed above installed via pypi on macOS Big Sur. diff --git a/docs/extra/redirects.js b/docs/extra/redirects.js index d8aec3e981e..b6b1a3c0f55 100644 --- a/docs/extra/redirects.js +++ b/docs/extra/redirects.js @@ -82,8 +82,6 @@ const lookup = { 'id7': '/usage/postponed_annotations/', 'id8': '/usage/postponed_annotations/', 'usage-of-union-in-annotations-and-type-order': '/usage/types/#unions', - 'benchmarks': '/benchmarks/', - 'benchmarks-tag': '/benchmarks/', 'contributing-to-pydantic': '/contributing/', 'pycharm-plugin': '/pycharm_plugin/', 'id9': '/pycharm_plugin/', diff --git a/docs/index.md b/docs/index.md index 772a6ea9993..ba4366dd114 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,7 +56,8 @@ So *pydantic* uses some cool new language features, but why should I actually go be read from environment variables, and more complex objects like DSNs and python objects are often required. **fast** -: In [benchmarks](benchmarks.md) *pydantic* is faster than all other tested libraries. +: *pydantic* has always taken performance seriously, most of the library is compiled with cython giving a ~50% speedup, + it's generally as fast or faster than most similar libraries. **validate complex structures** : use of [recursive *pydantic* models](usage/models.md#recursive-models), `typing`'s @@ -114,6 +115,11 @@ Hundreds of organisations and packages are using *pydantic*, including: : trusts *pydantic* (via FastAPI) and [*arq*](https://github.com/samuelcolvin/arq) (Samuel's excellent asynchronous task queue) to reliably power multiple mission-critical microservices. +[Robusta.dev](https://robusta.dev/) +: are using *pydantic* to automate Kubernetes troubleshooting and maintenance. For example, their open source + [tools to debug and profile Python applications on Kubernetes](https://home.robusta.dev/python/) use + *pydantic* models. + For a more comprehensive list of open-source projects using *pydantic* see the [list of dependents on github](https://github.com/samuelcolvin/pydantic/network/dependents). diff --git a/docs/requirements.txt b/docs/requirements.txt index 88f06ea3f75..348505f4d92 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,12 +1,12 @@ ansi2html==1.6.0 flake8==4.0.1 flake8-quotes==3.3.1 -hypothesis==6.31.6 +hypothesis==6.46.3 markdown-include==0.6.0 mdx-truly-sane-lists==1.2 -mkdocs==1.2.3 +mkdocs==1.3.0 mkdocs-exclude==1.0.2 -mkdocs-material==8.1.3 +mkdocs-material==8.2.14 sqlalchemy orjson ujson diff --git a/docs/usage/settings.md b/docs/usage/settings.md index 1de49c0a583..6bb752a1ea3 100644 --- a/docs/usage/settings.md +++ b/docs/usage/settings.md @@ -71,7 +71,7 @@ by treating the environment variable's value as a JSON-encoded string. Another way to populate nested complex variables is to configure your model with the `env_nested_delimiter` config setting, then use an env variable with a name pointing to the nested module fields. -What it does is simply explodes yor variable into nested models or dicts. +What it does is simply explodes your variable into nested models or dicts. So if you define a variable `FOO__BAR__BAZ=123` it will convert it into `FOO={'BAR': {'BAZ': 123}}` If you have multiple variables with the same structure they will be merged. @@ -278,6 +278,3 @@ You might also want to disable a source: {!.tmp_examples/settings_disable_source.py!} ``` _(This script is complete, it should run "as is", here you might need to set the `my_api_key` environment variable)_ - -Because of the callables approach of `customise_sources`, evaluation of sources is lazy so unused sources don't -have an adverse effect on performance. diff --git a/docs/usage/types.md b/docs/usage/types.md index 3fcd61d318a..86a437ba6e1 100644 --- a/docs/usage/types.md +++ b/docs/usage/types.md @@ -297,6 +297,12 @@ _(This script is complete, it should run "as is")_ Using the [Annotated Fields syntax](../schema/#typingannotated-fields) can be handy to regroup the `Union` and `discriminator` information. See below for an example! +!!! warning + Discriminated unions cannot be used with only a single variant, such as `Union[Cat]`. + + Python changes `Union[T]` into `T` at interpretation time, so it is not possible for `pydantic` to + distinguish fields of `Union[T]` from `T`. + #### Nested Discriminated Unions Only one discriminator can be set for a field but sometimes you want to combine multiple discriminators. diff --git a/docs/usage/validation_decorator.md b/docs/usage/validation_decorator.md index 914b9319865..74fcc8aa311 100644 --- a/docs/usage/validation_decorator.md +++ b/docs/usage/validation_decorator.md @@ -149,7 +149,7 @@ to use this, it may even become the default for the decorator. ### Performance -We've made a big effort to make *pydantic* as performant as possible (see [the benchmarks](../benchmarks.md)) +We've made a big effort to make *pydantic* as performant as possible and argument inspect and model creation is only performed once when the function is defined, however there will still be a performance impact to using the `validate_arguments` decorator compared to calling the raw function. diff --git a/docs/visual_studio_code.md b/docs/visual_studio_code.md index 98c58fb01f4..6439a5d3dfe 100644 --- a/docs/visual_studio_code.md +++ b/docs/visual_studio_code.md @@ -130,10 +130,18 @@ Below are several techniques to achieve it. You can disable the errors for a specific line using a comment of: -``` +```py # type: ignore ``` +or (to be specific to pylance/pyright): + +```py +# pyright: ignore +``` + +([pyright](https://github.com/microsoft/pyright) is the language server used by Pylance.). + coming back to the example with `age='23'`, it would be: ```Python hl_lines="10" @@ -146,7 +154,7 @@ class Knight(BaseModel): color: str = 'blue' -lancelot = Knight(title='Sir Lancelot', age='23') # type: ignore +lancelot = Knight(title='Sir Lancelot', age='23') # pyright: ignore ``` that way Pylance and mypy will ignore errors in that line. @@ -243,10 +251,44 @@ The specific configuration **`frozen`** (in beta) has a special meaning. It prevents other code from changing a model instance once it's created, keeping it **"frozen"**. -When using the second version to declare `frozen=True` (with **keyword arguments** in the class definition), Pylance can use it to help you check in your code and **detect errors** when something is trying to set values in a model that is "frozen". +When using the second version to declare `frozen=True` (with **keyword arguments** in the class definition), +Pylance can use it to help you check in your code and **detect errors** when something is trying to set values +in a model that is "frozen". ![VS Code strict type errors with model](./img/vs_code_08.png) +## BaseSettings and ignoring Pylance/pyright errors + +Pylance/pyright does not work well with [`BaseSettings`](./usage/settings.md) - fields in settings classes can be +configured via environment variables and therefore "required" fields do not have to be explicitly set when +initialising a settings instance. However, pyright considers these fields as "required" and will therefore +show an error when they're not set. + +See [#3753](https://github.com/samuelcolvin/pydantic/issues/3753#issuecomment-1087417884) for an explanation of the +reasons behind this, and why we can't avoid the problem. + +There are two potential workarounds: + +* use an ignore comment (`# pylance: ignore`) when initialising `settings` +* or, use `settings.parse_obj({})` to avoid the warning + +## Adding a default with `Field` + +Pylance/pyright requires `default` to be a keyword argument to `Field` in order to infer that the field is optional. + +```py +from pydantic import BaseModel, Field + + +class Knight(BaseModel): + title: str = Field(default='Sir Lancelot') # this is okay + age: int = Field(23) # this works fine at runtime but will case an error for pyright + +lance = Knight() # error: Argument missing for parameter "age" +``` + +Like the issue with `BaseSettings`, this is a limitation of dataclass transforms and cannot be fixed in pydantic. + ## Technical Details !!! warning diff --git a/mkdocs.yml b/mkdocs.yml index ff51c5b7a68..efdbbc20680 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,7 +56,6 @@ nav: - 'Usage with mypy': usage/mypy.md - 'Usage with devtools': usage/devtools.md - Contributing to pydantic: contributing.md -- benchmarks.md - 'Mypy plugin': mypy_plugin.md - 'PyCharm plugin': pycharm_plugin.md - 'Visual Studio Code': visual_studio_code.md diff --git a/pydantic/_hypothesis_plugin.py b/pydantic/_hypothesis_plugin.py index 79d787e9c63..890e192ccaf 100644 --- a/pydantic/_hypothesis_plugin.py +++ b/pydantic/_hypothesis_plugin.py @@ -358,7 +358,7 @@ def resolve_constr(cls): # type: ignore[no-untyped-def] # pragma: no cover # Finally, register all previously-defined types, and patch in our new function -for typ in pydantic.types._DEFINED_TYPES: +for typ in list(pydantic.types._DEFINED_TYPES): _registered(typ) pydantic.types._registered = _registered st.register_type_strategy(pydantic.Json, resolve_json) diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py index 12d4c588a58..ac8fd6d89cb 100644 --- a/pydantic/dataclasses.py +++ b/pydantic/dataclasses.py @@ -184,7 +184,12 @@ def _process_class( validators = gather_all_validators(cls) cls.__pydantic_model__ = create_model( - cls.__name__, __config__=config, __module__=_cls.__module__, __validators__=validators, **field_definitions + cls.__name__, + __config__=config, + __module__=_cls.__module__, + __validators__=validators, + __cls_kwargs__={'__resolve_forward_refs__': False}, + **field_definitions, ) cls.__initialised__ = False @@ -196,6 +201,8 @@ def _process_class( if cls.__pydantic_model__.__config__.validate_assignment and not frozen: cls.__setattr__ = setattr_validate_assignment # type: ignore[assignment] + cls.__pydantic_model__.__try_update_forward_refs__(**{cls.__name__: cls}) + return cls diff --git a/pydantic/fields.py b/pydantic/fields.py index 495a2ae04a4..3d8a0e1816a 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -1,3 +1,4 @@ +import copy from collections import Counter as CollectionCounter, defaultdict, deque from collections.abc import Hashable as CollectionsHashable, Iterable as CollectionsIterable from typing import ( @@ -38,6 +39,7 @@ display_as_type, get_args, get_origin, + is_classvar, is_literal_type, is_new_type, is_none_type, @@ -447,6 +449,7 @@ def _get_field_info( raise ValueError(f'cannot specify multiple `Annotated` `Field`s for {field_name!r}') field_info = next(iter(field_infos), None) if field_info is not None: + field_info = copy.copy(field_info) field_info.update_from_config(field_info_from_config) if field_info.default is not Undefined: raise ValueError(f'`Field` default cannot be set in `Annotated` for {field_name!r}') @@ -601,7 +604,7 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) return if self.discriminator_key is not None and not is_union(origin): - raise TypeError('`discriminator` can only be used with `Union` type') + raise TypeError('`discriminator` can only be used with `Union` type with more than one variant') # add extra check for `collections.abc.Hashable` for python 3.10+ where origin is not `None` if origin is None or origin is CollectionsHashable: @@ -612,6 +615,8 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) return elif origin is Callable: return + elif is_classvar(origin): + return elif is_union(origin): types_ = [] for type_ in get_args(self.type_): diff --git a/pydantic/generics.py b/pydantic/generics.py index a712d26f2fc..baad72cbf73 100644 --- a/pydantic/generics.py +++ b/pydantic/generics.py @@ -98,6 +98,7 @@ def __class_getitem__(cls: Type[GenericModelT], params: Union[Type[Any], Tuple[T __base__=(cls,) + tuple(cls.__parameterized_bases__(typevars_map)), __config__=None, __validators__=validators, + __cls_kwargs__=None, **fields, ), ) diff --git a/pydantic/json.py b/pydantic/json.py index b732cc0a2a7..0769228e416 100644 --- a/pydantic/json.py +++ b/pydantic/json.py @@ -26,7 +26,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: This is useful when we use ConstrainedDecimal to represent Numeric(x,0) where a integer (but not int typed) is used. Encoding this as a float - results in failed round-tripping between encode and prase. + results in failed round-tripping between encode and parse. Our Id type is a prime example of this. >>> decimal_encoder(Decimal("1.0")) diff --git a/pydantic/main.py b/pydantic/main.py index eea8abcbbe8..7afcafc4978 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -154,6 +154,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901 class_vars.update(base.__class_vars__) hash_func = base.__hash__ + resolve_forward_refs = kwargs.pop('__resolve_forward_refs__', True) allowed_config_kwargs: SetStr = { key for key in dir(config) @@ -289,7 +290,8 @@ def is_untouched(v: Any) -> bool: cls = super().__new__(mcs, name, bases, new_namespace, **kwargs) # set __signature__ attr only for model class, but not for its instances cls.__signature__ = ClassAttribute('__signature__', generate_model_signature(cls.__init__, fields, config)) - cls.__try_update_forward_refs__() + if resolve_forward_refs: + cls.__try_update_forward_refs__() return cls @@ -666,7 +668,7 @@ def __get_validators__(cls) -> 'CallableGenerator': def validate(cls: Type['Model'], value: Any) -> 'Model': if isinstance(value, cls): if cls.__config__.copy_on_model_validation: - return value._copy_and_set_values(value.__dict__, value.__fields_set__, deep=False) + return value._copy_and_set_values(value.__dict__, value.__fields_set__, deep=True) else: return value @@ -765,12 +767,12 @@ def _get_value( return v @classmethod - def __try_update_forward_refs__(cls) -> None: + def __try_update_forward_refs__(cls, **localns: Any) -> None: """ Same as update_forward_refs but will not raise exception when forward references are not defined. """ - update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, {}, (NameError,)) + update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, localns, (NameError,)) @classmethod def update_forward_refs(cls, **localns: Any) -> None: @@ -892,6 +894,7 @@ def create_model( __base__: None = None, __module__: str = __name__, __validators__: Dict[str, 'AnyClassMethod'] = None, + __cls_kwargs__: Dict[str, Any] = None, **field_definitions: Any, ) -> Type['BaseModel']: ... @@ -905,6 +908,7 @@ def create_model( __base__: Union[Type['Model'], Tuple[Type['Model'], ...]], __module__: str = __name__, __validators__: Dict[str, 'AnyClassMethod'] = None, + __cls_kwargs__: Dict[str, Any] = None, **field_definitions: Any, ) -> Type['Model']: ... @@ -917,6 +921,7 @@ def create_model( __base__: Union[None, Type['Model'], Tuple[Type['Model'], ...]] = None, __module__: str = __name__, __validators__: Dict[str, 'AnyClassMethod'] = None, + __cls_kwargs__: Dict[str, Any] = None, **field_definitions: Any, ) -> Type['Model']: """ @@ -926,6 +931,7 @@ def create_model( :param __base__: base class for the new model to inherit from :param __module__: module of the created model :param __validators__: a dict of method names and @validator class methods + :param __cls_kwargs__: a dict for class creation :param field_definitions: fields of the model (or extra fields if a base is supplied) in the format `=(, )` or `=, e.g. `foobar=(str, ...)` or `foobar=123`, or, for complex use-cases, in the format @@ -940,6 +946,8 @@ def create_model( else: __base__ = (cast(Type['Model'], BaseModel),) + __cls_kwargs__ = __cls_kwargs__ or {} + fields = {} annotations = {} @@ -969,7 +977,7 @@ def create_model( if __config__: namespace['Config'] = inherit_config(__config__, BaseConfig) - return type(__model_name, __base__, namespace) + return type(__model_name, __base__, namespace, **__cls_kwargs__) _missing = object() diff --git a/pydantic/networks.py b/pydantic/networks.py index 18a042ca979..cbc9e21f4c5 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -122,7 +122,7 @@ def int_domain_regex() -> Pattern[str]: class AnyUrl(str): strip_whitespace = True min_length = 1 - max_length = 2 ** 16 + max_length = 2**16 allowed_schemes: Optional[Collection[str]] = None tld_required: bool = False user_required: bool = False @@ -265,7 +265,7 @@ def validate_parts(cls, parts: 'Parts') -> 'Parts': def validate_host(cls, parts: 'Parts') -> Tuple[str, Optional[str], str, bool]: host, tld, host_type, rebuild = None, None, None, False for f in ('domain', 'ipv4', 'ipv6'): - host = parts[f] # type: ignore[misc] + host = parts[f] # type: ignore[literal-required] if host: host_type = f break @@ -310,8 +310,8 @@ def get_default_parts(parts: 'Parts') -> 'Parts': @classmethod def apply_default_parts(cls, parts: 'Parts') -> 'Parts': for key, value in cls.get_default_parts(parts).items(): - if not parts[key]: # type: ignore[misc] - parts[key] = value # type: ignore[misc] + if not parts[key]: # type: ignore[literal-required] + parts[key] = value # type: ignore[literal-required] return parts def __repr__(self) -> str: @@ -386,7 +386,7 @@ def stricturl( *, strip_whitespace: bool = True, min_length: int = 1, - max_length: int = 2 ** 16, + max_length: int = 2**16, tld_required: bool = True, host_required: bool = True, allowed_schemes: Optional[Collection[str]] = None, diff --git a/pydantic/schema.py b/pydantic/schema.py index e979678c229..b608d54b76b 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -419,10 +419,13 @@ def get_flat_models_from_field(field: ModelField, known_models: TypeModelSet) -> # Handle dataclass-based models if is_builtin_dataclass(field.type_): field.type_ = dataclass(field.type_) + was_dataclass = True + else: + was_dataclass = False field_type = field.type_ if lenient_issubclass(getattr(field_type, '__pydantic_model__', None), BaseModel): field_type = field_type.__pydantic_model__ - if field.sub_fields and not lenient_issubclass(field_type, BaseModel): + if field.sub_fields and (not lenient_issubclass(field_type, BaseModel) or was_dataclass): flat_models |= get_flat_models_from_fields(field.sub_fields, known_models=known_models) elif lenient_issubclass(field_type, BaseModel) and field_type not in known_models: flat_models |= get_flat_models_from_model(field_type, known_models=known_models) diff --git a/pydantic/types.py b/pydantic/types.py index e3d6c1277dd..2d0cc18f8d7 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -1010,18 +1010,18 @@ def _get_brand(card_number: str) -> PaymentCardBrand: BYTE_SIZES = { 'b': 1, - 'kb': 10 ** 3, - 'mb': 10 ** 6, - 'gb': 10 ** 9, - 'tb': 10 ** 12, - 'pb': 10 ** 15, - 'eb': 10 ** 18, - 'kib': 2 ** 10, - 'mib': 2 ** 20, - 'gib': 2 ** 30, - 'tib': 2 ** 40, - 'pib': 2 ** 50, - 'eib': 2 ** 60, + 'kb': 10**3, + 'mb': 10**6, + 'gb': 10**9, + 'tb': 10**12, + 'pb': 10**15, + 'eb': 10**18, + 'kib': 2**10, + 'mib': 2**20, + 'gib': 2**30, + 'tib': 2**40, + 'pib': 2**50, + 'eib': 2**60, } BYTE_SIZES.update({k.lower()[0]: v for k, v in BYTE_SIZES.items() if 'i' not in k}) byte_string_re = re.compile(r'^\s*(\d*\.?\d+)\s*(\w+)?', re.IGNORECASE) diff --git a/pydantic/validators.py b/pydantic/validators.py index 63b7a59e080..d4783d97b12 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -76,7 +76,7 @@ def strict_str_validator(v: Any) -> Union[str]: raise errors.StrError() -def bytes_validator(v: Any) -> bytes: +def bytes_validator(v: Any) -> Union[bytes]: if isinstance(v, bytes): return v elif isinstance(v, bytearray): diff --git a/requirements.txt b/requirements.txt index 95aef085232..d82a66c99a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # requirements for compilation and from setup.py so dependabot prompts us to test with latest version of these packages -Cython==0.29.26;sys_platform!='win32' +Cython==0.29.28;sys_platform!='win32' devtools==0.8.0 -email-validator==1.1.3 +email-validator==1.2.1 dataclasses==0.6; python_version < '3.7' -typing-extensions==4.0.1 -python-dotenv==0.19.2 +typing-extensions==4.2.0 +python-dotenv==0.20.0 diff --git a/setup.py b/setup.py index ae91cf9a0dd..c59056e0e56 100644 --- a/setup.py +++ b/setup.py @@ -59,12 +59,12 @@ def extra(self): description = 'Data validation and settings management using python type hints' THIS_DIR = Path(__file__).resolve().parent try: - history = (THIS_DIR / 'HISTORY.md').read_text() + history = (THIS_DIR / 'HISTORY.md').read_text(encoding='utf-8') history = re.sub(r'#(\d+)', r'[#\1](https://github.com/samuelcolvin/pydantic/issues/\1)', history) history = re.sub(r'( +)@([\w\-]+)', r'\1[@\2](https://github.com/\2)', history, flags=re.I) history = re.sub('@@', '@', history) - long_description = (THIS_DIR / 'README.md').read_text() + '\n\n' + history + long_description = (THIS_DIR / 'README.md').read_text(encoding='utf-8') + '\n\n' + history except FileNotFoundError: long_description = description + '.\n\nSee https://pydantic-docs.helpmanual.io/ for documentation.' diff --git a/tests/pyright/pyproject.toml b/tests/pyright/pyproject.toml new file mode 100644 index 00000000000..991559aeafd --- /dev/null +++ b/tests/pyright/pyproject.toml @@ -0,0 +1,4 @@ +[tool.pyright] +extraPaths = ['../..'] +reportUnnecessaryTypeIgnoreComment = true +pythonVersion = '3.10' diff --git a/tests/pyright/pyright_example.py b/tests/pyright/pyright_example.py new file mode 100644 index 00000000000..0819afc3c4b --- /dev/null +++ b/tests/pyright/pyright_example.py @@ -0,0 +1,38 @@ +""" +This file is used to test pyright's ability to check pydantic code. + +In particular pydantic provides the `@__dataclass_transform__` for `BaseModel` +and all subclasses (including `BaseSettings`), see #2721. +""" + +from typing import List + +from pydantic import BaseModel, BaseSettings, Field + + +class MyModel(BaseModel): + x: str + y: List[int] + + +m1 = MyModel(x='hello', y=[1, 2, 3]) + +m2 = MyModel(x='hello') # pyright: ignore + + +class Knight(BaseModel): + title: str = Field(default='Sir Lancelot') # this is okay + age: int = Field(23) # this works fine at runtime but will case an error for pyright + + +k = Knight() # pyright: ignore + + +class Settings(BaseSettings): + x: str + y: int + + +s1 = Settings.parse_obj({}) + +s2 = Settings() # pyright: ignore[reportGeneralTypeIssues] diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index 9a3f0464b33..4bfa013c456 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -1,10 +1,10 @@ -black==21.12b0 +black==22.3.0 flake8==4.0.1 flake8-quotes==3.3.1 -hypothesis==6.31.6 +hypothesis==6.46.3 isort==5.10.1 -mypy==0.930 -pre-commit==2.16.0 +mypy==0.942 +pre-commit==2.19.0 pycodestyle==2.8.0 pyflakes==2.4.0 -twine==3.7.1 +twine==4.0.0 diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index 1ee135251bb..ba68b3311c0 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -1,9 +1,9 @@ -coverage==6.2 -hypothesis==6.31.6 +coverage==6.3.2 +hypothesis==6.46.3 # pin importlib-metadata as upper versions need typing-extensions to work if on python < 3.8 importlib-metadata==3.1.0;python_version<"3.8" -mypy==0.930 -pytest==6.2.5 +mypy==0.942 +pytest==7.1.2 pytest-cov==3.0.0 -pytest-mock==3.6.1 +pytest-mock==3.7.0 pytest-sugar==0.9.4 diff --git a/tests/test_annotated.py b/tests/test_annotated.py index b5c27a8c52f..bf2cad4b882 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,3 +1,5 @@ +from typing import List + import pytest from typing_extensions import Annotated @@ -132,3 +134,21 @@ class Config: assert Foo.schema(by_alias=True)['properties'] == { 'a': {'title': 'A', 'description': 'descr', 'foobar': 'hello', 'type': 'integer'}, } + + +def test_annotated_alias() -> None: + # https://github.com/samuelcolvin/pydantic/issues/2971 + + StrAlias = Annotated[str, Field(max_length=3)] + IntAlias = Annotated[int, Field(default_factory=lambda: 2)] + + Nested = Annotated[List[StrAlias], Field(description='foo')] + + class MyModel(BaseModel): + a: StrAlias = 'abc' + b: StrAlias + c: IntAlias + d: IntAlias + e: Nested + + assert MyModel(b='def', e=['xyz']) == MyModel(a='abc', b='def', c=2, d=2, e=['xyz']) diff --git a/tests/test_construction.py b/tests/test_construction.py index 19e912b398e..e7b12a0a76d 100644 --- a/tests/test_construction.py +++ b/tests/test_construction.py @@ -63,8 +63,8 @@ class Model(BaseModel): a: bytes b: str - content_bytes = b'x' * (2 ** 16 + 1) - content_str = 'x' * (2 ** 16 + 1) + content_bytes = b'x' * (2**16 + 1) + content_str = 'x' * (2**16 + 1) m = Model(a=content_bytes, b=content_str) assert m.a == content_bytes assert m.b == content_str diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py index e99a9c72343..ef5968ce07f 100644 --- a/tests/test_dataclasses.py +++ b/tests/test_dataclasses.py @@ -989,3 +989,11 @@ def __new__(cls, *args, **kwargs): instance = cls(a=test_string) assert instance._special_property == 1 assert instance.a == test_string + + +def test_self_reference_dataclass(): + @pydantic.dataclasses.dataclass + class MyDataclass: + self_reference: 'MyDataclass' + + assert MyDataclass.__pydantic_model__.__fields__['self_reference'].type_ is MyDataclass diff --git a/tests/test_discrimated_union.py b/tests/test_discrimated_union.py index 120f9d2f16b..a4dd501bc97 100644 --- a/tests/test_discrimated_union.py +++ b/tests/test_discrimated_union.py @@ -11,12 +11,23 @@ def test_discriminated_union_only_union(): - with pytest.raises(TypeError, match='`discriminator` can only be used with `Union` type'): + with pytest.raises( + TypeError, match='`discriminator` can only be used with `Union` type with more than one variant' + ): class Model(BaseModel): x: str = Field(..., discriminator='qwe') +def test_discriminated_union_single_variant(): + with pytest.raises( + TypeError, match='`discriminator` can only be used with `Union` type with more than one variant' + ): + + class Model(BaseModel): + x: Union[str] = Field(..., discriminator='qwe') + + def test_discriminated_union_invalid_type(): with pytest.raises(TypeError, match="Type 'str' is not a valid `BaseModel` or `dataclass`"): diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index dd07eb3d37b..5da62257040 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -1906,3 +1906,29 @@ class Config: arbitrary_types_allowed = True assert Model().x == Foo() + + +def test_bytes_subclass(): + class MyModel(BaseModel): + my_bytes: bytes + + class BytesSubclass(bytes): + def __new__(cls, data: bytes): + self = bytes.__new__(cls, data) + return self + + m = MyModel(my_bytes=BytesSubclass(b'foobar')) + assert m.my_bytes.__class__ == BytesSubclass + + +def test_int_subclass(): + class MyModel(BaseModel): + my_int: int + + class IntSubclass(int): + def __new__(cls, data: int): + self = int.__new__(cls, data) + return self + + m = MyModel(my_int=IntSubclass(123)) + assert m.my_int.__class__ == IntSubclass diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index 32d82b0bfb3..436cbcc0ab0 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -703,3 +703,19 @@ class Hero(BaseModel): Team.update_forward_refs() module.Hero(name='Ivan', teams=[module.Team(name='TheBest', heroes=[])]) + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason='needs 3.9 or newer') +def test_class_var_forward_ref(create_module): + # see #3679 + create_module( + # language=Python + """ +from __future__ import annotations +from typing import ClassVar +from pydantic import BaseModel + +class WithClassVar(BaseModel): + Instances: ClassVar[dict[str, WithClassVar]] = {} +""" + ) diff --git a/tests/test_main.py b/tests/test_main.py index 642b5610b3a..f4d3844c4aa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -433,13 +433,13 @@ class Foo(BaseModel): x: int def __hash__(self): - return self.x ** 2 + return self.x**2 class Bar(Foo): y: int def __hash__(self): - return self.y ** 3 + return self.y**3 class Buz(Bar): z: int @@ -1561,12 +1561,28 @@ class Config: assert t.user is not my_user assert t.user.hobbies == ['scuba diving'] - assert t.user.hobbies is my_user.hobbies # `Config.copy_on_model_validation` only does a shallow copy + assert t.user.hobbies is not my_user.hobbies # `Config.copy_on_model_validation` does a deep copy assert t.user._priv == 13 assert t.user.password.get_secret_value() == 'hashedpassword' assert t.dict() == {'id': '1234567890', 'user': {'id': 42, 'hobbies': ['scuba diving']}} +def test_validation_deep_copy(): + """By default, Config.copy_on_model_validation should do a deep copy""" + + class A(BaseModel): + name: str + + class B(BaseModel): + list_a: List[A] + + a = A(name='a') + b = B(list_a=[a]) + assert b.list_a == [A(name='a')] + a.name = 'b' + assert b.list_a == [A(name='a')] + + @pytest.mark.parametrize( 'kinds', [ diff --git a/tests/test_networks_ipaddress.py b/tests/test_networks_ipaddress.py index cd1351b5928..c73c512307d 100644 --- a/tests/test_networks_ipaddress.py +++ b/tests/test_networks_ipaddress.py @@ -114,7 +114,7 @@ class Model(BaseModel): [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}], ), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}], ), ], @@ -141,7 +141,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}]), ( - 2 ** 32 + 1, + 2**32 + 1, [{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}], ), ( @@ -172,7 +172,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}], ), ( @@ -203,7 +203,7 @@ class Model(BaseModel): ('192.168.0.0/24', IPv4Network), ('192.168.128.0/30', IPv4Network), ('2001:db00::0/120', IPv6Network), - (2 ** 32 - 1, IPv4Network), # no mask equals to mask /32 + (2**32 - 1, IPv4Network), # no mask equals to mask /32 (20_282_409_603_651_670_423_947_251_286_015, IPv6Network), # /128 (b'\xff\xff\xff\xff', IPv4Network), # /32 (b'\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Network), @@ -224,7 +224,7 @@ class Model(BaseModel): [ ('192.168.0.0/24', IPv4Network), ('192.168.128.0/30', IPv4Network), - (2 ** 32 - 1, IPv4Network), # no mask equals to mask /32 + (2**32 - 1, IPv4Network), # no mask equals to mask /32 (b'\xff\xff\xff\xff', IPv4Network), # /32 (('192.168.0.0', 24), IPv4Network), (IPv4Network('192.168.0.0/24'), IPv4Network), @@ -270,7 +270,7 @@ class Model(BaseModel): [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}], ), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}], ), ], @@ -297,7 +297,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}], ), ( @@ -328,7 +328,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}], ), ( @@ -362,8 +362,8 @@ class Model(BaseModel): ('192.168.128.1/30', IPv4Interface), ('2001:db00::0/120', IPv6Interface), ('2001:db00::1/120', IPv6Interface), - (2 ** 32 - 1, IPv4Interface), # no mask equals to mask /32 - (2 ** 32 - 1, IPv4Interface), # so ``strict`` has no effect + (2**32 - 1, IPv4Interface), # no mask equals to mask /32 + (2**32 - 1, IPv4Interface), # so ``strict`` has no effect (20_282_409_603_651_670_423_947_251_286_015, IPv6Interface), # /128 (20_282_409_603_651_670_423_947_251_286_014, IPv6Interface), (b'\xff\xff\xff\xff', IPv4Interface), # /32 @@ -394,8 +394,8 @@ class Model(BaseModel): ('192.168.0.1/24', IPv4Interface), ('192.168.128.0/30', IPv4Interface), ('192.168.128.1/30', IPv4Interface), - (2 ** 32 - 1, IPv4Interface), # no mask equals to mask /32 - (2 ** 32 - 1, IPv4Interface), # so ``strict`` has no effect + (2**32 - 1, IPv4Interface), # no mask equals to mask /32 + (2**32 - 1, IPv4Interface), # so ``strict`` has no effect (b'\xff\xff\xff\xff', IPv4Interface), # /32 (b'\xff\xff\xff\xff', IPv4Interface), (('192.168.0.0', 24), IPv4Interface), @@ -467,7 +467,7 @@ class Model(BaseModel): ], ), ( - 2 ** 128 + 1, + 2**128 + 1, [ { 'loc': ('ip',), @@ -500,7 +500,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}], ), ], @@ -527,7 +527,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}], ), ], diff --git a/tests/test_schema.py b/tests/test_schema.py index ea0290f09e1..4d3d4a49970 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -799,7 +799,7 @@ class Model(BaseModel): @pytest.mark.parametrize( 'field_type,expected_schema', [ - (AnyUrl, {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2 ** 16}), + (AnyUrl, {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2**16}), ( stricturl(min_length=5, max_length=10), {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 5, 'maxLength': 10}, @@ -2884,3 +2884,34 @@ class Model(BaseModel): }, }, } + + +def test_nested_python_dataclasses(): + """ + Test schema generation for nested python dataclasses + """ + + from dataclasses import dataclass as python_dataclass + + @python_dataclass + class ChildModel: + name: str + + @python_dataclass + class NestedModel: + child: List[ChildModel] + + assert model_schema(dataclass(NestedModel)) == { + 'title': 'NestedModel', + 'type': 'object', + 'properties': {'child': {'title': 'Child', 'type': 'array', 'items': {'$ref': '#/definitions/ChildModel'}}}, + 'required': ['child'], + 'definitions': { + 'ChildModel': { + 'title': 'ChildModel', + 'type': 'object', + 'properties': {'name': {'title': 'Name', 'type': 'string'}}, + 'required': ['name'], + } + }, + } diff --git a/tests/test_types.py b/tests/test_types.py index a7b7db89f0a..8e5481719c2 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1144,7 +1144,7 @@ class Model(BaseModel): ([1, 2, '3'], [1, 2, '3']), ((1, 2, '3'), [1, 2, '3']), ({1, 2, '3'}, list({1, 2, '3'})), - ((i ** 2 for i in range(5)), [0, 1, 4, 9, 16]), + ((i**2 for i in range(5)), [0, 1, 4, 9, 16]), ((deque((1, 2, 3)), list(deque((1, 2, 3))))), ), ) @@ -1184,7 +1184,7 @@ class Model(BaseModel): ([1, 2, '3'], (1, 2, '3')), ((1, 2, '3'), (1, 2, '3')), ({1, 2, '3'}, tuple({1, 2, '3'})), - ((i ** 2 for i in range(5)), (0, 1, 4, 9, 16)), + ((i**2 for i in range(5)), (0, 1, 4, 9, 16)), (deque([1, 2, 3]), (1, 2, 3)), ), ) @@ -1210,7 +1210,7 @@ class Model(BaseModel): ( ([1, 2, '3'], int, (1, 2, 3)), ((1, 2, '3'), int, (1, 2, 3)), - ((i ** 2 for i in range(5)), int, (0, 1, 4, 9, 16)), + ((i**2 for i in range(5)), int, (0, 1, 4, 9, 16)), (('a', 'b', 'c'), str, ('a', 'b', 'c')), ), ) @@ -1250,7 +1250,7 @@ class Model(BaseModel): ({1, 2, 2, '3'}, {1, 2, '3'}), ((1, 2, 2, '3'), {1, 2, '3'}), ([1, 2, 2, '3'], {1, 2, '3'}), - ({i ** 2 for i in range(5)}, {0, 1, 4, 9, 16}), + ({i**2 for i in range(5)}, {0, 1, 4, 9, 16}), ), ) def test_set_success(value, result):