├── .cargo └── config.toml ├── .codecov.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── actions │ └── build-pgo-wheel │ │ └── action.yml ├── check_version.py ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codspeed.yml ├── .gitignore ├── .mypy-stubtest-allowlist ├── .pre-commit-config.yaml ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── benches └── main.rs ├── build.rs ├── package.json ├── pyproject.toml ├── python └── pydantic_core │ ├── __init__.py │ ├── _pydantic_core.pyi │ ├── core_schema.py │ └── py.typed ├── src ├── argument_markers.rs ├── build_tools.rs ├── common │ ├── mod.rs │ ├── prebuilt.rs │ └── union.rs ├── definitions.rs ├── errors │ ├── line_error.rs │ ├── location.rs │ ├── mod.rs │ ├── types.rs │ ├── validation_exception.rs │ └── value_exception.rs ├── input │ ├── datetime.rs │ ├── input_abstract.rs │ ├── input_json.rs │ ├── input_python.rs │ ├── input_string.rs │ ├── mod.rs │ ├── return_enums.rs │ └── shared.rs ├── lib.rs ├── lookup_key.rs ├── py_gc.rs ├── recursion_guard.rs ├── serializers │ ├── computed_fields.rs │ ├── config.rs │ ├── errors.rs │ ├── extra.rs │ ├── fields.rs │ ├── filter.rs │ ├── infer.rs │ ├── mod.rs │ ├── ob_type.rs │ ├── prebuilt.rs │ ├── ser.rs │ ├── shared.rs │ └── type_serializers │ │ ├── any.rs │ │ ├── bytes.rs │ │ ├── complex.rs │ │ ├── dataclass.rs │ │ ├── datetime_etc.rs │ │ ├── decimal.rs │ │ ├── definitions.rs │ │ ├── dict.rs │ │ ├── enum_.rs │ │ ├── float.rs │ │ ├── format.rs │ │ ├── function.rs │ │ ├── generator.rs │ │ ├── json.rs │ │ ├── json_or_python.rs │ │ ├── list.rs │ │ ├── literal.rs │ │ ├── mod.rs │ │ ├── model.rs │ │ ├── nullable.rs │ │ ├── other.rs │ │ ├── set_frozenset.rs │ │ ├── simple.rs │ │ ├── string.rs │ │ ├── timedelta.rs │ │ ├── tuple.rs │ │ ├── typed_dict.rs │ │ ├── union.rs │ │ ├── url.rs │ │ ├── uuid.rs │ │ └── with_default.rs ├── tools.rs ├── url.rs └── validators │ ├── any.rs │ ├── arguments.rs │ ├── arguments_v3.rs │ ├── bool.rs │ ├── bytes.rs │ ├── call.rs │ ├── callable.rs │ ├── chain.rs │ ├── complex.rs │ ├── config.rs │ ├── custom_error.rs │ ├── dataclass.rs │ ├── date.rs │ ├── datetime.rs │ ├── decimal.rs │ ├── definitions.rs │ ├── dict.rs │ ├── enum_.rs │ ├── float.rs │ ├── frozenset.rs │ ├── function.rs │ ├── generator.rs │ ├── int.rs │ ├── is_instance.rs │ ├── is_subclass.rs │ ├── json.rs │ ├── json_or_python.rs │ ├── lax_or_strict.rs │ ├── list.rs │ ├── literal.rs │ ├── mod.rs │ ├── model.rs │ ├── model_fields.rs │ ├── none.rs │ ├── nullable.rs │ ├── prebuilt.rs │ ├── set.rs │ ├── string.rs │ ├── time.rs │ ├── timedelta.rs │ ├── tuple.rs │ ├── typed_dict.rs │ ├── union.rs │ ├── url.rs │ ├── uuid.rs │ ├── validation_state.rs │ └── with_default.rs ├── tests ├── __init__.py ├── benchmarks │ ├── __init__.py │ ├── complete_schema.py │ ├── nested_schema.py │ ├── test_complete_benchmark.py │ ├── test_micro_benchmarks.py │ ├── test_nested_benchmark.py │ └── test_serialization_micro.py ├── conftest.py ├── emscripten_runner.js ├── serializers │ ├── __init__.py │ ├── test_any.py │ ├── test_bytes.py │ ├── test_complex.py │ ├── test_dataclasses.py │ ├── test_datetime.py │ ├── test_decimal.py │ ├── test_definitions.py │ ├── test_definitions_recursive.py │ ├── test_dict.py │ ├── test_enum.py │ ├── test_format.py │ ├── test_functions.py │ ├── test_generator.py │ ├── test_infer.py │ ├── test_json.py │ ├── test_json_or_python.py │ ├── test_list_tuple.py │ ├── test_literal.py │ ├── test_model.py │ ├── test_model_root.py │ ├── test_none.py │ ├── test_nullable.py │ ├── test_other.py │ ├── test_pickling.py │ ├── test_serialize_as_any.py │ ├── test_set_frozenset.py │ ├── test_simple.py │ ├── test_string.py │ ├── test_timedelta.py │ ├── test_typed_dict.py │ ├── test_union.py │ ├── test_url.py │ └── test_uuid.py ├── test.rs ├── test_build.py ├── test_config.py ├── test_custom_errors.py ├── test_docstrings.py ├── test_errors.py ├── test_garbage_collection.py ├── test_hypothesis.py ├── test_isinstance.py ├── test_json.py ├── test_misc.py ├── test_prebuilt.py ├── test_schema_functions.py ├── test_strict.py ├── test_typing.py ├── test_tzinfo.py ├── test_validate_strings.py ├── test_validation_context.py └── validators │ ├── __init__.py │ ├── arguments_v3 │ ├── __init__.py │ ├── test_alias.py │ ├── test_build_errors.py │ ├── test_extra.py │ ├── test_general.py │ ├── test_keyword_only.py │ ├── test_positional_only.py │ ├── test_positional_or_keyword.py │ ├── test_var_args.py │ ├── test_var_kwargs_uniform.py │ └── test_var_kwargs_unpacked_typed_dict.py │ ├── test_allow_partial.py │ ├── test_arguments.py │ ├── test_bool.py │ ├── test_bytes.py │ ├── test_call.py │ ├── test_callable.py │ ├── test_chain.py │ ├── test_complex.py │ ├── test_custom_error.py │ ├── test_dataclasses.py │ ├── test_date.py │ ├── test_datetime.py │ ├── test_decimal.py │ ├── test_definitions.py │ ├── test_definitions_recursive.py │ ├── test_dict.py │ ├── test_enums.py │ ├── test_float.py │ ├── test_frozenset.py │ ├── test_function.py │ ├── test_generator.py │ ├── test_int.py │ ├── test_is_instance.py │ ├── test_is_subclass.py │ ├── test_json.py │ ├── test_json_or_python.py │ ├── test_lax_or_strict.py │ ├── test_list.py │ ├── test_literal.py │ ├── test_model.py │ ├── test_model_fields.py │ ├── test_model_init.py │ ├── test_model_root.py │ ├── test_none.py │ ├── test_nullable.py │ ├── test_pickling.py │ ├── test_set.py │ ├── test_string.py │ ├── test_tagged_union.py │ ├── test_time.py │ ├── test_timedelta.py │ ├── test_tuple.py │ ├── test_typed_dict.py │ ├── test_union.py │ ├── test_url.py │ ├── test_uuid.py │ └── test_with_default.py ├── uv.lock └── wasm-preview ├── README.md ├── index.html ├── run_tests.py └── worker.js /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = [] 3 | 4 | # see https://pyo3.rs/main/building_and_distribution.html#macos 5 | [target.x86_64-apple-darwin] 6 | rustflags = [ 7 | "-C", "link-arg=-undefined", 8 | "-C", "link-arg=dynamic_lookup", 9 | ] 10 | 11 | [target.aarch64-apple-darwin] 12 | rustflags = [ 13 | "-C", "link-arg=-undefined", 14 | "-C", "link-arg=dynamic_lookup", 15 | ] 16 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: false 3 | 4 | coverage: 5 | precision: 2 6 | range: [90, 100] 7 | status: 8 | patch: false 9 | project: false 10 | 11 | comment: 12 | layout: 'header, diff, flags, files, footer' 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Change Summary 4 | 5 | 6 | 7 | ## Related issue number 8 | 9 | 10 | 11 | 12 | ## Checklist 13 | 14 | * [ ] Unit tests for the changes exist 15 | * [ ] Documentation reflects the changes where applicable 16 | * [ ] Pydantic tests pass with this `pydantic-core` (except for expected changes) 17 | * [ ] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers** 18 | -------------------------------------------------------------------------------- /.github/actions/build-pgo-wheel/action.yml: -------------------------------------------------------------------------------- 1 | name: Build PGO wheel 2 | description: Builds a PGO-optimized wheel 3 | inputs: 4 | interpreter: 5 | description: 'Interpreter to build the wheel for' 6 | required: true 7 | rust-toolchain: 8 | description: 'Rust toolchain to use' 9 | required: true 10 | outputs: 11 | wheel: 12 | description: 'Path to the built wheel' 13 | value: ${{ steps.find_wheel.outputs.path }} 14 | runs: 15 | using: "composite" 16 | steps: 17 | - name: prepare profiling directory 18 | shell: bash 19 | # making this ahead of the compile ensures that the local user can write to this 20 | # directory; the maturin action (on linux) runs in docker so would create as root 21 | run: mkdir -p ${{ github.workspace }}/profdata 22 | 23 | - name: build initial wheel 24 | uses: PyO3/maturin-action@v1 25 | with: 26 | manylinux: auto 27 | args: > 28 | --release 29 | --out pgo-wheel 30 | --interpreter ${{ inputs.interpreter }} 31 | rust-toolchain: ${{ inputs.rust-toolchain }} 32 | docker-options: -e CI 33 | env: 34 | RUSTFLAGS: '-Cprofile-generate=${{ github.workspace }}/profdata' 35 | 36 | - name: detect rust host 37 | run: echo RUST_HOST=$(rustc -Vv | grep host | cut -d ' ' -f 2) >> "$GITHUB_ENV" 38 | shell: bash 39 | 40 | - name: generate pgo data 41 | run: | 42 | uv sync --group testing 43 | uv pip install pydantic-core --no-index --no-deps --find-links pgo-wheel --force-reinstall 44 | uv run pytest tests/benchmarks 45 | RUST_HOST=$(rustc -Vv | grep host | cut -d ' ' -f 2) 46 | rustup run ${{ inputs.rust-toolchain }} bash -c 'echo LLVM_PROFDATA=$RUSTUP_HOME/toolchains/$RUSTUP_TOOLCHAIN/lib/rustlib/$RUST_HOST/bin/llvm-profdata >> "$GITHUB_ENV"' 47 | shell: bash 48 | 49 | - name: merge pgo data 50 | run: ${{ env.LLVM_PROFDATA }} merge -o ${{ github.workspace }}/merged.profdata ${{ github.workspace }}/profdata 51 | shell: pwsh # because it handles paths on windows better, and works well enough on unix for this step 52 | 53 | - name: build pgo-optimized wheel 54 | uses: PyO3/maturin-action@v1 55 | with: 56 | manylinux: auto 57 | args: > 58 | --release 59 | --out dist 60 | --interpreter ${{ inputs.interpreter }} 61 | rust-toolchain: ${{inputs.rust-toolchain}} 62 | docker-options: -e CI 63 | env: 64 | RUSTFLAGS: '-Cprofile-use=${{ github.workspace }}/merged.profdata' 65 | 66 | - name: find built wheel 67 | id: find_wheel 68 | run: echo "path=$(ls dist/*.whl)" | tee -a "$GITHUB_OUTPUT" 69 | shell: bash 70 | -------------------------------------------------------------------------------- /.github/check_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Check the version in Cargo.toml matches the version from `GITHUB_REF` environment variable. 4 | """ 5 | 6 | import os 7 | import re 8 | import sys 9 | from pathlib import Path 10 | 11 | 12 | def main() -> int: 13 | cargo_path = Path('Cargo.toml') 14 | if not cargo_path.is_file(): 15 | print(f'✖ path "{cargo_path}" does not exist') 16 | return 1 17 | 18 | version_ref = os.getenv('GITHUB_REF') 19 | if version_ref: 20 | version = re.sub('^refs/tags/v*', '', version_ref.lower()) 21 | else: 22 | print('✖ "GITHUB_REF" env variables not found') 23 | return 1 24 | 25 | # convert from python pre-release version to rust pre-release version 26 | # this is the reverse of what's done in lib.rs::_rust_notify 27 | version = version.replace('a', '-alpha').replace('b', '-beta') 28 | 29 | version_regex = re.compile(r"""^version ?= ?(["'])(.+)\1""", re.M) 30 | cargo_content = cargo_path.read_text() 31 | match = version_regex.search(cargo_content) 32 | if not match: 33 | print(f'✖ {version_regex!r} not found in {cargo_path}') 34 | return 1 35 | 36 | cargo_version = match.group(2) 37 | if cargo_version == version: 38 | print(f'✓ GITHUB_REF version matches {cargo_path} version "{cargo_version}"') 39 | return 0 40 | else: 41 | print(f'✖ GITHUB_REF version "{version}" does not match {cargo_path} version "{cargo_version}"') 42 | return 1 43 | 44 | 45 | if __name__ == '__main__': 46 | sys.exit(main()) 47 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | groups: 13 | python-packages: 14 | patterns: 15 | - "*" 16 | 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "monthly" 21 | -------------------------------------------------------------------------------- /.github/workflows/codspeed.yml: -------------------------------------------------------------------------------- 1 | name: codspeed 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | # `workflow_dispatch` allows CodSpeed to trigger backtest 9 | # performance analysis in order to generate initial data. 10 | workflow_dispatch: 11 | 12 | env: 13 | UV_FROZEN: true 14 | UV_PYTHON: 3.13 15 | 16 | jobs: 17 | benchmarks: 18 | runs-on: ubuntu-22.04 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | # Using this action is still necessary for CodSpeed to work: 24 | - uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ env.UV_PYTHON}} 27 | 28 | - name: install uv 29 | uses: astral-sh/setup-uv@v6 30 | with: 31 | enable-cache: true 32 | 33 | - name: Install deps 34 | run: | 35 | uv sync --group testing 36 | uv pip uninstall pytest-speed 37 | uv pip install pytest-benchmark==4.0.0 pytest-codspeed 38 | 39 | - name: Install rust stable 40 | id: rust-toolchain 41 | uses: dtolnay/rust-toolchain@stable 42 | with: 43 | components: llvm-tools 44 | 45 | - name: Cache rust 46 | uses: Swatinem/rust-cache@v2 47 | 48 | - name: Build PGO wheel 49 | id: pgo-wheel 50 | uses: ./.github/actions/build-pgo-wheel 51 | with: 52 | interpreter: ${{ env.UV_PYTHON }} 53 | rust-toolchain: ${{ steps.rust-toolchain.outputs.name }} 54 | env: 55 | # make sure profiling information is present 56 | CARGO_PROFILE_RELEASE_DEBUG: "line-tables-only" 57 | CARGO_PROFILE_RELEASE_STRIP: false 58 | 59 | - name: Install PGO wheel 60 | run: uv pip install ${{ steps.pgo-wheel.outputs.wheel }} --force-reinstall 61 | 62 | - name: Run CodSpeed benchmarks 63 | uses: CodSpeedHQ/action@v3 64 | with: 65 | run: uv run --group=codspeed pytest tests/benchmarks/ --codspeed 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.egg-info/ 3 | 4 | .coverage 5 | .python-version 6 | package-lock.json 7 | test.py 8 | 9 | .cache/ 10 | .hypothesis/ 11 | build/ 12 | dist/ 13 | docs/_build/ 14 | htmlcov/ 15 | node_modules/ 16 | 17 | /.benchmarks/ 18 | /.idea/ 19 | /.pytest_cache/ 20 | /.vscode/ 21 | /env*/ 22 | /env/ 23 | /flame/ 24 | /pytest-speed/ 25 | /sandbox/ 26 | /sandbox/ 27 | /site/ 28 | /target/ 29 | /worktree/ 30 | 31 | /.editorconfig 32 | /*.lcov 33 | /*.profdata 34 | /*.profraw 35 | /foobar.py 36 | /python/pydantic_core/*.so 37 | 38 | # samply 39 | /profile.json 40 | -------------------------------------------------------------------------------- /.mypy-stubtest-allowlist: -------------------------------------------------------------------------------- 1 | # TODO: don't want to expose this staticmethod, requires https://github.com/PyO3/pyo3/issues/2384 2 | pydantic_core._pydantic_core.PydanticUndefinedType.new 3 | # See #1540 for discussion 4 | pydantic_core._pydantic_core.from_json 5 | pydantic_core._pydantic_core.SchemaValidator.validate_python 6 | pydantic_core._pydantic_core.SchemaValidator.validate_json 7 | pydantic_core._pydantic_core.SchemaValidator.validate_strings 8 | # the `warnings` kwarg for SchemaSerializer functions has custom logic 9 | pydantic_core._pydantic_core.SchemaSerializer.to_python 10 | pydantic_core._pydantic_core.SchemaSerializer.to_json 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.0.1 6 | hooks: 7 | - id: check-yaml 8 | - id: check-toml 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | - id: check-added-large-files 12 | 13 | - repo: local 14 | hooks: 15 | - id: lint-python 16 | name: Lint Python 17 | entry: make lint-python 18 | types: [python] 19 | language: system 20 | pass_filenames: false 21 | - id: typecheck-python 22 | name: Typecheck Python 23 | entry: make pyright 24 | types: [python] 25 | language: system 26 | pass_filenames: false 27 | - id: lint-rust 28 | name: Lint Rust 29 | entry: make lint-rust 30 | types: [rust] 31 | language: system 32 | pass_filenames: false 33 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pydantic-core" 3 | version = "2.34.1" 4 | edition = "2021" 5 | license = "MIT" 6 | homepage = "https://github.com/pydantic/pydantic-core" 7 | repository = "https://github.com/pydantic/pydantic-core.git" 8 | readme = "README.md" 9 | include = [ 10 | "/pyproject.toml", 11 | "/README.md", 12 | "/LICENSE", 13 | "/Makefile", 14 | "/build.rs", 15 | "/rust-toolchain", 16 | "/src", 17 | "/python/pydantic_core", 18 | "/tests", 19 | "/.cargo", 20 | "!__pycache__", 21 | "!tests/.hypothesis", 22 | "!tests/.pytest_cache", 23 | "!*.so", 24 | ] 25 | rust-version = "1.75" 26 | 27 | [dependencies] 28 | # TODO it would be very nice to remove the "py-clone" feature as it can panic, 29 | # but needs a bit of work to make sure it's not used in the codebase 30 | pyo3 = { version = "0.25", features = ["generate-import-lib", "num-bigint", "py-clone"] } 31 | regex = "1.11.1" 32 | strum = { version = "0.26.3", features = ["derive"] } 33 | strum_macros = "0.26.4" 34 | serde_json = { version = "1.0.140", features = ["arbitrary_precision"] } 35 | enum_dispatch = "0.3.13" 36 | serde = { version = "1.0.219", features = ["derive"] } 37 | speedate = "0.15.0" 38 | smallvec = "1.15.0" 39 | ahash = "0.8.12" 40 | url = "2.5.4" 41 | # idna is already required by url, added here to be explicit 42 | idna = "1.0.3" 43 | base64 = "0.22.1" 44 | num-bigint = "0.4.6" 45 | num-traits = "0.2.19" 46 | uuid = "1.16.0" 47 | jiter = { version = "0.10.0", features = ["python"] } 48 | hex = "0.4.3" 49 | 50 | [lib] 51 | name = "_pydantic_core" 52 | crate-type = ["cdylib", "rlib"] 53 | 54 | [features] 55 | # must be enabled when building with `cargo build`, maturin enables this automatically 56 | extension-module = ["pyo3/extension-module"] 57 | 58 | [profile.release] 59 | lto = "fat" 60 | codegen-units = 1 61 | strip = true 62 | 63 | [profile.bench] 64 | debug = true 65 | strip = false 66 | 67 | # This is separate to benchmarks because `bench` ends up building testing 68 | # harnesses into code, as it's a special cargo profile. 69 | [profile.profiling] 70 | inherits = "release" 71 | debug = true 72 | strip = false 73 | 74 | [dev-dependencies] 75 | pyo3 = { version = "0.25", features = ["auto-initialize"] } 76 | 77 | [build-dependencies] 78 | version_check = "0.9.5" 79 | # used where logic has to be version/distribution specific, e.g. pypy 80 | pyo3-build-config = { version = "0.25" } 81 | 82 | [lints.clippy] 83 | dbg_macro = "warn" 84 | print_stdout = "warn" 85 | 86 | # in general we lint against the pedantic group, but we will whitelist 87 | # certain lints which we don't want to enforce (for now) 88 | pedantic = { level = "warn", priority = -1 } 89 | cast_possible_truncation = "allow" 90 | cast_possible_wrap = "allow" 91 | cast_precision_loss = "allow" 92 | cast_sign_loss = "allow" 93 | doc_markdown = "allow" 94 | float_cmp = "allow" 95 | fn_params_excessive_bools = "allow" 96 | if_not_else = "allow" 97 | manual_let_else = "allow" 98 | match_bool = "allow" 99 | match_same_arms = "allow" 100 | missing_errors_doc = "allow" 101 | missing_panics_doc = "allow" 102 | module_name_repetitions = "allow" 103 | must_use_candidate = "allow" 104 | needless_pass_by_value = "allow" 105 | similar_names = "allow" 106 | single_match_else = "allow" 107 | struct_excessive_bools = "allow" 108 | too_many_lines = "allow" 109 | unnecessary_wraps = "allow" 110 | unused_self = "allow" 111 | used_underscore_binding = "allow" 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Samuel Colvin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | pyo3_build_config::use_pyo3_cfgs(); 3 | if let Some(true) = version_check::supports_feature("coverage_attribute") { 4 | println!("cargo:rustc-cfg=has_coverage_attribute"); 5 | } 6 | println!("cargo:rustc-check-cfg=cfg(has_coverage_attribute)"); 7 | 8 | if std::env::var("RUSTFLAGS") 9 | .unwrap_or_default() 10 | .contains("-Cprofile-use=") 11 | { 12 | println!("cargo:rustc-cfg=specified_profile_use"); 13 | } 14 | println!("cargo:rustc-check-cfg=cfg(specified_profile_use)"); 15 | println!("cargo:rustc-env=PROFILE={}", std::env::var("PROFILE").unwrap()); 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pydantic-core", 3 | "version": "1.0.0", 4 | "description": "for running wasm tests.", 5 | "author": "Samuel Colvin", 6 | "license": "MIT", 7 | "homepage": "https://github.com/pydantic/pydantic-core#readme", 8 | "main": "tests/emscripten_runner.js", 9 | "dependencies": { 10 | "prettier": "^2.7.1", 11 | "pyodide": "^0.26.3" 12 | }, 13 | "scripts": { 14 | "test": "node tests/emscripten_runner.js", 15 | "format": "prettier --write 'tests/emscripten_runner.js' 'wasm-preview/*.{html,js}'", 16 | "lint": "prettier --check 'tests/emscripten_runner.js' 'wasm-preview/*.{html,js}'" 17 | }, 18 | "prettier": { 19 | "singleQuote": true, 20 | "trailingComma": "all", 21 | "tabWidth": 2, 22 | "printWidth": 119, 23 | "bracketSpacing": false, 24 | "arrowParens": "avoid" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /python/pydantic_core/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydantic/pydantic-core/52e9a5309580bf53b9d85e069f956dcfca84998f/python/pydantic_core/py.typed -------------------------------------------------------------------------------- /src/argument_markers.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyNotImplementedError; 2 | use pyo3::prelude::*; 3 | use pyo3::sync::GILOnceCell; 4 | use pyo3::types::{PyDict, PyTuple}; 5 | 6 | use crate::tools::safe_repr; 7 | 8 | #[pyclass(module = "pydantic_core._pydantic_core", get_all, frozen, freelist = 100)] 9 | #[derive(Debug, Clone)] 10 | pub struct ArgsKwargs { 11 | pub(crate) args: Py, 12 | pub(crate) kwargs: Option>, 13 | } 14 | 15 | #[pymethods] 16 | impl ArgsKwargs { 17 | #[new] 18 | #[pyo3(signature = (args, kwargs = None))] 19 | fn py_new(args: Bound<'_, PyTuple>, kwargs: Option>) -> Self { 20 | Self { 21 | args: args.unbind(), 22 | kwargs: kwargs.filter(|d| !d.is_empty()).map(Bound::unbind), 23 | } 24 | } 25 | 26 | fn __eq__(&self, py: Python, other: &Self) -> PyResult { 27 | if !self.args.bind(py).eq(&other.args)? { 28 | return Ok(false); 29 | } 30 | 31 | match (&self.kwargs, &other.kwargs) { 32 | (Some(d1), Some(d2)) => d1.bind(py).eq(d2), 33 | (None, None) => Ok(true), 34 | _ => Ok(false), 35 | } 36 | } 37 | 38 | pub fn __repr__(&self, py: Python) -> String { 39 | let args = safe_repr(self.args.bind(py)); 40 | match self.kwargs { 41 | Some(ref d) => format!("ArgsKwargs({args}, {})", safe_repr(d.bind(py))), 42 | None => format!("ArgsKwargs({args})"), 43 | } 44 | } 45 | } 46 | 47 | static UNDEFINED_CELL: GILOnceCell> = GILOnceCell::new(); 48 | 49 | #[pyclass(module = "pydantic_core._pydantic_core", frozen)] 50 | #[derive(Debug)] 51 | pub struct PydanticUndefinedType {} 52 | 53 | #[pymethods] 54 | impl PydanticUndefinedType { 55 | #[new] 56 | pub fn py_new(_py: Python) -> PyResult { 57 | Err(PyNotImplementedError::new_err( 58 | "Creating instances of \"UndefinedType\" is not supported", 59 | )) 60 | } 61 | 62 | #[staticmethod] 63 | pub fn new(py: Python) -> Py { 64 | UNDEFINED_CELL 65 | .get_or_init(py, || Py::new(py, PydanticUndefinedType {}).unwrap()) 66 | .clone_ref(py) 67 | } 68 | 69 | fn __repr__(&self) -> &'static str { 70 | "PydanticUndefined" 71 | } 72 | 73 | fn __copy__(&self, py: Python) -> Py { 74 | UNDEFINED_CELL.get(py).unwrap().clone_ref(py) 75 | } 76 | 77 | #[pyo3(signature = (_memo, /))] 78 | fn __deepcopy__(&self, py: Python, _memo: &Bound<'_, PyAny>) -> Py { 79 | self.__copy__(py) 80 | } 81 | 82 | fn __reduce__(&self) -> &'static str { 83 | "PydanticUndefined" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod prebuilt; 2 | pub(crate) mod union; 3 | -------------------------------------------------------------------------------- /src/common/prebuilt.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::{PyAny, PyDict, PyType}; 4 | 5 | use crate::tools::SchemaDict; 6 | 7 | pub fn get_prebuilt( 8 | type_: &str, 9 | schema: &Bound<'_, PyDict>, 10 | prebuilt_attr_name: &str, 11 | extractor: impl FnOnce(Bound<'_, PyAny>) -> PyResult>, 12 | ) -> PyResult> { 13 | let py = schema.py(); 14 | 15 | // we can only use prebuilt validators/serializers from models and Pydantic dataclasses. 16 | // However, we don't want to use a prebuilt structure from dataclasses if we have a `generic_origin` 17 | // as this means the dataclass was parametrized (so a generic alias instance), and `cls` in the 18 | // core schema is still the (unparametrized) class, meaning we would fetch the wrong validator/serializer. 19 | if !matches!(type_, "model" | "dataclass") 20 | || (type_ == "dataclass" && schema.contains(intern!(py, "generic_origin"))?) 21 | { 22 | return Ok(None); 23 | } 24 | 25 | let class: Bound<'_, PyType> = schema.get_as_req(intern!(py, "cls"))?; 26 | 27 | // Note: we NEED to use the __dict__ here (and perform get_item calls rather than getattr) 28 | // because we don't want to fetch prebuilt validators from parent classes. 29 | // We don't downcast here because __dict__ on a class is a readonly mappingproxy, 30 | // so we can just leave it as is and do get_item checks. 31 | let class_dict = class.getattr(intern!(py, "__dict__"))?; 32 | 33 | let is_complete: bool = class_dict 34 | .get_item(intern!(py, "__pydantic_complete__")) 35 | .is_ok_and(|b| b.extract().unwrap_or(false)); 36 | 37 | if !is_complete { 38 | return Ok(None); 39 | } 40 | 41 | // Retrieve the prebuilt validator / serializer if available 42 | let prebuilt: Bound<'_, PyAny> = class_dict.get_item(prebuilt_attr_name)?; 43 | extractor(prebuilt) 44 | } 45 | -------------------------------------------------------------------------------- /src/common/union.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::{PyTraverseError, PyVisit}; 3 | 4 | use crate::lookup_key::LookupKey; 5 | use crate::py_gc::PyGcTraverse; 6 | 7 | #[derive(Debug)] 8 | pub enum Discriminator { 9 | /// use `LookupKey` to find the tag, same as we do to find values in typed_dict aliases 10 | LookupKey(LookupKey), 11 | /// call a function to find the tag to use 12 | Function(PyObject), 13 | } 14 | 15 | impl Discriminator { 16 | pub fn new(py: Python, raw: &Bound<'_, PyAny>) -> PyResult { 17 | if raw.is_callable() { 18 | return Ok(Self::Function(raw.clone().unbind())); 19 | } 20 | 21 | let lookup_key = LookupKey::from_py(py, raw, None)?; 22 | Ok(Self::LookupKey(lookup_key)) 23 | } 24 | 25 | pub fn to_string_py(&self, py: Python) -> PyResult { 26 | match self { 27 | Self::Function(f) => Ok(format!("{}()", f.getattr(py, "__name__")?)), 28 | Self::LookupKey(lookup_key) => Ok(lookup_key.to_string()), 29 | } 30 | } 31 | } 32 | 33 | impl PyGcTraverse for Discriminator { 34 | fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { 35 | match self { 36 | Self::Function(obj) => visit.call(obj)?, 37 | Self::LookupKey(_) => {} 38 | } 39 | Ok(()) 40 | } 41 | } 42 | 43 | pub(crate) const SMALL_UNION_THRESHOLD: usize = 4; 44 | -------------------------------------------------------------------------------- /src/errors/mod.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | 3 | mod line_error; 4 | mod location; 5 | mod types; 6 | mod validation_exception; 7 | mod value_exception; 8 | 9 | pub use self::line_error::{InputValue, ToErrorValue, ValError, ValLineError, ValResult}; 10 | pub use self::location::{LocItem, Location}; 11 | pub use self::types::{list_all_errors, ErrorType, ErrorTypeDefaults, Number}; 12 | pub use self::validation_exception::{PyLineError, ValidationError}; 13 | pub use self::value_exception::{PydanticCustomError, PydanticKnownError, PydanticOmit, PydanticUseDefault}; 14 | 15 | pub fn py_err_string(py: Python, err: PyErr) -> String { 16 | let value = err.value(py); 17 | match value.get_type().qualname() { 18 | Ok(type_name) => match value.str() { 19 | Ok(py_str) => { 20 | let str_cow = py_str.to_string_lossy(); 21 | let str = str_cow.as_ref(); 22 | if !str.is_empty() { 23 | format!("{type_name}: {str}") 24 | } else { 25 | type_name.to_string() 26 | } 27 | } 28 | Err(_) => format!("{type_name}: "), 29 | }, 30 | Err(_) => "Unknown Error".to_string(), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/input/mod.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::c_int; 2 | 3 | use pyo3::prelude::*; 4 | 5 | mod datetime; 6 | mod input_abstract; 7 | mod input_json; 8 | mod input_python; 9 | mod input_string; 10 | mod return_enums; 11 | mod shared; 12 | 13 | pub use datetime::TzInfo; 14 | pub(crate) use datetime::{ 15 | duration_as_pytimedelta, pydate_as_date, pydatetime_as_datetime, pytime_as_time, EitherDate, EitherDateTime, 16 | EitherTimedelta, 17 | }; 18 | pub(crate) use input_abstract::{ 19 | Arguments, BorrowInput, ConsumeIterator, Input, InputType, KeywordArgs, PositionalArgs, ValidatedDict, 20 | ValidatedList, ValidatedSet, ValidatedTuple, 21 | }; 22 | pub(crate) use input_python::{downcast_python_input, input_as_python_instance}; 23 | pub(crate) use input_string::StringMapping; 24 | pub(crate) use return_enums::{ 25 | no_validator_iter_to_vec, py_string_str, validate_iter_to_set, validate_iter_to_vec, EitherBytes, EitherFloat, 26 | EitherInt, EitherString, GenericIterator, Int, MaxLengthCheck, ValidationMatch, 27 | }; 28 | 29 | // Defined here as it's not exported by pyo3 30 | pub fn py_error_on_minusone(py: Python<'_>, result: c_int) -> PyResult<()> { 31 | if result != -1 { 32 | Ok(()) 33 | } else { 34 | Err(PyErr::fetch(py)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/py_gc.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ahash::AHashMap; 4 | use enum_dispatch::enum_dispatch; 5 | use pyo3::{Py, PyTraverseError, PyVisit}; 6 | 7 | /// Trait implemented by types which can be traversed by the Python GC. 8 | #[enum_dispatch] 9 | pub trait PyGcTraverse { 10 | fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError>; 11 | } 12 | 13 | impl PyGcTraverse for Py { 14 | fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { 15 | visit.call(self) 16 | } 17 | } 18 | 19 | impl PyGcTraverse for Vec { 20 | fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { 21 | for item in self { 22 | item.py_gc_traverse(visit)?; 23 | } 24 | Ok(()) 25 | } 26 | } 27 | 28 | impl PyGcTraverse for AHashMap { 29 | fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { 30 | for item in self.values() { 31 | item.py_gc_traverse(visit)?; 32 | } 33 | Ok(()) 34 | } 35 | } 36 | 37 | impl PyGcTraverse for Arc { 38 | fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { 39 | T::py_gc_traverse(self, visit) 40 | } 41 | } 42 | 43 | impl PyGcTraverse for Box { 44 | fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { 45 | T::py_gc_traverse(self, visit) 46 | } 47 | } 48 | 49 | impl PyGcTraverse for Option { 50 | fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> { 51 | match self { 52 | Some(item) => T::py_gc_traverse(item, visit), 53 | None => Ok(()), 54 | } 55 | } 56 | } 57 | 58 | /// A crude alternative to a "derive" macro to help with building PyGcTraverse implementations 59 | macro_rules! impl_py_gc_traverse { 60 | ($name:ty { }) => { 61 | impl crate::py_gc::PyGcTraverse for $name { 62 | fn py_gc_traverse(&self, _visit: &pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { 63 | Ok(()) 64 | } 65 | } 66 | }; 67 | ($name:ty { $($fields:ident),* }) => { 68 | impl crate::py_gc::PyGcTraverse for $name { 69 | fn py_gc_traverse(&self, visit: &pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { 70 | $(self.$fields.py_gc_traverse(visit)?;)* 71 | Ok(()) 72 | } 73 | } 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/serializers/prebuilt.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::prelude::*; 4 | use pyo3::types::PyDict; 5 | 6 | use crate::common::prebuilt::get_prebuilt; 7 | use crate::SchemaSerializer; 8 | 9 | use super::extra::Extra; 10 | use super::shared::{CombinedSerializer, TypeSerializer}; 11 | 12 | #[derive(Debug)] 13 | pub struct PrebuiltSerializer { 14 | schema_serializer: Py, 15 | } 16 | 17 | impl PrebuiltSerializer { 18 | pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult> { 19 | get_prebuilt(type_, schema, "__pydantic_serializer__", |py_any| { 20 | let schema_serializer = py_any.extract::>()?; 21 | if matches!(schema_serializer.get().serializer, CombinedSerializer::FunctionWrap(_)) { 22 | return Ok(None); 23 | } 24 | Ok(Some(Self { schema_serializer }.into())) 25 | }) 26 | } 27 | } 28 | 29 | impl_py_gc_traverse!(PrebuiltSerializer { schema_serializer }); 30 | 31 | impl TypeSerializer for PrebuiltSerializer { 32 | fn to_python( 33 | &self, 34 | value: &Bound<'_, PyAny>, 35 | include: Option<&Bound<'_, PyAny>>, 36 | exclude: Option<&Bound<'_, PyAny>>, 37 | extra: &Extra, 38 | ) -> PyResult { 39 | self.schema_serializer 40 | .get() 41 | .serializer 42 | .to_python_no_infer(value, include, exclude, extra) 43 | } 44 | 45 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 46 | self.schema_serializer.get().serializer.json_key_no_infer(key, extra) 47 | } 48 | 49 | fn serde_serialize( 50 | &self, 51 | value: &Bound<'_, PyAny>, 52 | serializer: S, 53 | include: Option<&Bound<'_, PyAny>>, 54 | exclude: Option<&Bound<'_, PyAny>>, 55 | extra: &Extra, 56 | ) -> Result { 57 | self.schema_serializer 58 | .get() 59 | .serializer 60 | .serde_serialize_no_infer(value, serializer, include, exclude, extra) 61 | } 62 | 63 | fn get_name(&self) -> &str { 64 | self.schema_serializer.get().serializer.get_name() 65 | } 66 | 67 | fn retry_with_lax_check(&self) -> bool { 68 | self.schema_serializer.get().serializer.retry_with_lax_check() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/any.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | sync::{Arc, OnceLock}, 4 | }; 5 | 6 | use pyo3::prelude::*; 7 | use pyo3::types::PyDict; 8 | 9 | use serde::ser::Serializer; 10 | 11 | use crate::definitions::DefinitionsBuilder; 12 | 13 | use super::{ 14 | infer_json_key, infer_serialize, infer_to_python, BuildSerializer, CombinedSerializer, Extra, TypeSerializer, 15 | }; 16 | 17 | #[derive(Debug, Clone, Default)] 18 | pub struct AnySerializer; 19 | 20 | impl AnySerializer { 21 | pub fn get() -> &'static Arc { 22 | static ANY_SERIALIZER: OnceLock> = OnceLock::new(); 23 | ANY_SERIALIZER.get_or_init(|| Arc::new(Self.into())) 24 | } 25 | } 26 | 27 | impl BuildSerializer for AnySerializer { 28 | const EXPECTED_TYPE: &'static str = "any"; 29 | 30 | fn build( 31 | _schema: &Bound<'_, PyDict>, 32 | _config: Option<&Bound<'_, PyDict>>, 33 | _definitions: &mut DefinitionsBuilder, 34 | ) -> PyResult { 35 | Ok(Self {}.into()) 36 | } 37 | } 38 | 39 | impl_py_gc_traverse!(AnySerializer {}); 40 | 41 | impl TypeSerializer for AnySerializer { 42 | fn to_python( 43 | &self, 44 | value: &Bound<'_, PyAny>, 45 | include: Option<&Bound<'_, PyAny>>, 46 | exclude: Option<&Bound<'_, PyAny>>, 47 | extra: &Extra, 48 | ) -> PyResult { 49 | infer_to_python(value, include, exclude, extra) 50 | } 51 | 52 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 53 | infer_json_key(key, extra) 54 | } 55 | 56 | fn serde_serialize( 57 | &self, 58 | value: &Bound<'_, PyAny>, 59 | serializer: S, 60 | include: Option<&Bound<'_, PyAny>>, 61 | exclude: Option<&Bound<'_, PyAny>>, 62 | extra: &Extra, 63 | ) -> Result { 64 | infer_serialize(value, serializer, include, exclude, extra) 65 | } 66 | 67 | fn get_name(&self) -> &str { 68 | Self::EXPECTED_TYPE 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/bytes.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::types::{PyBytes, PyDict}; 4 | use pyo3::{prelude::*, IntoPyObjectExt}; 5 | 6 | use crate::definitions::DefinitionsBuilder; 7 | use crate::serializers::config::{BytesMode, FromConfig}; 8 | 9 | use super::{ 10 | infer_json_key, infer_serialize, infer_to_python, BuildSerializer, CombinedSerializer, Extra, SerMode, 11 | TypeSerializer, 12 | }; 13 | 14 | #[derive(Debug)] 15 | pub struct BytesSerializer { 16 | bytes_mode: BytesMode, 17 | } 18 | 19 | impl BuildSerializer for BytesSerializer { 20 | const EXPECTED_TYPE: &'static str = "bytes"; 21 | 22 | fn build( 23 | _schema: &Bound<'_, PyDict>, 24 | config: Option<&Bound<'_, PyDict>>, 25 | _definitions: &mut DefinitionsBuilder, 26 | ) -> PyResult { 27 | let bytes_mode = BytesMode::from_config(config)?; 28 | Ok(Self { bytes_mode }.into()) 29 | } 30 | } 31 | 32 | impl_py_gc_traverse!(BytesSerializer {}); 33 | 34 | impl TypeSerializer for BytesSerializer { 35 | fn to_python( 36 | &self, 37 | value: &Bound<'_, PyAny>, 38 | include: Option<&Bound<'_, PyAny>>, 39 | exclude: Option<&Bound<'_, PyAny>>, 40 | extra: &Extra, 41 | ) -> PyResult { 42 | let py = value.py(); 43 | match value.downcast::() { 44 | Ok(py_bytes) => match extra.mode { 45 | SerMode::Json => self 46 | .bytes_mode 47 | .bytes_to_string(py, py_bytes.as_bytes())? 48 | .into_py_any(py), 49 | _ => Ok(value.clone().unbind()), 50 | }, 51 | Err(_) => { 52 | extra.warnings.on_fallback_py(self.get_name(), value, extra)?; 53 | infer_to_python(value, include, exclude, extra) 54 | } 55 | } 56 | } 57 | 58 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 59 | match key.downcast::() { 60 | Ok(py_bytes) => self.bytes_mode.bytes_to_string(key.py(), py_bytes.as_bytes()), 61 | Err(_) => { 62 | extra.warnings.on_fallback_py(self.get_name(), key, extra)?; 63 | infer_json_key(key, extra) 64 | } 65 | } 66 | } 67 | 68 | fn serde_serialize( 69 | &self, 70 | value: &Bound<'_, PyAny>, 71 | serializer: S, 72 | include: Option<&Bound<'_, PyAny>>, 73 | exclude: Option<&Bound<'_, PyAny>>, 74 | extra: &Extra, 75 | ) -> Result { 76 | match value.downcast::() { 77 | Ok(py_bytes) => self.bytes_mode.serialize_bytes(py_bytes.as_bytes(), serializer), 78 | Err(_) => { 79 | extra.warnings.on_fallback_ser::(self.get_name(), value, extra)?; 80 | infer_serialize(value, serializer, include, exclude, extra) 81 | } 82 | } 83 | } 84 | 85 | fn get_name(&self) -> &str { 86 | Self::EXPECTED_TYPE 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/complex.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::types::{PyComplex, PyDict}; 4 | use pyo3::{prelude::*, IntoPyObjectExt}; 5 | 6 | use crate::definitions::DefinitionsBuilder; 7 | 8 | use super::{infer_serialize, infer_to_python, BuildSerializer, CombinedSerializer, Extra, SerMode, TypeSerializer}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct ComplexSerializer {} 12 | 13 | impl BuildSerializer for ComplexSerializer { 14 | const EXPECTED_TYPE: &'static str = "complex"; 15 | fn build( 16 | _schema: &Bound<'_, PyDict>, 17 | _config: Option<&Bound<'_, PyDict>>, 18 | _definitions: &mut DefinitionsBuilder, 19 | ) -> PyResult { 20 | Ok(Self {}.into()) 21 | } 22 | } 23 | 24 | impl_py_gc_traverse!(ComplexSerializer {}); 25 | 26 | impl TypeSerializer for ComplexSerializer { 27 | fn to_python( 28 | &self, 29 | value: &Bound<'_, PyAny>, 30 | include: Option<&Bound<'_, PyAny>>, 31 | exclude: Option<&Bound<'_, PyAny>>, 32 | extra: &Extra, 33 | ) -> PyResult { 34 | let py = value.py(); 35 | match value.downcast::() { 36 | Ok(py_complex) => match extra.mode { 37 | SerMode::Json => complex_to_str(py_complex).into_py_any(py), 38 | _ => Ok(value.clone().unbind()), 39 | }, 40 | Err(_) => { 41 | extra.warnings.on_fallback_py(self.get_name(), value, extra)?; 42 | infer_to_python(value, include, exclude, extra) 43 | } 44 | } 45 | } 46 | 47 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 48 | self.invalid_as_json_key(key, extra, "complex") 49 | } 50 | 51 | fn serde_serialize( 52 | &self, 53 | value: &Bound<'_, PyAny>, 54 | serializer: S, 55 | include: Option<&Bound<'_, PyAny>>, 56 | exclude: Option<&Bound<'_, PyAny>>, 57 | extra: &Extra, 58 | ) -> Result { 59 | match value.downcast::() { 60 | Ok(py_complex) => { 61 | let s = complex_to_str(py_complex); 62 | Ok(serializer.collect_str::(&s)?) 63 | } 64 | Err(_) => { 65 | extra.warnings.on_fallback_ser::(self.get_name(), value, extra)?; 66 | infer_serialize(value, serializer, include, exclude, extra) 67 | } 68 | } 69 | } 70 | 71 | fn get_name(&self) -> &'static str { 72 | "complex" 73 | } 74 | } 75 | 76 | pub fn complex_to_str(py_complex: &Bound<'_, PyComplex>) -> String { 77 | let re = py_complex.real(); 78 | let im = py_complex.imag(); 79 | let mut s = format!("{im}j"); 80 | if re != 0.0 { 81 | let mut sign = ""; 82 | if im >= 0.0 { 83 | sign = "+"; 84 | } 85 | s = format!("{re}{sign}{s}"); 86 | } 87 | s 88 | } 89 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/decimal.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::prelude::*; 4 | use pyo3::types::PyDict; 5 | 6 | use crate::definitions::DefinitionsBuilder; 7 | use crate::serializers::infer::{infer_json_key_known, infer_serialize_known, infer_to_python_known}; 8 | use crate::serializers::ob_type::{IsType, ObType}; 9 | 10 | use super::{ 11 | infer_json_key, infer_serialize, infer_to_python, BuildSerializer, CombinedSerializer, Extra, TypeSerializer, 12 | }; 13 | 14 | #[derive(Debug)] 15 | pub struct DecimalSerializer {} 16 | 17 | impl BuildSerializer for DecimalSerializer { 18 | const EXPECTED_TYPE: &'static str = "decimal"; 19 | 20 | fn build( 21 | _schema: &Bound<'_, PyDict>, 22 | _config: Option<&Bound<'_, PyDict>>, 23 | _definitions: &mut DefinitionsBuilder, 24 | ) -> PyResult { 25 | Ok(Self {}.into()) 26 | } 27 | } 28 | 29 | impl_py_gc_traverse!(DecimalSerializer {}); 30 | 31 | impl TypeSerializer for DecimalSerializer { 32 | fn to_python( 33 | &self, 34 | value: &Bound<'_, PyAny>, 35 | include: Option<&Bound<'_, PyAny>>, 36 | exclude: Option<&Bound<'_, PyAny>>, 37 | extra: &Extra, 38 | ) -> PyResult { 39 | let _py = value.py(); 40 | match extra.ob_type_lookup.is_type(value, ObType::Decimal) { 41 | IsType::Exact | IsType::Subclass => infer_to_python_known(ObType::Decimal, value, include, exclude, extra), 42 | IsType::False => { 43 | extra.warnings.on_fallback_py(self.get_name(), value, extra)?; 44 | infer_to_python(value, include, exclude, extra) 45 | } 46 | } 47 | } 48 | 49 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 50 | match extra.ob_type_lookup.is_type(key, ObType::Decimal) { 51 | IsType::Exact | IsType::Subclass => infer_json_key_known(ObType::Decimal, key, extra), 52 | IsType::False => { 53 | extra.warnings.on_fallback_py(self.get_name(), key, extra)?; 54 | infer_json_key(key, extra) 55 | } 56 | } 57 | } 58 | 59 | fn serde_serialize( 60 | &self, 61 | value: &Bound<'_, PyAny>, 62 | serializer: S, 63 | include: Option<&Bound<'_, PyAny>>, 64 | exclude: Option<&Bound<'_, PyAny>>, 65 | extra: &Extra, 66 | ) -> Result { 67 | match extra.ob_type_lookup.is_type(value, ObType::Decimal) { 68 | IsType::Exact | IsType::Subclass => { 69 | infer_serialize_known(ObType::Decimal, value, serializer, include, exclude, extra) 70 | } 71 | IsType::False => { 72 | extra.warnings.on_fallback_ser::(self.get_name(), value, extra)?; 73 | infer_serialize(value, serializer, include, exclude, extra) 74 | } 75 | } 76 | } 77 | 78 | fn get_name(&self) -> &str { 79 | Self::EXPECTED_TYPE 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/json.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::str::from_utf8; 3 | 4 | use pyo3::intern; 5 | use pyo3::prelude::*; 6 | use pyo3::types::PyDict; 7 | 8 | use pyo3::types::PyString; 9 | use serde::ser::Error; 10 | 11 | use crate::definitions::DefinitionsBuilder; 12 | use crate::tools::SchemaDict; 13 | 14 | use super::any::AnySerializer; 15 | use super::{ 16 | infer_json_key, py_err_se_err, to_json_bytes, utf8_py_error, BuildSerializer, CombinedSerializer, Extra, 17 | TypeSerializer, 18 | }; 19 | 20 | #[derive(Debug)] 21 | pub struct JsonSerializer { 22 | serializer: Box, 23 | } 24 | 25 | impl BuildSerializer for JsonSerializer { 26 | const EXPECTED_TYPE: &'static str = "json"; 27 | 28 | fn build( 29 | schema: &Bound<'_, PyDict>, 30 | config: Option<&Bound<'_, PyDict>>, 31 | definitions: &mut DefinitionsBuilder, 32 | ) -> PyResult { 33 | let py = schema.py(); 34 | 35 | let serializer = match schema.get_as(intern!(py, "schema"))? { 36 | Some(items_schema) => CombinedSerializer::build(&items_schema, config, definitions)?, 37 | None => AnySerializer::build(schema, config, definitions)?, 38 | }; 39 | Ok(Self { 40 | serializer: Box::new(serializer), 41 | } 42 | .into()) 43 | } 44 | } 45 | 46 | impl_py_gc_traverse!(JsonSerializer { serializer }); 47 | 48 | impl TypeSerializer for JsonSerializer { 49 | fn to_python( 50 | &self, 51 | value: &Bound<'_, PyAny>, 52 | include: Option<&Bound<'_, PyAny>>, 53 | exclude: Option<&Bound<'_, PyAny>>, 54 | extra: &Extra, 55 | ) -> PyResult { 56 | if extra.round_trip { 57 | let bytes = to_json_bytes(value, &self.serializer, include, exclude, extra, None, 0)?; 58 | let py = value.py(); 59 | let s = from_utf8(&bytes).map_err(|e| utf8_py_error(py, e, &bytes))?; 60 | Ok(PyString::new(py, s).into()) 61 | } else { 62 | self.serializer.to_python(value, include, exclude, extra) 63 | } 64 | } 65 | 66 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 67 | if extra.round_trip { 68 | let bytes = to_json_bytes(key, &self.serializer, None, None, extra, None, 0)?; 69 | let py = key.py(); 70 | let s = from_utf8(&bytes).map_err(|e| utf8_py_error(py, e, &bytes))?; 71 | Ok(Cow::Owned(s.to_string())) 72 | } else { 73 | infer_json_key(key, extra) 74 | } 75 | } 76 | 77 | fn serde_serialize( 78 | &self, 79 | value: &Bound<'_, PyAny>, 80 | serializer: S, 81 | include: Option<&Bound<'_, PyAny>>, 82 | exclude: Option<&Bound<'_, PyAny>>, 83 | extra: &Extra, 84 | ) -> Result { 85 | if extra.round_trip { 86 | let bytes = 87 | to_json_bytes(value, &self.serializer, include, exclude, extra, None, 0).map_err(py_err_se_err)?; 88 | match from_utf8(&bytes) { 89 | Ok(s) => serializer.serialize_str(s), 90 | Err(e) => Err(Error::custom(e.to_string())), 91 | } 92 | } else { 93 | self.serializer 94 | .serde_serialize(value, serializer, include, exclude, extra) 95 | } 96 | } 97 | 98 | fn get_name(&self) -> &str { 99 | Self::EXPECTED_TYPE 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/json_or_python.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::intern; 4 | use pyo3::prelude::*; 5 | use pyo3::types::PyDict; 6 | 7 | use super::{BuildSerializer, CombinedSerializer, Extra, TypeSerializer}; 8 | use crate::definitions::DefinitionsBuilder; 9 | use crate::tools::SchemaDict; 10 | 11 | #[derive(Debug)] 12 | pub struct JsonOrPythonSerializer { 13 | json: Box, 14 | python: Box, 15 | name: String, 16 | } 17 | 18 | impl BuildSerializer for JsonOrPythonSerializer { 19 | const EXPECTED_TYPE: &'static str = "json-or-python"; 20 | 21 | fn build( 22 | schema: &Bound<'_, PyDict>, 23 | config: Option<&Bound<'_, PyDict>>, 24 | definitions: &mut DefinitionsBuilder, 25 | ) -> PyResult { 26 | let py = schema.py(); 27 | let json_schema = schema.get_as_req(intern!(py, "json_schema"))?; 28 | let python_schema = schema.get_as_req(intern!(py, "python_schema"))?; 29 | 30 | let json = CombinedSerializer::build(&json_schema, config, definitions)?; 31 | let python = CombinedSerializer::build(&python_schema, config, definitions)?; 32 | 33 | let name = format!( 34 | "{}[json={}, python={}]", 35 | Self::EXPECTED_TYPE, 36 | json.get_name(), 37 | python.get_name(), 38 | ); 39 | Ok(Self { 40 | json: Box::new(json), 41 | python: Box::new(python), 42 | name, 43 | } 44 | .into()) 45 | } 46 | } 47 | 48 | impl_py_gc_traverse!(JsonOrPythonSerializer { json, python }); 49 | 50 | impl TypeSerializer for JsonOrPythonSerializer { 51 | fn to_python( 52 | &self, 53 | value: &Bound<'_, PyAny>, 54 | include: Option<&Bound<'_, PyAny>>, 55 | exclude: Option<&Bound<'_, PyAny>>, 56 | extra: &Extra, 57 | ) -> PyResult { 58 | self.python.to_python(value, include, exclude, extra) 59 | } 60 | 61 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 62 | self.json.json_key(key, extra) 63 | } 64 | 65 | fn serde_serialize( 66 | &self, 67 | value: &Bound<'_, PyAny>, 68 | serializer: S, 69 | include: Option<&Bound<'_, PyAny>>, 70 | exclude: Option<&Bound<'_, PyAny>>, 71 | extra: &Extra, 72 | ) -> Result { 73 | self.json.serde_serialize(value, serializer, include, exclude, extra) 74 | } 75 | 76 | fn get_name(&self) -> &str { 77 | &self.name 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod any; 2 | pub mod bytes; 3 | pub mod complex; 4 | pub mod dataclass; 5 | pub mod datetime_etc; 6 | pub mod decimal; 7 | pub mod definitions; 8 | pub mod dict; 9 | pub mod enum_; 10 | pub mod float; 11 | pub mod format; 12 | pub mod function; 13 | pub mod generator; 14 | pub mod json; 15 | pub mod json_or_python; 16 | pub mod list; 17 | pub mod literal; 18 | pub mod model; 19 | pub mod nullable; 20 | pub mod other; 21 | pub mod set_frozenset; 22 | pub mod simple; 23 | pub mod string; 24 | pub mod timedelta; 25 | pub mod tuple; 26 | pub mod typed_dict; 27 | pub mod union; 28 | pub mod url; 29 | pub mod uuid; 30 | pub mod with_default; 31 | 32 | use super::computed_fields::ComputedFields; 33 | use super::config::utf8_py_error; 34 | use super::errors::{py_err_se_err, PydanticSerializationError}; 35 | use super::extra::{Extra, ExtraOwned, SerCheck, SerMode}; 36 | use super::fields::{FieldsMode, GeneralFieldsSerializer, SerField}; 37 | use super::filter::{AnyFilter, SchemaFilter}; 38 | use super::infer::{infer_json_key, infer_json_key_known, infer_serialize, infer_to_python}; 39 | use super::ob_type::{IsType, ObType}; 40 | use super::shared::{to_json_bytes, BuildSerializer, CombinedSerializer, PydanticSerializer, TypeSerializer}; 41 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/nullable.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::intern; 4 | use pyo3::prelude::*; 5 | use pyo3::types::PyDict; 6 | 7 | use crate::definitions::DefinitionsBuilder; 8 | use crate::tools::SchemaDict; 9 | 10 | use super::{infer_json_key_known, BuildSerializer, CombinedSerializer, Extra, IsType, ObType, TypeSerializer}; 11 | 12 | #[derive(Debug)] 13 | pub struct NullableSerializer { 14 | serializer: Box, 15 | } 16 | 17 | impl BuildSerializer for NullableSerializer { 18 | const EXPECTED_TYPE: &'static str = "nullable"; 19 | 20 | fn build( 21 | schema: &Bound<'_, PyDict>, 22 | config: Option<&Bound<'_, PyDict>>, 23 | definitions: &mut DefinitionsBuilder, 24 | ) -> PyResult { 25 | let sub_schema = schema.get_as_req(intern!(schema.py(), "schema"))?; 26 | Ok(Self { 27 | serializer: Box::new(CombinedSerializer::build(&sub_schema, config, definitions)?), 28 | } 29 | .into()) 30 | } 31 | } 32 | 33 | impl_py_gc_traverse!(NullableSerializer { serializer }); 34 | 35 | impl TypeSerializer for NullableSerializer { 36 | fn to_python( 37 | &self, 38 | value: &Bound<'_, PyAny>, 39 | include: Option<&Bound<'_, PyAny>>, 40 | exclude: Option<&Bound<'_, PyAny>>, 41 | extra: &Extra, 42 | ) -> PyResult { 43 | let py = value.py(); 44 | match extra.ob_type_lookup.is_type(value, ObType::None) { 45 | IsType::Exact => Ok(py.None()), 46 | // I don't think subclasses of None can exist 47 | _ => self.serializer.to_python(value, include, exclude, extra), 48 | } 49 | } 50 | 51 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 52 | match extra.ob_type_lookup.is_type(key, ObType::None) { 53 | IsType::Exact => infer_json_key_known(ObType::None, key, extra), 54 | _ => self.serializer.json_key(key, extra), 55 | } 56 | } 57 | 58 | fn serde_serialize( 59 | &self, 60 | value: &Bound<'_, PyAny>, 61 | serializer: S, 62 | include: Option<&Bound<'_, PyAny>>, 63 | exclude: Option<&Bound<'_, PyAny>>, 64 | extra: &Extra, 65 | ) -> Result { 66 | match extra.ob_type_lookup.is_type(value, ObType::None) { 67 | IsType::Exact => serializer.serialize_none(), 68 | _ => self 69 | .serializer 70 | .serde_serialize(value, serializer, include, exclude, extra), 71 | } 72 | } 73 | 74 | fn get_name(&self) -> &str { 75 | Self::EXPECTED_TYPE 76 | } 77 | 78 | fn retry_with_lax_check(&self) -> bool { 79 | self.serializer.retry_with_lax_check() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/other.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::{PyDict, PyList}; 4 | 5 | use crate::build_tools::py_schema_err; 6 | use crate::definitions::DefinitionsBuilder; 7 | use crate::tools::SchemaDict; 8 | 9 | use super::any::AnySerializer; 10 | use super::{BuildSerializer, CombinedSerializer}; 11 | 12 | pub struct ChainBuilder; 13 | 14 | impl BuildSerializer for ChainBuilder { 15 | const EXPECTED_TYPE: &'static str = "chain"; 16 | 17 | fn build( 18 | schema: &Bound<'_, PyDict>, 19 | config: Option<&Bound<'_, PyDict>>, 20 | definitions: &mut DefinitionsBuilder, 21 | ) -> PyResult { 22 | let last_schema = schema 23 | .get_as_req::>(intern!(schema.py(), "steps"))? 24 | .iter() 25 | .last() 26 | .unwrap() 27 | .downcast_into()?; 28 | CombinedSerializer::build(&last_schema, config, definitions) 29 | } 30 | } 31 | 32 | pub struct CustomErrorBuilder; 33 | 34 | impl BuildSerializer for CustomErrorBuilder { 35 | const EXPECTED_TYPE: &'static str = "custom-error"; 36 | 37 | fn build( 38 | schema: &Bound<'_, PyDict>, 39 | config: Option<&Bound<'_, PyDict>>, 40 | definitions: &mut DefinitionsBuilder, 41 | ) -> PyResult { 42 | let sub_schema = schema.get_as_req(intern!(schema.py(), "schema"))?; 43 | CombinedSerializer::build(&sub_schema, config, definitions) 44 | } 45 | } 46 | 47 | pub struct CallBuilder; 48 | 49 | impl BuildSerializer for CallBuilder { 50 | const EXPECTED_TYPE: &'static str = "call"; 51 | 52 | fn build( 53 | schema: &Bound<'_, PyDict>, 54 | config: Option<&Bound<'_, PyDict>>, 55 | definitions: &mut DefinitionsBuilder, 56 | ) -> PyResult { 57 | let return_schema = schema.get_as(intern!(schema.py(), "return_schema"))?; 58 | match return_schema { 59 | Some(return_schema) => CombinedSerializer::build(&return_schema, config, definitions), 60 | None => AnySerializer::build(schema, config, definitions), 61 | } 62 | } 63 | } 64 | 65 | pub struct LaxOrStrictBuilder; 66 | 67 | impl BuildSerializer for LaxOrStrictBuilder { 68 | const EXPECTED_TYPE: &'static str = "lax-or-strict"; 69 | 70 | fn build( 71 | schema: &Bound<'_, PyDict>, 72 | config: Option<&Bound<'_, PyDict>>, 73 | definitions: &mut DefinitionsBuilder, 74 | ) -> PyResult { 75 | let strict_schema = schema.get_as_req(intern!(schema.py(), "strict_schema"))?; 76 | CombinedSerializer::build(&strict_schema, config, definitions) 77 | } 78 | } 79 | 80 | pub struct ArgumentsBuilder; 81 | 82 | impl BuildSerializer for ArgumentsBuilder { 83 | const EXPECTED_TYPE: &'static str = "arguments"; 84 | 85 | fn build( 86 | _schema: &Bound<'_, PyDict>, 87 | _config: Option<&Bound<'_, PyDict>>, 88 | _definitions: &mut DefinitionsBuilder, 89 | ) -> PyResult { 90 | py_schema_err!("`arguments` validators require a custom serializer") 91 | } 92 | } 93 | 94 | macro_rules! any_build_serializer { 95 | ($struct_name:ident, $expected_type:literal) => { 96 | pub struct $struct_name; 97 | 98 | impl BuildSerializer for $struct_name { 99 | const EXPECTED_TYPE: &'static str = $expected_type; 100 | 101 | fn build( 102 | schema: &Bound<'_, PyDict>, 103 | config: Option<&Bound<'_, PyDict>>, 104 | definitions: &mut DefinitionsBuilder, 105 | ) -> PyResult { 106 | AnySerializer::build(schema, config, definitions) 107 | } 108 | } 109 | }; 110 | } 111 | any_build_serializer!(IsInstanceBuilder, "is-instance"); 112 | any_build_serializer!(IsSubclassBuilder, "is-subclass"); 113 | any_build_serializer!(CallableBuilder, "callable"); 114 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/string.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::types::{PyDict, PyString}; 4 | use pyo3::{prelude::*, IntoPyObjectExt}; 5 | 6 | use crate::definitions::DefinitionsBuilder; 7 | 8 | use super::{ 9 | infer_json_key, infer_serialize, infer_to_python, py_err_se_err, BuildSerializer, CombinedSerializer, Extra, 10 | IsType, ObType, SerMode, TypeSerializer, 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub struct StrSerializer; 15 | 16 | impl StrSerializer { 17 | pub fn new() -> Self { 18 | Self {} 19 | } 20 | } 21 | 22 | impl BuildSerializer for StrSerializer { 23 | const EXPECTED_TYPE: &'static str = "str"; 24 | 25 | fn build( 26 | _schema: &Bound<'_, PyDict>, 27 | _config: Option<&Bound<'_, PyDict>>, 28 | _definitions: &mut DefinitionsBuilder, 29 | ) -> PyResult { 30 | Ok(Self::new().into()) 31 | } 32 | } 33 | 34 | impl_py_gc_traverse!(StrSerializer {}); 35 | 36 | impl TypeSerializer for StrSerializer { 37 | fn to_python( 38 | &self, 39 | value: &Bound<'_, PyAny>, 40 | include: Option<&Bound<'_, PyAny>>, 41 | exclude: Option<&Bound<'_, PyAny>>, 42 | extra: &Extra, 43 | ) -> PyResult { 44 | let py = value.py(); 45 | match extra.ob_type_lookup.is_type(value, ObType::Str) { 46 | IsType::Exact => Ok(value.clone().unbind()), 47 | IsType::Subclass => match extra.mode { 48 | SerMode::Json => value.downcast::()?.to_str()?.into_py_any(py), 49 | _ => Ok(value.clone().unbind()), 50 | }, 51 | IsType::False => { 52 | extra.warnings.on_fallback_py(self.get_name(), value, extra)?; 53 | infer_to_python(value, include, exclude, extra) 54 | } 55 | } 56 | } 57 | 58 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 59 | if let Ok(py_str) = key.downcast::() { 60 | // FIXME py cow to avoid the copy 61 | Ok(Cow::Owned(py_str.to_string_lossy().into_owned())) 62 | } else { 63 | extra.warnings.on_fallback_py(self.get_name(), key, extra)?; 64 | infer_json_key(key, extra) 65 | } 66 | } 67 | 68 | fn serde_serialize( 69 | &self, 70 | value: &Bound<'_, PyAny>, 71 | serializer: S, 72 | include: Option<&Bound<'_, PyAny>>, 73 | exclude: Option<&Bound<'_, PyAny>>, 74 | extra: &Extra, 75 | ) -> Result { 76 | match value.downcast::() { 77 | Ok(py_str) => serialize_py_str(py_str, serializer), 78 | Err(_) => { 79 | extra.warnings.on_fallback_ser::(self.get_name(), value, extra)?; 80 | infer_serialize(value, serializer, include, exclude, extra) 81 | } 82 | } 83 | } 84 | 85 | fn get_name(&self) -> &str { 86 | Self::EXPECTED_TYPE 87 | } 88 | } 89 | 90 | pub fn serialize_py_str( 91 | py_str: &Bound<'_, PyString>, 92 | serializer: S, 93 | ) -> Result { 94 | let s = py_str.to_str().map_err(py_err_se_err)?; 95 | serializer.serialize_str(s) 96 | } 97 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/timedelta.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::prelude::*; 4 | use pyo3::types::PyDict; 5 | 6 | use crate::definitions::DefinitionsBuilder; 7 | use crate::input::EitherTimedelta; 8 | use crate::serializers::config::{FromConfig, TimedeltaMode}; 9 | 10 | use super::{ 11 | infer_json_key, infer_serialize, infer_to_python, BuildSerializer, CombinedSerializer, Extra, SerMode, 12 | TypeSerializer, 13 | }; 14 | 15 | #[derive(Debug)] 16 | pub struct TimeDeltaSerializer { 17 | timedelta_mode: TimedeltaMode, 18 | } 19 | 20 | impl BuildSerializer for TimeDeltaSerializer { 21 | const EXPECTED_TYPE: &'static str = "timedelta"; 22 | 23 | fn build( 24 | _schema: &Bound<'_, PyDict>, 25 | config: Option<&Bound<'_, PyDict>>, 26 | _definitions: &mut DefinitionsBuilder, 27 | ) -> PyResult { 28 | let timedelta_mode = TimedeltaMode::from_config(config)?; 29 | Ok(Self { timedelta_mode }.into()) 30 | } 31 | } 32 | 33 | impl_py_gc_traverse!(TimeDeltaSerializer {}); 34 | 35 | impl TypeSerializer for TimeDeltaSerializer { 36 | fn to_python( 37 | &self, 38 | value: &Bound<'_, PyAny>, 39 | include: Option<&Bound<'_, PyAny>>, 40 | exclude: Option<&Bound<'_, PyAny>>, 41 | extra: &Extra, 42 | ) -> PyResult { 43 | match extra.mode { 44 | SerMode::Json => match EitherTimedelta::try_from(value) { 45 | Ok(either_timedelta) => self.timedelta_mode.either_delta_to_json(value.py(), either_timedelta), 46 | Err(_) => { 47 | extra.warnings.on_fallback_py(self.get_name(), value, extra)?; 48 | infer_to_python(value, include, exclude, extra) 49 | } 50 | }, 51 | _ => infer_to_python(value, include, exclude, extra), 52 | } 53 | } 54 | 55 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 56 | match EitherTimedelta::try_from(key) { 57 | Ok(either_timedelta) => self.timedelta_mode.json_key(key.py(), either_timedelta), 58 | Err(_) => { 59 | extra.warnings.on_fallback_py(self.get_name(), key, extra)?; 60 | infer_json_key(key, extra) 61 | } 62 | } 63 | } 64 | 65 | fn serde_serialize( 66 | &self, 67 | value: &Bound<'_, PyAny>, 68 | serializer: S, 69 | include: Option<&Bound<'_, PyAny>>, 70 | exclude: Option<&Bound<'_, PyAny>>, 71 | extra: &Extra, 72 | ) -> Result { 73 | match EitherTimedelta::try_from(value) { 74 | Ok(either_timedelta) => self 75 | .timedelta_mode 76 | .timedelta_serialize(value.py(), either_timedelta, serializer), 77 | Err(_) => { 78 | extra.warnings.on_fallback_ser::(self.get_name(), value, extra)?; 79 | infer_serialize(value, serializer, include, exclude, extra) 80 | } 81 | } 82 | } 83 | 84 | fn get_name(&self) -> &str { 85 | Self::EXPECTED_TYPE 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/typed_dict.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::{PyDict, PyString}; 4 | 5 | use ahash::AHashMap; 6 | 7 | use crate::build_tools::py_schema_err; 8 | use crate::build_tools::{py_schema_error_type, schema_or_config, ExtraBehavior}; 9 | use crate::definitions::DefinitionsBuilder; 10 | use crate::tools::SchemaDict; 11 | 12 | use super::{BuildSerializer, CombinedSerializer, ComputedFields, FieldsMode, GeneralFieldsSerializer, SerField}; 13 | 14 | #[derive(Debug)] 15 | pub struct TypedDictBuilder; 16 | 17 | impl BuildSerializer for TypedDictBuilder { 18 | const EXPECTED_TYPE: &'static str = "typed-dict"; 19 | 20 | fn build( 21 | schema: &Bound<'_, PyDict>, 22 | config: Option<&Bound<'_, PyDict>>, 23 | definitions: &mut DefinitionsBuilder, 24 | ) -> PyResult { 25 | let py = schema.py(); 26 | 27 | let total = 28 | schema_or_config(schema, config, intern!(py, "total"), intern!(py, "typed_dict_total"))?.unwrap_or(true); 29 | 30 | let fields_mode = match ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)? { 31 | ExtraBehavior::Allow => FieldsMode::TypedDictAllow, 32 | _ => FieldsMode::SimpleDict, 33 | }; 34 | 35 | let serialize_by_alias = config.get_as(intern!(py, "serialize_by_alias"))?; 36 | 37 | let fields_dict: Bound<'_, PyDict> = schema.get_as_req(intern!(py, "fields"))?; 38 | let mut fields: AHashMap = AHashMap::with_capacity(fields_dict.len()); 39 | 40 | let extra_serializer = match (schema.get_item(intern!(py, "extras_schema"))?, &fields_mode) { 41 | (Some(v), FieldsMode::TypedDictAllow) => { 42 | Some(CombinedSerializer::build(&v.extract()?, config, definitions)?) 43 | } 44 | (Some(_), _) => return py_schema_err!("extras_schema can only be used if extra_behavior=allow"), 45 | (_, _) => None, 46 | }; 47 | 48 | for (key, value) in fields_dict { 49 | let key_py = key.downcast_into::()?; 50 | let key: String = key_py.extract()?; 51 | let field_info = value.downcast()?; 52 | 53 | let key_py: Py = key_py.into(); 54 | let required = field_info.get_as(intern!(py, "required"))?.unwrap_or(total); 55 | 56 | if field_info.get_as(intern!(py, "serialization_exclude"))? == Some(true) { 57 | fields.insert(key, SerField::new(py, key_py, None, None, required, serialize_by_alias)); 58 | } else { 59 | let alias: Option = field_info.get_as(intern!(py, "serialization_alias"))?; 60 | 61 | let schema = field_info.get_as_req(intern!(py, "schema"))?; 62 | let serializer = CombinedSerializer::build(&schema, config, definitions) 63 | .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", key, e))?; 64 | fields.insert( 65 | key, 66 | SerField::new(py, key_py, alias, Some(serializer), required, serialize_by_alias), 67 | ); 68 | } 69 | } 70 | 71 | let computed_fields = ComputedFields::new(schema, config, definitions)?; 72 | 73 | Ok(GeneralFieldsSerializer::new(fields, fields_mode, extra_serializer, computed_fields).into()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/url.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::prelude::*; 4 | use pyo3::types::PyDict; 5 | use pyo3::IntoPyObjectExt; 6 | 7 | use crate::definitions::DefinitionsBuilder; 8 | 9 | use crate::url::{PyMultiHostUrl, PyUrl}; 10 | 11 | use super::{ 12 | infer_json_key, infer_serialize, infer_to_python, BuildSerializer, CombinedSerializer, Extra, SerMode, 13 | TypeSerializer, 14 | }; 15 | 16 | macro_rules! build_serializer { 17 | ($struct_name:ident, $expected_type:literal, $extract:ty) => { 18 | #[derive(Debug)] 19 | pub struct $struct_name; 20 | 21 | impl BuildSerializer for $struct_name { 22 | const EXPECTED_TYPE: &'static str = $expected_type; 23 | 24 | fn build( 25 | _schema: &Bound<'_, PyDict>, 26 | _config: Option<&Bound<'_, PyDict>>, 27 | _definitions: &mut DefinitionsBuilder, 28 | ) -> PyResult { 29 | Ok(Self {}.into()) 30 | } 31 | } 32 | 33 | impl_py_gc_traverse!($struct_name {}); 34 | 35 | impl TypeSerializer for $struct_name { 36 | fn to_python( 37 | &self, 38 | value: &Bound<'_, PyAny>, 39 | include: Option<&Bound<'_, PyAny>>, 40 | exclude: Option<&Bound<'_, PyAny>>, 41 | extra: &Extra, 42 | ) -> PyResult { 43 | let py = value.py(); 44 | match value.extract::<$extract>() { 45 | Ok(py_url) => match extra.mode { 46 | SerMode::Json => py_url.__str__().into_py_any(py), 47 | _ => Ok(value.clone().unbind()), 48 | }, 49 | Err(_) => { 50 | extra.warnings.on_fallback_py(self.get_name(), value, extra)?; 51 | infer_to_python(value, include, exclude, extra) 52 | } 53 | } 54 | } 55 | 56 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 57 | match key.extract::<$extract>() { 58 | Ok(py_url) => Ok(Cow::Owned(py_url.__str__().to_string())), 59 | Err(_) => { 60 | extra.warnings.on_fallback_py(self.get_name(), key, extra)?; 61 | infer_json_key(key, extra) 62 | } 63 | } 64 | } 65 | 66 | fn serde_serialize( 67 | &self, 68 | value: &Bound<'_, PyAny>, 69 | serializer: S, 70 | include: Option<&Bound<'_, PyAny>>, 71 | exclude: Option<&Bound<'_, PyAny>>, 72 | extra: &Extra, 73 | ) -> Result { 74 | match value.extract::<$extract>() { 75 | Ok(py_url) => serializer.serialize_str(&py_url.__str__()), 76 | Err(_) => { 77 | extra 78 | .warnings 79 | .on_fallback_ser::(self.get_name(), value, extra)?; 80 | infer_serialize(value, serializer, include, exclude, extra) 81 | } 82 | } 83 | } 84 | 85 | fn get_name(&self) -> &str { 86 | Self::EXPECTED_TYPE 87 | } 88 | } 89 | }; 90 | } 91 | build_serializer!(UrlSerializer, "url", PyUrl); 92 | build_serializer!(MultiHostUrlSerializer, "multi-host-url", PyMultiHostUrl); 93 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/uuid.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::types::PyDict; 4 | use pyo3::{intern, prelude::*, IntoPyObjectExt}; 5 | use uuid::Uuid; 6 | 7 | use crate::definitions::DefinitionsBuilder; 8 | 9 | use super::{ 10 | infer_json_key, infer_serialize, infer_to_python, py_err_se_err, BuildSerializer, CombinedSerializer, Extra, 11 | IsType, ObType, SerMode, TypeSerializer, 12 | }; 13 | 14 | pub(crate) fn uuid_to_string(py_uuid: &Bound<'_, PyAny>) -> PyResult { 15 | let py = py_uuid.py(); 16 | let uuid_int_val: u128 = py_uuid.getattr(intern!(py, "int"))?.extract()?; 17 | let uuid = Uuid::from_u128(uuid_int_val); 18 | Ok(uuid.to_string()) 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct UuidSerializer; 23 | 24 | impl_py_gc_traverse!(UuidSerializer {}); 25 | 26 | impl BuildSerializer for UuidSerializer { 27 | const EXPECTED_TYPE: &'static str = "uuid"; 28 | 29 | fn build( 30 | _schema: &Bound<'_, PyDict>, 31 | _config: Option<&Bound<'_, PyDict>>, 32 | _definitions: &mut DefinitionsBuilder, 33 | ) -> PyResult { 34 | Ok(Self {}.into()) 35 | } 36 | } 37 | 38 | impl TypeSerializer for UuidSerializer { 39 | fn to_python( 40 | &self, 41 | value: &Bound<'_, PyAny>, 42 | include: Option<&Bound<'_, PyAny>>, 43 | exclude: Option<&Bound<'_, PyAny>>, 44 | extra: &Extra, 45 | ) -> PyResult { 46 | let py = value.py(); 47 | match extra.ob_type_lookup.is_type(value, ObType::Uuid) { 48 | IsType::Exact | IsType::Subclass => match extra.mode { 49 | SerMode::Json => uuid_to_string(value)?.into_py_any(py), 50 | _ => Ok(value.clone().unbind()), 51 | }, 52 | IsType::False => { 53 | extra.warnings.on_fallback_py(self.get_name(), value, extra)?; 54 | infer_to_python(value, include, exclude, extra) 55 | } 56 | } 57 | } 58 | 59 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 60 | match extra.ob_type_lookup.is_type(key, ObType::Uuid) { 61 | IsType::Exact | IsType::Subclass => { 62 | let str = uuid_to_string(key)?; 63 | Ok(Cow::Owned(str)) 64 | } 65 | IsType::False => { 66 | extra.warnings.on_fallback_py(self.get_name(), key, extra)?; 67 | infer_json_key(key, extra) 68 | } 69 | } 70 | } 71 | 72 | fn serde_serialize( 73 | &self, 74 | value: &Bound<'_, PyAny>, 75 | serializer: S, 76 | include: Option<&Bound<'_, PyAny>>, 77 | exclude: Option<&Bound<'_, PyAny>>, 78 | extra: &Extra, 79 | ) -> Result { 80 | match extra.ob_type_lookup.is_type(value, ObType::Uuid) { 81 | IsType::Exact | IsType::Subclass => { 82 | let s = uuid_to_string(value).map_err(py_err_se_err)?; 83 | serializer.serialize_str(&s) 84 | } 85 | IsType::False => { 86 | extra.warnings.on_fallback_ser::(self.get_name(), value, extra)?; 87 | infer_serialize(value, serializer, include, exclude, extra) 88 | } 89 | } 90 | } 91 | 92 | fn get_name(&self) -> &str { 93 | Self::EXPECTED_TYPE 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/serializers/type_serializers/with_default.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use pyo3::intern; 4 | use pyo3::prelude::*; 5 | use pyo3::types::PyDict; 6 | 7 | use crate::definitions::DefinitionsBuilder; 8 | use crate::tools::SchemaDict; 9 | use crate::validators::DefaultType; 10 | 11 | use super::{BuildSerializer, CombinedSerializer, Extra, TypeSerializer}; 12 | 13 | #[derive(Debug)] 14 | pub struct WithDefaultSerializer { 15 | default: DefaultType, 16 | serializer: Box, 17 | } 18 | 19 | impl BuildSerializer for WithDefaultSerializer { 20 | const EXPECTED_TYPE: &'static str = "default"; 21 | 22 | fn build( 23 | schema: &Bound<'_, PyDict>, 24 | config: Option<&Bound<'_, PyDict>>, 25 | definitions: &mut DefinitionsBuilder, 26 | ) -> PyResult { 27 | let py = schema.py(); 28 | let default = DefaultType::new(schema)?; 29 | 30 | let sub_schema = schema.get_as_req(intern!(py, "schema"))?; 31 | let serializer = Box::new(CombinedSerializer::build(&sub_schema, config, definitions)?); 32 | 33 | Ok(Self { default, serializer }.into()) 34 | } 35 | } 36 | 37 | impl_py_gc_traverse!(WithDefaultSerializer { default, serializer }); 38 | 39 | impl TypeSerializer for WithDefaultSerializer { 40 | fn to_python( 41 | &self, 42 | value: &Bound<'_, PyAny>, 43 | include: Option<&Bound<'_, PyAny>>, 44 | exclude: Option<&Bound<'_, PyAny>>, 45 | extra: &Extra, 46 | ) -> PyResult { 47 | self.serializer.to_python(value, include, exclude, extra) 48 | } 49 | 50 | fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { 51 | self.serializer.json_key(key, extra) 52 | } 53 | 54 | fn serde_serialize( 55 | &self, 56 | value: &Bound<'_, PyAny>, 57 | serializer: S, 58 | include: Option<&Bound<'_, PyAny>>, 59 | exclude: Option<&Bound<'_, PyAny>>, 60 | extra: &Extra, 61 | ) -> Result { 62 | self.serializer 63 | .serde_serialize(value, serializer, include, exclude, extra) 64 | } 65 | 66 | fn get_name(&self) -> &str { 67 | Self::EXPECTED_TYPE 68 | } 69 | 70 | fn retry_with_lax_check(&self) -> bool { 71 | self.serializer.retry_with_lax_check() 72 | } 73 | 74 | fn get_default(&self, py: Python) -> PyResult> { 75 | if let DefaultType::DefaultFactory(_, _takes_data @ true) = self.default { 76 | // We currently don't compute the default if the default factory takes 77 | // the data from other fields. 78 | Ok(None) 79 | } else { 80 | self.default.default_value( 81 | py, None, // Won't be used. 82 | ) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/validators/any.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::types::PyDict; 3 | 4 | use crate::errors::ValResult; 5 | use crate::input::Input; 6 | 7 | use super::{ 8 | validation_state::Exactness, BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator, 9 | }; 10 | 11 | /// This might seem useless, but it's useful in DictValidator to avoid Option a lot 12 | #[derive(Debug, Clone)] 13 | pub struct AnyValidator; 14 | 15 | impl BuildValidator for AnyValidator { 16 | const EXPECTED_TYPE: &'static str = "any"; 17 | 18 | fn build( 19 | _schema: &Bound<'_, PyDict>, 20 | _config: Option<&Bound<'_, PyDict>>, 21 | _definitions: &mut DefinitionsBuilder, 22 | ) -> PyResult { 23 | Ok(Self.into()) 24 | } 25 | } 26 | 27 | impl_py_gc_traverse!(AnyValidator {}); 28 | 29 | impl Validator for AnyValidator { 30 | fn validate<'py>( 31 | &self, 32 | py: Python<'py>, 33 | input: &(impl Input<'py> + ?Sized), 34 | state: &mut ValidationState<'_, 'py>, 35 | ) -> ValResult { 36 | // in a union, Any should be preferred to doing lax coercions 37 | state.floor_exactness(Exactness::Strict); 38 | Ok(input.to_object(py)?.unbind()) 39 | } 40 | 41 | fn get_name(&self) -> &str { 42 | Self::EXPECTED_TYPE 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/validators/bool.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::PyDict; 2 | use pyo3::{prelude::*, IntoPyObjectExt}; 3 | 4 | use crate::build_tools::is_strict; 5 | use crate::errors::ValResult; 6 | use crate::input::Input; 7 | 8 | use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct BoolValidator { 12 | strict: bool, 13 | } 14 | 15 | impl BuildValidator for BoolValidator { 16 | const EXPECTED_TYPE: &'static str = "bool"; 17 | 18 | fn build( 19 | schema: &Bound<'_, PyDict>, 20 | config: Option<&Bound<'_, PyDict>>, 21 | _definitions: &mut DefinitionsBuilder, 22 | ) -> PyResult { 23 | Ok(Self { 24 | strict: is_strict(schema, config)?, 25 | } 26 | .into()) 27 | } 28 | } 29 | 30 | impl_py_gc_traverse!(BoolValidator {}); 31 | 32 | impl Validator for BoolValidator { 33 | fn validate<'py>( 34 | &self, 35 | py: Python<'py>, 36 | input: &(impl Input<'py> + ?Sized), 37 | state: &mut ValidationState<'_, 'py>, 38 | ) -> ValResult { 39 | // TODO in theory this could be quicker if we used PyBool rather than going to a bool 40 | // and back again, might be worth profiling? 41 | input 42 | .validate_bool(state.strict_or(self.strict)) 43 | .and_then(|val_match| Ok(val_match.unpack(state).into_py_any(py)?)) 44 | } 45 | 46 | fn get_name(&self) -> &str { 47 | Self::EXPECTED_TYPE 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/validators/bytes.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::PyDict; 4 | use pyo3::IntoPyObjectExt; 5 | 6 | use crate::build_tools::is_strict; 7 | use crate::errors::{ErrorType, ValError, ValResult}; 8 | use crate::input::Input; 9 | 10 | use crate::tools::SchemaDict; 11 | 12 | use super::config::ValBytesMode; 13 | use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct BytesValidator { 17 | strict: bool, 18 | bytes_mode: ValBytesMode, 19 | } 20 | 21 | impl BuildValidator for BytesValidator { 22 | const EXPECTED_TYPE: &'static str = "bytes"; 23 | 24 | fn build( 25 | schema: &Bound<'_, PyDict>, 26 | config: Option<&Bound<'_, PyDict>>, 27 | _definitions: &mut DefinitionsBuilder, 28 | ) -> PyResult { 29 | let py = schema.py(); 30 | let use_constrained = schema.get_item(intern!(py, "max_length"))?.is_some() 31 | || schema.get_item(intern!(py, "min_length"))?.is_some(); 32 | if use_constrained { 33 | BytesConstrainedValidator::build(schema, config) 34 | } else { 35 | Ok(Self { 36 | strict: is_strict(schema, config)?, 37 | bytes_mode: ValBytesMode::from_config(config)?, 38 | } 39 | .into()) 40 | } 41 | } 42 | } 43 | 44 | impl_py_gc_traverse!(BytesValidator {}); 45 | 46 | impl Validator for BytesValidator { 47 | fn validate<'py>( 48 | &self, 49 | py: Python<'py>, 50 | input: &(impl Input<'py> + ?Sized), 51 | state: &mut ValidationState<'_, 'py>, 52 | ) -> ValResult { 53 | input 54 | .validate_bytes(state.strict_or(self.strict), self.bytes_mode) 55 | .and_then(|m| Ok(m.unpack(state).into_py_any(py)?)) 56 | } 57 | 58 | fn get_name(&self) -> &str { 59 | Self::EXPECTED_TYPE 60 | } 61 | } 62 | 63 | #[derive(Debug, Clone)] 64 | pub struct BytesConstrainedValidator { 65 | strict: bool, 66 | bytes_mode: ValBytesMode, 67 | max_length: Option, 68 | min_length: Option, 69 | } 70 | 71 | impl_py_gc_traverse!(BytesConstrainedValidator {}); 72 | 73 | impl Validator for BytesConstrainedValidator { 74 | fn validate<'py>( 75 | &self, 76 | py: Python<'py>, 77 | input: &(impl Input<'py> + ?Sized), 78 | state: &mut ValidationState<'_, 'py>, 79 | ) -> ValResult { 80 | let either_bytes = input 81 | .validate_bytes(state.strict_or(self.strict), self.bytes_mode)? 82 | .unpack(state); 83 | let len = either_bytes.len()?; 84 | 85 | if let Some(min_length) = self.min_length { 86 | if len < min_length { 87 | return Err(ValError::new( 88 | ErrorType::BytesTooShort { 89 | min_length, 90 | context: None, 91 | }, 92 | input, 93 | )); 94 | } 95 | } 96 | if let Some(max_length) = self.max_length { 97 | if len > max_length { 98 | return Err(ValError::new( 99 | ErrorType::BytesTooLong { 100 | max_length, 101 | context: None, 102 | }, 103 | input, 104 | )); 105 | } 106 | } 107 | Ok(either_bytes.into_py_any(py)?) 108 | } 109 | 110 | fn get_name(&self) -> &'static str { 111 | "constrained-bytes" 112 | } 113 | } 114 | 115 | impl BytesConstrainedValidator { 116 | fn build(schema: &Bound<'_, PyDict>, config: Option<&Bound<'_, PyDict>>) -> PyResult { 117 | let py = schema.py(); 118 | Ok(Self { 119 | strict: is_strict(schema, config)?, 120 | bytes_mode: ValBytesMode::from_config(config)?, 121 | min_length: schema.get_as(intern!(py, "min_length"))?, 122 | max_length: schema.get_as(intern!(py, "max_length"))?, 123 | } 124 | .into()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/validators/call.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyTypeError; 2 | use pyo3::intern; 3 | use pyo3::prelude::*; 4 | use pyo3::types::PyString; 5 | use pyo3::types::{PyDict, PyTuple}; 6 | 7 | use crate::errors::ValResult; 8 | use crate::input::Input; 9 | 10 | use crate::tools::SchemaDict; 11 | 12 | use super::validation_state::ValidationState; 13 | use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; 14 | 15 | #[derive(Debug)] 16 | pub struct CallValidator { 17 | function: PyObject, 18 | arguments_validator: Box, 19 | return_validator: Option>, 20 | name: String, 21 | } 22 | 23 | impl BuildValidator for CallValidator { 24 | const EXPECTED_TYPE: &'static str = "call"; 25 | 26 | fn build( 27 | schema: &Bound<'_, PyDict>, 28 | config: Option<&Bound<'_, PyDict>>, 29 | definitions: &mut DefinitionsBuilder, 30 | ) -> PyResult { 31 | let py = schema.py(); 32 | 33 | let arguments_schema = schema.get_as_req(intern!(py, "arguments_schema"))?; 34 | let arguments_validator = Box::new(build_validator(&arguments_schema, config, definitions)?); 35 | 36 | let return_schema = schema.get_item(intern!(py, "return_schema"))?; 37 | let return_validator = match return_schema { 38 | Some(return_schema) => Some(Box::new(build_validator(&return_schema, config, definitions)?)), 39 | None => None, 40 | }; 41 | let function: Bound<'_, PyAny> = schema.get_as_req(intern!(py, "function"))?; 42 | let function_name: Py = match schema.get_as(intern!(py, "function_name"))? { 43 | Some(name) => name, 44 | None => { 45 | match function.getattr(intern!(py, "__name__")) { 46 | Ok(name) => name.extract()?, 47 | Err(_) => { 48 | // partials we use `function.func.__name__` 49 | if let Ok(func) = function.getattr(intern!(py, "func")) { 50 | func.getattr(intern!(py, "__name__"))?.extract()? 51 | } else { 52 | intern!(py, "").clone().into() 53 | } 54 | } 55 | } 56 | } 57 | }; 58 | let function_name = function_name.bind(py); 59 | let name = format!("{}[{function_name}]", Self::EXPECTED_TYPE); 60 | 61 | Ok(Self { 62 | function: function.unbind(), 63 | arguments_validator, 64 | return_validator, 65 | name, 66 | } 67 | .into()) 68 | } 69 | } 70 | 71 | impl_py_gc_traverse!(CallValidator { 72 | function, 73 | arguments_validator, 74 | return_validator 75 | }); 76 | 77 | impl Validator for CallValidator { 78 | fn validate<'py>( 79 | &self, 80 | py: Python<'py>, 81 | input: &(impl Input<'py> + ?Sized), 82 | state: &mut ValidationState<'_, 'py>, 83 | ) -> ValResult { 84 | let args = self.arguments_validator.validate(py, input, state)?.into_bound(py); 85 | 86 | let return_value = if let Ok((args, kwargs)) = args.extract::<(Bound, Bound)>() { 87 | self.function.call(py, args, Some(&kwargs))? 88 | } else if let Ok(kwargs) = args.downcast::() { 89 | self.function.call(py, (), Some(kwargs))? 90 | } else { 91 | let msg = "Arguments validator should return a tuple of (args, kwargs) or a dict of kwargs"; 92 | return Err(PyTypeError::new_err(msg).into()); 93 | }; 94 | 95 | if let Some(return_validator) = &self.return_validator { 96 | return_validator 97 | .validate(py, return_value.bind(py), state) 98 | .map_err(|e| e.with_outer_location("return")) 99 | } else { 100 | Ok(return_value) 101 | } 102 | } 103 | 104 | fn get_name(&self) -> &str { 105 | &self.name 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/validators/callable.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::types::PyDict; 3 | 4 | use crate::errors::{ErrorTypeDefaults, ValError, ValResult}; 5 | use crate::input::Input; 6 | 7 | use super::validation_state::Exactness; 8 | use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct CallableValidator; 12 | 13 | impl BuildValidator for CallableValidator { 14 | const EXPECTED_TYPE: &'static str = "callable"; 15 | 16 | fn build( 17 | _schema: &Bound<'_, PyDict>, 18 | _config: Option<&Bound<'_, PyDict>>, 19 | _definitions: &mut DefinitionsBuilder, 20 | ) -> PyResult { 21 | Ok(Self.into()) 22 | } 23 | } 24 | 25 | impl_py_gc_traverse!(CallableValidator {}); 26 | 27 | impl Validator for CallableValidator { 28 | fn validate<'py>( 29 | &self, 30 | _py: Python<'py>, 31 | input: &(impl Input<'py> + ?Sized), 32 | state: &mut ValidationState<'_, 'py>, 33 | ) -> ValResult { 34 | state.floor_exactness(Exactness::Lax); 35 | if let Some(py_input) = input.as_python() { 36 | if py_input.is_callable() { 37 | return Ok(py_input.clone().unbind()); 38 | } 39 | } 40 | Err(ValError::new(ErrorTypeDefaults::CallableType, input)) 41 | } 42 | 43 | fn get_name(&self) -> &str { 44 | Self::EXPECTED_TYPE 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/validators/chain.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::{PyDict, PyList}; 4 | 5 | use crate::build_tools::py_schema_err; 6 | use crate::errors::ValResult; 7 | use crate::input::Input; 8 | use crate::tools::SchemaDict; 9 | 10 | use super::validation_state::ValidationState; 11 | use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; 12 | 13 | #[derive(Debug)] 14 | pub struct ChainValidator { 15 | steps: Vec, 16 | name: String, 17 | } 18 | 19 | impl BuildValidator for ChainValidator { 20 | const EXPECTED_TYPE: &'static str = "chain"; 21 | 22 | fn build( 23 | schema: &Bound<'_, PyDict>, 24 | config: Option<&Bound<'_, PyDict>>, 25 | definitions: &mut DefinitionsBuilder, 26 | ) -> PyResult { 27 | let steps: Vec = schema 28 | .get_as_req::>(intern!(schema.py(), "steps"))? 29 | .iter() 30 | .map(|step| build_validator_steps(&step, config, definitions)) 31 | .collect::>>>()? 32 | .into_iter() 33 | .flatten() 34 | .collect::>(); 35 | 36 | match steps.len() { 37 | 0 => py_schema_err!("One or more steps are required for a chain validator"), 38 | 1 => { 39 | let step = steps.into_iter().next().unwrap(); 40 | Ok(step) 41 | } 42 | _ => { 43 | let descr = steps.iter().map(Validator::get_name).collect::>().join(","); 44 | 45 | Ok(Self { 46 | steps, 47 | name: format!("{}[{descr}]", Self::EXPECTED_TYPE), 48 | } 49 | .into()) 50 | } 51 | } 52 | } 53 | } 54 | 55 | // either a vec of the steps from a nested `ChainValidator`, or a length-1 vec containing the validator 56 | // to be flattened into `steps` above 57 | fn build_validator_steps( 58 | step: &Bound<'_, PyAny>, 59 | config: Option<&Bound<'_, PyDict>>, 60 | definitions: &mut DefinitionsBuilder, 61 | ) -> PyResult> { 62 | let validator = build_validator(step, config, definitions)?; 63 | if let CombinedValidator::Chain(chain_validator) = validator { 64 | Ok(chain_validator.steps) 65 | } else { 66 | Ok(vec![validator]) 67 | } 68 | } 69 | 70 | impl_py_gc_traverse!(ChainValidator { steps }); 71 | 72 | impl Validator for ChainValidator { 73 | fn validate<'py>( 74 | &self, 75 | py: Python<'py>, 76 | input: &(impl Input<'py> + ?Sized), 77 | state: &mut ValidationState<'_, 'py>, 78 | ) -> ValResult { 79 | let mut steps_iter = self.steps.iter(); 80 | let first_step = steps_iter.next().unwrap(); 81 | let value = first_step.validate(py, input, state)?; 82 | 83 | steps_iter.try_fold(value, |v, step| step.validate(py, v.bind(py), state)) 84 | } 85 | 86 | fn get_name(&self) -> &str { 87 | &self.name 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/validators/complex.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyValueError; 2 | use pyo3::prelude::*; 3 | use pyo3::sync::GILOnceCell; 4 | use pyo3::types::{PyComplex, PyDict, PyString, PyType}; 5 | 6 | use crate::build_tools::is_strict; 7 | use crate::errors::{ErrorTypeDefaults, ToErrorValue, ValError, ValResult}; 8 | use crate::input::Input; 9 | 10 | use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; 11 | 12 | static COMPLEX_TYPE: GILOnceCell> = GILOnceCell::new(); 13 | 14 | pub fn get_complex_type(py: Python) -> &Bound<'_, PyType> { 15 | COMPLEX_TYPE 16 | .get_or_init(py, || py.get_type::().into()) 17 | .bind(py) 18 | } 19 | 20 | #[derive(Debug)] 21 | pub struct ComplexValidator { 22 | strict: bool, 23 | } 24 | 25 | impl BuildValidator for ComplexValidator { 26 | const EXPECTED_TYPE: &'static str = "complex"; 27 | fn build( 28 | schema: &Bound<'_, PyDict>, 29 | config: Option<&Bound<'_, PyDict>>, 30 | _definitions: &mut DefinitionsBuilder, 31 | ) -> PyResult { 32 | Ok(Self { 33 | strict: is_strict(schema, config)?, 34 | } 35 | .into()) 36 | } 37 | } 38 | 39 | impl_py_gc_traverse!(ComplexValidator {}); 40 | 41 | impl Validator for ComplexValidator { 42 | fn validate<'py>( 43 | &self, 44 | py: Python<'py>, 45 | input: &(impl Input<'py> + ?Sized), 46 | state: &mut ValidationState<'_, 'py>, 47 | ) -> ValResult { 48 | let res = input.validate_complex(self.strict, py)?.unpack(state); 49 | Ok(res.into_pyobject(py)?.into()) 50 | } 51 | 52 | fn get_name(&self) -> &'static str { 53 | "complex" 54 | } 55 | } 56 | 57 | pub(crate) fn string_to_complex<'py>( 58 | arg: &Bound<'py, PyString>, 59 | input: impl ToErrorValue, 60 | ) -> ValResult> { 61 | let py = arg.py(); 62 | Ok(get_complex_type(py) 63 | .call1((arg,)) 64 | .map_err(|err| { 65 | // Since arg is a string, the only possible error here is ValueError 66 | // triggered by invalid complex strings and thus only this case is handled. 67 | if err.is_instance_of::(py) { 68 | ValError::new(ErrorTypeDefaults::ComplexStrParsing, input) 69 | } else { 70 | ValError::InternalErr(err) 71 | } 72 | })? 73 | .downcast::()? 74 | .to_owned()) 75 | } 76 | -------------------------------------------------------------------------------- /src/validators/config.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::str::FromStr; 3 | 4 | use base64::engine::general_purpose::GeneralPurpose; 5 | use base64::engine::{DecodePaddingMode, GeneralPurposeConfig}; 6 | use base64::{alphabet, DecodeError, Engine}; 7 | use pyo3::types::{PyDict, PyString}; 8 | use pyo3::{intern, prelude::*}; 9 | 10 | use crate::errors::ErrorType; 11 | use crate::input::EitherBytes; 12 | use crate::serializers::BytesMode; 13 | use crate::tools::SchemaDict; 14 | 15 | const URL_SAFE_OPTIONAL_PADDING: GeneralPurpose = GeneralPurpose::new( 16 | &alphabet::URL_SAFE, 17 | GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent), 18 | ); 19 | const STANDARD_OPTIONAL_PADDING: GeneralPurpose = GeneralPurpose::new( 20 | &alphabet::STANDARD, 21 | GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent), 22 | ); 23 | 24 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] 25 | pub struct ValBytesMode { 26 | pub ser: BytesMode, 27 | } 28 | 29 | impl ValBytesMode { 30 | pub fn from_config(config: Option<&Bound<'_, PyDict>>) -> PyResult { 31 | let Some(config_dict) = config else { 32 | return Ok(Self::default()); 33 | }; 34 | let raw_mode = config_dict.get_as::>(intern!(config_dict.py(), "val_json_bytes"))?; 35 | let ser_mode = raw_mode.map_or_else(|| Ok(BytesMode::default()), |raw| BytesMode::from_str(&raw.to_cow()?))?; 36 | Ok(Self { ser: ser_mode }) 37 | } 38 | 39 | pub fn deserialize_string<'py>(self, s: &str) -> Result, ErrorType> { 40 | match self.ser { 41 | BytesMode::Utf8 => Ok(EitherBytes::Cow(Cow::Borrowed(s.as_bytes()))), 42 | BytesMode::Base64 => URL_SAFE_OPTIONAL_PADDING 43 | .decode(s) 44 | .or_else(|err| match err { 45 | DecodeError::InvalidByte(_, b'/' | b'+') => STANDARD_OPTIONAL_PADDING.decode(s), 46 | _ => Err(err), 47 | }) 48 | .map(EitherBytes::from) 49 | .map_err(|err| ErrorType::BytesInvalidEncoding { 50 | encoding: "base64".to_string(), 51 | encoding_error: err.to_string(), 52 | context: None, 53 | }), 54 | BytesMode::Hex => match hex::decode(s) { 55 | Ok(vec) => Ok(EitherBytes::from(vec)), 56 | Err(err) => Err(ErrorType::BytesInvalidEncoding { 57 | encoding: "hex".to_string(), 58 | encoding_error: err.to_string(), 59 | context: None, 60 | }), 61 | }, 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/validators/custom_error.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::PyDict; 4 | 5 | use crate::build_tools::py_schema_err; 6 | use crate::errors::ToErrorValue; 7 | use crate::errors::{ErrorType, PydanticCustomError, PydanticKnownError, ValError, ValResult}; 8 | use crate::input::Input; 9 | use crate::tools::SchemaDict; 10 | 11 | use super::validation_state::ValidationState; 12 | use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; 13 | 14 | #[derive(Debug, Clone)] 15 | pub enum CustomError { 16 | Custom(PydanticCustomError), 17 | KnownError(PydanticKnownError), 18 | } 19 | 20 | impl CustomError { 21 | pub fn build( 22 | schema: &Bound<'_, PyDict>, 23 | _config: Option<&Bound<'_, PyDict>>, 24 | _definitions: &mut DefinitionsBuilder, 25 | ) -> PyResult> { 26 | let py = schema.py(); 27 | let error_type: String = match schema.get_as(intern!(py, "custom_error_type"))? { 28 | Some(error_type) => error_type, 29 | None => return Ok(None), 30 | }; 31 | let context: Option> = schema.get_as(intern!(py, "custom_error_context"))?; 32 | 33 | if ErrorType::valid_type(py, &error_type) { 34 | if schema.contains(intern!(py, "custom_error_message"))? { 35 | py_schema_err!( 36 | "custom_error_message should not be provided if 'custom_error_type' matches a known error" 37 | ) 38 | } else { 39 | let error = PydanticKnownError::py_new(py, &error_type, context)?; 40 | Ok(Some(Self::KnownError(error))) 41 | } 42 | } else { 43 | let error = PydanticCustomError::py_new( 44 | error_type, 45 | schema.get_as_req::(intern!(py, "custom_error_message"))?, 46 | context, 47 | ); 48 | Ok(Some(Self::Custom(error))) 49 | } 50 | } 51 | 52 | pub fn as_val_error(&self, input: impl ToErrorValue) -> ValError { 53 | match self { 54 | CustomError::KnownError(ref known_error) => known_error.clone().into_val_error(input), 55 | CustomError::Custom(ref custom_error) => custom_error.clone().into_val_error(input), 56 | } 57 | } 58 | } 59 | 60 | #[derive(Debug)] 61 | pub struct CustomErrorValidator { 62 | validator: Box, 63 | custom_error: CustomError, 64 | name: String, 65 | } 66 | 67 | impl BuildValidator for CustomErrorValidator { 68 | const EXPECTED_TYPE: &'static str = "custom-error"; 69 | 70 | fn build( 71 | schema: &Bound<'_, PyDict>, 72 | config: Option<&Bound<'_, PyDict>>, 73 | definitions: &mut DefinitionsBuilder, 74 | ) -> PyResult { 75 | let custom_error = CustomError::build(schema, config, definitions)?.unwrap(); 76 | let schema = schema.get_as_req(intern!(schema.py(), "schema"))?; 77 | let validator = Box::new(build_validator(&schema, config, definitions)?); 78 | let name = format!("{}[{}]", Self::EXPECTED_TYPE, validator.get_name()); 79 | Ok(Self { 80 | validator, 81 | custom_error, 82 | name, 83 | } 84 | .into()) 85 | } 86 | } 87 | 88 | impl_py_gc_traverse!(CustomErrorValidator { validator }); 89 | 90 | impl Validator for CustomErrorValidator { 91 | fn validate<'py>( 92 | &self, 93 | py: Python<'py>, 94 | input: &(impl Input<'py> + ?Sized), 95 | state: &mut ValidationState<'_, 'py>, 96 | ) -> ValResult { 97 | self.validator 98 | .validate(py, input, state) 99 | .map_err(|_| self.custom_error.as_val_error(input)) 100 | } 101 | 102 | fn get_name(&self) -> &str { 103 | &self.name 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/validators/frozenset.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyDict, PyFrozenSet}; 2 | use pyo3::{prelude::*, IntoPyObjectExt}; 3 | 4 | use crate::errors::ValResult; 5 | use crate::input::{validate_iter_to_set, BorrowInput, ConsumeIterator, Input, ValidatedSet}; 6 | use crate::tools::SchemaDict; 7 | 8 | use super::list::min_length_check; 9 | use super::set::set_build; 10 | use super::validation_state::ValidationState; 11 | use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; 12 | 13 | #[derive(Debug)] 14 | pub struct FrozenSetValidator { 15 | strict: bool, 16 | item_validator: Box, 17 | min_length: Option, 18 | max_length: Option, 19 | name: String, 20 | fail_fast: bool, 21 | } 22 | 23 | impl BuildValidator for FrozenSetValidator { 24 | const EXPECTED_TYPE: &'static str = "frozenset"; 25 | set_build!(); 26 | } 27 | 28 | impl_py_gc_traverse!(FrozenSetValidator { item_validator }); 29 | 30 | impl Validator for FrozenSetValidator { 31 | fn validate<'py>( 32 | &self, 33 | py: Python<'py>, 34 | input: &(impl Input<'py> + ?Sized), 35 | state: &mut ValidationState<'_, 'py>, 36 | ) -> ValResult { 37 | let collection = input.validate_frozenset(state.strict_or(self.strict))?.unpack(state); 38 | let f_set = PyFrozenSet::empty(py)?; 39 | collection.iterate(ValidateToFrozenSet { 40 | py, 41 | input, 42 | f_set: &f_set, 43 | max_length: self.max_length, 44 | item_validator: &self.item_validator, 45 | state, 46 | fail_fast: self.fail_fast, 47 | })??; 48 | min_length_check!(input, "Frozenset", self.min_length, f_set); 49 | Ok(f_set.into_py_any(py)?) 50 | } 51 | 52 | fn get_name(&self) -> &str { 53 | &self.name 54 | } 55 | } 56 | 57 | struct ValidateToFrozenSet<'a, 's, 'py, I: Input<'py> + ?Sized> { 58 | py: Python<'py>, 59 | input: &'a I, 60 | f_set: &'a Bound<'py, PyFrozenSet>, 61 | max_length: Option, 62 | item_validator: &'a CombinedValidator, 63 | state: &'a mut ValidationState<'s, 'py>, 64 | fail_fast: bool, 65 | } 66 | 67 | impl<'py, T, I> ConsumeIterator> for ValidateToFrozenSet<'_, '_, 'py, I> 68 | where 69 | T: BorrowInput<'py>, 70 | I: Input<'py> + ?Sized, 71 | { 72 | type Output = ValResult<()>; 73 | fn consume_iterator(self, iterator: impl Iterator>) -> ValResult<()> { 74 | validate_iter_to_set( 75 | self.py, 76 | self.f_set, 77 | iterator, 78 | self.input, 79 | "Frozenset", 80 | self.max_length, 81 | self.item_validator, 82 | self.state, 83 | self.fail_fast, 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/validators/is_instance.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::{PyDict, PyType}; 4 | 5 | use crate::build_tools::py_schema_err; 6 | use crate::errors::{ErrorType, ValError, ValResult}; 7 | use crate::input::Input; 8 | use crate::tools::SchemaDict; 9 | 10 | use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct IsInstanceValidator { 14 | class: PyObject, 15 | class_repr: String, 16 | name: String, 17 | } 18 | 19 | impl BuildValidator for IsInstanceValidator { 20 | const EXPECTED_TYPE: &'static str = "is-instance"; 21 | 22 | fn build( 23 | schema: &Bound<'_, PyDict>, 24 | _config: Option<&Bound<'_, PyDict>>, 25 | _definitions: &mut DefinitionsBuilder, 26 | ) -> PyResult { 27 | let py = schema.py(); 28 | let cls_key = intern!(py, "cls"); 29 | let class = schema.get_as_req(cls_key)?; 30 | 31 | // test that class works with isinstance to avoid errors at call time, reuse cls_key since it doesn't 32 | // matter what object is being checked 33 | if cls_key.is_instance(&class).is_err() { 34 | return py_schema_err!("'cls' must be valid as the first argument to 'isinstance'"); 35 | } 36 | 37 | let class_repr = class_repr(schema, &class)?; 38 | let name = format!("{}[{class_repr}]", Self::EXPECTED_TYPE); 39 | Ok(Self { 40 | class: class.into(), 41 | class_repr, 42 | name, 43 | } 44 | .into()) 45 | } 46 | } 47 | 48 | impl_py_gc_traverse!(IsInstanceValidator { class }); 49 | 50 | impl Validator for IsInstanceValidator { 51 | fn validate<'py>( 52 | &self, 53 | py: Python<'py>, 54 | input: &(impl Input<'py> + ?Sized), 55 | _state: &mut ValidationState<'_, 'py>, 56 | ) -> ValResult { 57 | let Some(obj) = input.as_python() else { 58 | let method_name = "isinstance".to_string(); 59 | return Err(ValError::new( 60 | ErrorType::NeedsPythonObject { 61 | context: None, 62 | method_name, 63 | }, 64 | input, 65 | )); 66 | }; 67 | match obj.is_instance(self.class.bind(py))? { 68 | true => Ok(obj.clone().unbind()), 69 | false => Err(ValError::new( 70 | ErrorType::IsInstanceOf { 71 | class: self.class_repr.clone(), 72 | context: None, 73 | }, 74 | input, 75 | )), 76 | } 77 | } 78 | 79 | fn get_name(&self) -> &str { 80 | &self.name 81 | } 82 | } 83 | 84 | pub fn class_repr(schema: &Bound<'_, PyDict>, class: &Bound<'_, PyAny>) -> PyResult { 85 | match schema.get_as(intern!(schema.py(), "cls_repr"))? { 86 | Some(s) => Ok(s), 87 | None => match class.downcast::() { 88 | Ok(t) => Ok(t.qualname()?.to_string()), 89 | Err(_) => Ok(class.repr()?.extract()?), 90 | }, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/validators/is_subclass.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::{PyDict, PyType}; 4 | 5 | use crate::errors::{ErrorType, ValError, ValResult}; 6 | use crate::input::Input; 7 | use crate::tools::SchemaDict; 8 | 9 | use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct IsSubclassValidator { 13 | class: Py, 14 | class_repr: String, 15 | name: String, 16 | } 17 | 18 | impl BuildValidator for IsSubclassValidator { 19 | const EXPECTED_TYPE: &'static str = "is-subclass"; 20 | 21 | fn build( 22 | schema: &Bound<'_, PyDict>, 23 | _config: Option<&Bound<'_, PyDict>>, 24 | _definitions: &mut DefinitionsBuilder, 25 | ) -> PyResult { 26 | let py = schema.py(); 27 | let class = schema.get_as_req::>(intern!(py, "cls"))?; 28 | 29 | let class_repr = match schema.get_as(intern!(py, "cls_repr"))? { 30 | Some(s) => s, 31 | None => class.qualname()?.to_string(), 32 | }; 33 | let name = format!("{}[{class_repr}]", Self::EXPECTED_TYPE); 34 | Ok(Self { 35 | class: class.into(), 36 | class_repr, 37 | name, 38 | } 39 | .into()) 40 | } 41 | } 42 | 43 | impl_py_gc_traverse!(IsSubclassValidator { class }); 44 | 45 | impl Validator for IsSubclassValidator { 46 | fn validate<'py>( 47 | &self, 48 | py: Python<'py>, 49 | input: &(impl Input<'py> + ?Sized), 50 | _state: &mut ValidationState<'_, 'py>, 51 | ) -> ValResult { 52 | let Some(obj) = input.as_python() else { 53 | let method_name = "issubclass".to_string(); 54 | return Err(ValError::new( 55 | ErrorType::NeedsPythonObject { 56 | context: None, 57 | method_name, 58 | }, 59 | input, 60 | )); 61 | }; 62 | match obj.downcast::() { 63 | Ok(py_type) if py_type.is_subclass(self.class.bind(py))? => Ok(obj.clone().unbind()), 64 | _ => Err(ValError::new( 65 | ErrorType::IsSubclassOf { 66 | class: self.class_repr.clone(), 67 | context: None, 68 | }, 69 | input, 70 | )), 71 | } 72 | } 73 | 74 | fn get_name(&self) -> &str { 75 | &self.name 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/validators/json_or_python.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::PyDict; 4 | 5 | use crate::definitions::DefinitionsBuilder; 6 | use crate::errors::ValResult; 7 | use crate::input::Input; 8 | use crate::tools::SchemaDict; 9 | 10 | use super::{build_validator, BuildValidator, CombinedValidator, InputType, ValidationState, Validator}; 11 | 12 | #[derive(Debug)] 13 | pub struct JsonOrPython { 14 | json: Box, 15 | python: Box, 16 | name: String, 17 | } 18 | 19 | impl BuildValidator for JsonOrPython { 20 | const EXPECTED_TYPE: &'static str = "json-or-python"; 21 | 22 | fn build( 23 | schema: &Bound<'_, PyDict>, 24 | config: Option<&Bound<'_, PyDict>>, 25 | definitions: &mut DefinitionsBuilder, 26 | ) -> PyResult { 27 | let py = schema.py(); 28 | let json_schema = schema.get_as_req(intern!(py, "json_schema"))?; 29 | let python_schema = schema.get_as_req(intern!(py, "python_schema"))?; 30 | 31 | let json = build_validator(&json_schema, config, definitions)?; 32 | let python = build_validator(&python_schema, config, definitions)?; 33 | 34 | let name = format!( 35 | "{}[json={},python={}]", 36 | Self::EXPECTED_TYPE, 37 | json.get_name(), 38 | python.get_name(), 39 | ); 40 | Ok(Self { 41 | json: Box::new(json), 42 | python: Box::new(python), 43 | name, 44 | } 45 | .into()) 46 | } 47 | } 48 | 49 | impl_py_gc_traverse!(JsonOrPython { json, python }); 50 | 51 | impl Validator for JsonOrPython { 52 | fn validate<'py>( 53 | &self, 54 | py: Python<'py>, 55 | input: &(impl Input<'py> + ?Sized), 56 | state: &mut ValidationState<'_, 'py>, 57 | ) -> ValResult { 58 | match state.extra().input_type { 59 | InputType::Python => self.python.validate(py, input, state), 60 | _ => self.json.validate(py, input, state), 61 | } 62 | } 63 | 64 | fn get_name(&self) -> &str { 65 | &self.name 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/validators/lax_or_strict.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::PyDict; 4 | 5 | use crate::build_tools::is_strict; 6 | use crate::errors::ValResult; 7 | use crate::input::Input; 8 | use crate::tools::SchemaDict; 9 | 10 | use super::Exactness; 11 | use super::ValidationState; 12 | use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; 13 | 14 | #[derive(Debug)] 15 | pub struct LaxOrStrictValidator { 16 | strict: bool, 17 | lax_validator: Box, 18 | strict_validator: Box, 19 | name: String, 20 | } 21 | 22 | impl BuildValidator for LaxOrStrictValidator { 23 | const EXPECTED_TYPE: &'static str = "lax-or-strict"; 24 | 25 | fn build( 26 | schema: &Bound<'_, PyDict>, 27 | config: Option<&Bound<'_, PyDict>>, 28 | definitions: &mut DefinitionsBuilder, 29 | ) -> PyResult { 30 | let py = schema.py(); 31 | let lax_schema = schema.get_as_req(intern!(py, "lax_schema"))?; 32 | let lax_validator = Box::new(build_validator(&lax_schema, config, definitions)?); 33 | 34 | let strict_schema = schema.get_as_req(intern!(py, "strict_schema"))?; 35 | let strict_validator = Box::new(build_validator(&strict_schema, config, definitions)?); 36 | 37 | let name = format!( 38 | "{}[lax={},strict={}]", 39 | Self::EXPECTED_TYPE, 40 | lax_validator.get_name(), 41 | strict_validator.get_name() 42 | ); 43 | Ok(Self { 44 | strict: is_strict(schema, config)?, 45 | lax_validator, 46 | strict_validator, 47 | name, 48 | } 49 | .into()) 50 | } 51 | } 52 | 53 | impl_py_gc_traverse!(LaxOrStrictValidator { 54 | lax_validator, 55 | strict_validator 56 | }); 57 | 58 | impl Validator for LaxOrStrictValidator { 59 | fn validate<'py>( 60 | &self, 61 | py: Python<'py>, 62 | input: &(impl Input<'py> + ?Sized), 63 | state: &mut ValidationState<'_, 'py>, 64 | ) -> ValResult { 65 | if state.strict_or(self.strict) { 66 | self.strict_validator.validate(py, input, state) 67 | } else { 68 | // horrible edge case: if doing smart union validation, we need to try the strict validator 69 | // anyway and prefer that if it succeeds 70 | if state.exactness.is_some() { 71 | if let Ok(strict_result) = self.strict_validator.validate(py, input, state) { 72 | return Ok(strict_result); 73 | } 74 | // this is now known to be not strict 75 | state.floor_exactness(Exactness::Lax); 76 | } 77 | self.lax_validator.validate(py, input, state) 78 | } 79 | } 80 | 81 | fn get_name(&self) -> &str { 82 | &self.name 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/validators/none.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::types::PyDict; 3 | 4 | use crate::errors::{ErrorTypeDefaults, ValError, ValResult}; 5 | use crate::input::Input; 6 | 7 | use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct NoneValidator; 11 | 12 | impl BuildValidator for NoneValidator { 13 | const EXPECTED_TYPE: &'static str = "none"; 14 | 15 | fn build( 16 | _schema: &Bound<'_, PyDict>, 17 | _config: Option<&Bound<'_, PyDict>>, 18 | _definitions: &mut DefinitionsBuilder, 19 | ) -> PyResult { 20 | Ok(Self.into()) 21 | } 22 | } 23 | 24 | impl_py_gc_traverse!(NoneValidator {}); 25 | 26 | impl Validator for NoneValidator { 27 | fn validate<'py>( 28 | &self, 29 | py: Python<'py>, 30 | input: &(impl Input<'py> + ?Sized), 31 | _state: &mut ValidationState<'_, 'py>, 32 | ) -> ValResult { 33 | match input.is_none() { 34 | true => Ok(py.None()), 35 | false => Err(ValError::new(ErrorTypeDefaults::NoneRequired, input)), 36 | } 37 | } 38 | 39 | fn get_name(&self) -> &str { 40 | Self::EXPECTED_TYPE 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/validators/nullable.rs: -------------------------------------------------------------------------------- 1 | use pyo3::intern; 2 | use pyo3::prelude::*; 3 | use pyo3::types::PyDict; 4 | 5 | use crate::errors::ValResult; 6 | use crate::input::Input; 7 | use crate::tools::SchemaDict; 8 | 9 | use super::ValidationState; 10 | use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; 11 | 12 | #[derive(Debug)] 13 | pub struct NullableValidator { 14 | validator: Box, 15 | name: String, 16 | } 17 | 18 | impl BuildValidator for NullableValidator { 19 | const EXPECTED_TYPE: &'static str = "nullable"; 20 | 21 | fn build( 22 | schema: &Bound<'_, PyDict>, 23 | config: Option<&Bound<'_, PyDict>>, 24 | definitions: &mut DefinitionsBuilder, 25 | ) -> PyResult { 26 | let schema = schema.get_as_req(intern!(schema.py(), "schema"))?; 27 | let validator = Box::new(build_validator(&schema, config, definitions)?); 28 | let name = format!("{}[{}]", Self::EXPECTED_TYPE, validator.get_name()); 29 | Ok(Self { validator, name }.into()) 30 | } 31 | } 32 | 33 | impl_py_gc_traverse!(NullableValidator { validator }); 34 | 35 | impl Validator for NullableValidator { 36 | fn validate<'py>( 37 | &self, 38 | py: Python<'py>, 39 | input: &(impl Input<'py> + ?Sized), 40 | state: &mut ValidationState<'_, 'py>, 41 | ) -> ValResult { 42 | match input.is_none() { 43 | true => Ok(py.None()), 44 | false => self.validator.validate(py, input, state), 45 | } 46 | } 47 | 48 | fn get_name(&self) -> &str { 49 | &self.name 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/validators/prebuilt.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::types::PyDict; 3 | 4 | use crate::common::prebuilt::get_prebuilt; 5 | use crate::errors::ValResult; 6 | use crate::input::Input; 7 | 8 | use super::ValidationState; 9 | use super::{CombinedValidator, SchemaValidator, Validator}; 10 | 11 | #[derive(Debug)] 12 | pub struct PrebuiltValidator { 13 | schema_validator: Py, 14 | } 15 | 16 | impl PrebuiltValidator { 17 | pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult> { 18 | get_prebuilt(type_, schema, "__pydantic_validator__", |py_any| { 19 | let schema_validator = py_any.extract::>()?; 20 | if matches!( 21 | schema_validator.get().validator, 22 | CombinedValidator::FunctionWrap(_) | CombinedValidator::FunctionAfter(_) 23 | ) { 24 | return Ok(None); 25 | } 26 | Ok(Some(Self { schema_validator }.into())) 27 | }) 28 | } 29 | } 30 | 31 | impl_py_gc_traverse!(PrebuiltValidator { schema_validator }); 32 | 33 | impl Validator for PrebuiltValidator { 34 | fn validate<'py>( 35 | &self, 36 | py: Python<'py>, 37 | input: &(impl Input<'py> + ?Sized), 38 | state: &mut ValidationState<'_, 'py>, 39 | ) -> ValResult { 40 | self.schema_validator.get().validator.validate(py, input, state) 41 | } 42 | 43 | fn get_name(&self) -> &str { 44 | self.schema_validator.get().validator.get_name() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/validators/set.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyDict, PySet}; 2 | use pyo3::{prelude::*, IntoPyObjectExt}; 3 | 4 | use crate::errors::ValResult; 5 | use crate::input::{validate_iter_to_set, BorrowInput, ConsumeIterator, Input, ValidatedSet}; 6 | use crate::tools::SchemaDict; 7 | 8 | use super::list::min_length_check; 9 | use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; 10 | 11 | #[derive(Debug)] 12 | pub struct SetValidator { 13 | strict: bool, 14 | item_validator: Box, 15 | min_length: Option, 16 | max_length: Option, 17 | name: String, 18 | fail_fast: bool, 19 | } 20 | 21 | macro_rules! set_build { 22 | () => { 23 | fn build( 24 | schema: &Bound<'_, PyDict>, 25 | config: Option<&Bound<'_, PyDict>>, 26 | definitions: &mut DefinitionsBuilder, 27 | ) -> PyResult { 28 | let py = schema.py(); 29 | let item_validator = match schema.get_item(pyo3::intern!(schema.py(), "items_schema"))? { 30 | Some(d) => Box::new(crate::validators::build_validator(&d, config, definitions)?), 31 | None => Box::new(crate::validators::any::AnyValidator::build( 32 | schema, 33 | config, 34 | definitions, 35 | )?), 36 | }; 37 | let inner_name = item_validator.get_name(); 38 | let max_length = schema.get_as(pyo3::intern!(py, "max_length"))?; 39 | let name = format!("{}[{}]", Self::EXPECTED_TYPE, inner_name); 40 | Ok(Self { 41 | strict: crate::build_tools::is_strict(schema, config)?, 42 | item_validator, 43 | min_length: schema.get_as(pyo3::intern!(py, "min_length"))?, 44 | max_length, 45 | name, 46 | fail_fast: schema.get_as(pyo3::intern!(py, "fail_fast"))?.unwrap_or(false), 47 | } 48 | .into()) 49 | } 50 | }; 51 | } 52 | pub(crate) use set_build; 53 | 54 | impl BuildValidator for SetValidator { 55 | const EXPECTED_TYPE: &'static str = "set"; 56 | set_build!(); 57 | } 58 | 59 | impl_py_gc_traverse!(SetValidator { item_validator }); 60 | 61 | impl Validator for SetValidator { 62 | fn validate<'py>( 63 | &self, 64 | py: Python<'py>, 65 | input: &(impl Input<'py> + ?Sized), 66 | state: &mut ValidationState<'_, 'py>, 67 | ) -> ValResult { 68 | let collection = input.validate_set(state.strict_or(self.strict))?.unpack(state); 69 | let set = PySet::empty(py)?; 70 | collection.iterate(ValidateToSet { 71 | py, 72 | input, 73 | set: &set, 74 | max_length: self.max_length, 75 | item_validator: &self.item_validator, 76 | state, 77 | fail_fast: self.fail_fast, 78 | })??; 79 | min_length_check!(input, "Set", self.min_length, set); 80 | Ok(set.into_py_any(py)?) 81 | } 82 | 83 | fn get_name(&self) -> &str { 84 | &self.name 85 | } 86 | } 87 | 88 | struct ValidateToSet<'a, 's, 'py, I: Input<'py> + ?Sized> { 89 | py: Python<'py>, 90 | input: &'a I, 91 | set: &'a Bound<'py, PySet>, 92 | max_length: Option, 93 | item_validator: &'a CombinedValidator, 94 | state: &'a mut ValidationState<'s, 'py>, 95 | fail_fast: bool, 96 | } 97 | 98 | impl<'py, T, I> ConsumeIterator> for ValidateToSet<'_, '_, 'py, I> 99 | where 100 | T: BorrowInput<'py>, 101 | I: Input<'py> + ?Sized, 102 | { 103 | type Output = ValResult<()>; 104 | fn consume_iterator(self, iterator: impl Iterator>) -> ValResult<()> { 105 | validate_iter_to_set( 106 | self.py, 107 | self.set, 108 | iterator, 109 | self.input, 110 | "Set", 111 | self.max_length, 112 | self.item_validator, 113 | self.state, 114 | self.fail_fast, 115 | ) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydantic/pydantic-core/52e9a5309580bf53b9d85e069f956dcfca84998f/tests/__init__.py -------------------------------------------------------------------------------- /tests/benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydantic/pydantic-core/52e9a5309580bf53b9d85e069f956dcfca84998f/tests/benchmarks/__init__.py -------------------------------------------------------------------------------- /tests/benchmarks/nested_schema.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any 4 | 5 | if TYPE_CHECKING: 6 | from pydantic_core import core_schema as cs 7 | 8 | N = 5 # arbitrary number that takes ~0.05s per run 9 | 10 | 11 | class MyModel: 12 | # __slots__ is not required, but it avoids __pydantic_fields_set__ falling into __dict__ 13 | __slots__ = '__dict__', '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__' 14 | 15 | 16 | def schema_using_defs() -> cs.CoreSchema: 17 | definitions: list[cs.CoreSchema] = [ 18 | {'type': 'int', 'ref': 'int'}, 19 | { 20 | 'type': 'model', 21 | 'cls': MyModel, 22 | 'schema': { 23 | 'type': 'model-fields', 24 | 'fields': { 25 | str(c): {'type': 'model-field', 'schema': {'type': 'definition-ref', 'schema_ref': 'int'}} 26 | for c in range(N) 27 | }, 28 | }, 29 | 'ref': f'model_{N}', 30 | }, 31 | ] 32 | level = N 33 | for level in reversed(range(N)): 34 | definitions.append( 35 | { 36 | 'type': 'model', 37 | 'cls': MyModel, 38 | 'schema': { 39 | 'type': 'model-fields', 40 | 'fields': { 41 | str(c): { 42 | 'type': 'model-field', 43 | 'schema': {'type': 'definition-ref', 'schema_ref': f'model_{level + 1}'}, 44 | } 45 | for c in range(N) 46 | }, 47 | }, 48 | 'ref': f'model_{level}', 49 | } 50 | ) 51 | return { 52 | 'type': 'definitions', 53 | 'definitions': definitions, 54 | 'schema': {'type': 'definition-ref', 'schema_ref': 'model_0'}, 55 | } 56 | 57 | 58 | def inlined_schema() -> cs.CoreSchema: 59 | level = N 60 | schema: cs.CoreSchema = { 61 | 'type': 'model', 62 | 'cls': MyModel, 63 | 'schema': { 64 | 'type': 'model-fields', 65 | 'fields': {str(c): {'type': 'model-field', 'schema': {'type': 'int'}} for c in range(N)}, 66 | }, 67 | 'ref': f'model_{N}', 68 | } 69 | for level in reversed(range(N)): 70 | schema = { 71 | 'type': 'model', 72 | 'cls': MyModel, 73 | 'schema': { 74 | 'type': 'model-fields', 75 | 'fields': {str(c): {'type': 'model-field', 'schema': schema} for c in range(N)}, 76 | }, 77 | 'ref': f'model_{level}', 78 | } 79 | return schema 80 | 81 | 82 | def input_data_valid(levels: int = N) -> Any: 83 | data = {str(c): 1 for c in range(N)} 84 | for _ in range(levels): 85 | data = {str(c): data for c in range(N)} 86 | return data 87 | 88 | 89 | if __name__ == '__main__': 90 | from pydantic_core import SchemaValidator 91 | 92 | SchemaValidator(schema_using_defs()).validate_python(input_data_valid()) 93 | SchemaValidator(inlined_schema()).validate_python(input_data_valid()) 94 | -------------------------------------------------------------------------------- /tests/benchmarks/test_nested_benchmark.py: -------------------------------------------------------------------------------- 1 | """ 2 | Benchmarks for nested / recursive schemas using definitions. 3 | """ 4 | 5 | from typing import Callable 6 | 7 | from pydantic_core import SchemaValidator 8 | 9 | from .nested_schema import inlined_schema, input_data_valid, schema_using_defs 10 | 11 | 12 | def test_nested_schema_using_defs(benchmark: Callable[..., None]) -> None: 13 | v = SchemaValidator(schema_using_defs()) 14 | data = input_data_valid() 15 | v.validate_python(data) 16 | benchmark(v.validate_python, data) 17 | 18 | 19 | def test_nested_schema_inlined(benchmark: Callable[..., None]) -> None: 20 | v = SchemaValidator(inlined_schema()) 21 | data = input_data_valid() 22 | v.validate_python(data) 23 | benchmark(v.validate_python, data) 24 | -------------------------------------------------------------------------------- /tests/emscripten_runner.js: -------------------------------------------------------------------------------- 1 | const {opendir} = require('node:fs/promises'); 2 | const {loadPyodide} = require('pyodide'); 3 | const path = require('path'); 4 | 5 | async function find_wheel(dist_dir) { 6 | const dir = await opendir(dist_dir); 7 | for await (const dirent of dir) { 8 | if (dirent.name.endsWith('.whl')) { 9 | return path.join(dist_dir, dirent.name); 10 | } 11 | } 12 | } 13 | 14 | function make_tty_ops(stream) { 15 | return { 16 | // get_char has 3 particular return values: 17 | // a.) the next character represented as an integer 18 | // b.) undefined to signal that no data is currently available 19 | // c.) null to signal an EOF 20 | get_char(tty) { 21 | if (!tty.input.length) { 22 | let result = null; 23 | const BUFSIZE = 256; 24 | const buf = Buffer.alloc(BUFSIZE); 25 | const bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUFSIZE, -1); 26 | if (bytesRead === 0) { 27 | return null; 28 | } 29 | result = buf.slice(0, bytesRead); 30 | tty.input = Array.from(result); 31 | } 32 | return tty.input.shift(); 33 | }, 34 | put_char(tty, val) { 35 | try { 36 | if (val !== null) { 37 | tty.output.push(val); 38 | } 39 | if (val === null || val === 10) { 40 | process.stdout.write(Buffer.from(tty.output)); 41 | tty.output = []; 42 | } 43 | } catch (e) { 44 | console.warn(e); 45 | } 46 | }, 47 | fsync(tty) { 48 | if (!tty.output || tty.output.length === 0) { 49 | return; 50 | } 51 | stream.write(Buffer.from(tty.output)); 52 | tty.output = []; 53 | }, 54 | }; 55 | } 56 | 57 | function setupStreams(FS, TTY) { 58 | let mytty = FS.makedev(FS.createDevice.major++, 0); 59 | let myttyerr = FS.makedev(FS.createDevice.major++, 0); 60 | TTY.register(mytty, make_tty_ops(process.stdout)); 61 | TTY.register(myttyerr, make_tty_ops(process.stderr)); 62 | FS.mkdev('/dev/mytty', mytty); 63 | FS.mkdev('/dev/myttyerr', myttyerr); 64 | FS.unlink('/dev/stdin'); 65 | FS.unlink('/dev/stdout'); 66 | FS.unlink('/dev/stderr'); 67 | FS.symlink('/dev/mytty', '/dev/stdin'); 68 | FS.symlink('/dev/mytty', '/dev/stdout'); 69 | FS.symlink('/dev/myttyerr', '/dev/stderr'); 70 | FS.closeStream(0); 71 | FS.closeStream(1); 72 | FS.closeStream(2); 73 | FS.open('/dev/stdin', 0); 74 | FS.open('/dev/stdout', 1); 75 | FS.open('/dev/stderr', 1); 76 | } 77 | 78 | async function main() { 79 | const root_dir = path.resolve(__dirname, '..'); 80 | const wheel_path = await find_wheel(path.join(root_dir, 'dist')); 81 | let errcode = 1; 82 | try { 83 | const pyodide = await loadPyodide(); 84 | const FS = pyodide.FS; 85 | setupStreams(FS, pyodide._module.TTY); 86 | FS.mkdir('/test_dir'); 87 | FS.mount(FS.filesystems.NODEFS, {root: path.join(root_dir, 'tests')}, '/test_dir'); 88 | FS.chdir('/test_dir'); 89 | await pyodide.loadPackage(['micropip', 'pytest', 'pygments']); 90 | // language=python 91 | errcode = await pyodide.runPythonAsync(` 92 | import micropip 93 | import importlib 94 | 95 | # ugly hack to get tests to work on arm64 (my m1 mac) 96 | # see https://github.com/pyodide/pyodide/issues/2840 97 | # import sys; sys.setrecursionlimit(200) 98 | 99 | await micropip.install([ 100 | 'dirty-equals', 101 | # inline-snapshot 0.21 requires pytest 8.3.4, pyodide 0.26 ships with 8.1.1 102 | 'inline-snapshot < 0.21', 103 | 'hypothesis', 104 | 'pytest-speed', 105 | 'pytest-mock', 106 | 'tzdata', 107 | 'file:${wheel_path}', 108 | 'typing-extensions', 109 | 'typing-inspection', 110 | ]) 111 | importlib.invalidate_caches() 112 | 113 | print('installed packages:', micropip.list()) 114 | 115 | import pytest 116 | pytest.main() 117 | `); 118 | } catch (e) { 119 | console.error(e); 120 | process.exit(1); 121 | } 122 | process.exit(errcode); 123 | } 124 | 125 | main(); 126 | -------------------------------------------------------------------------------- /tests/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydantic/pydantic-core/52e9a5309580bf53b9d85e069f956dcfca84998f/tests/serializers/__init__.py -------------------------------------------------------------------------------- /tests/serializers/test_complex.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import pytest 4 | 5 | from pydantic_core import SchemaSerializer, core_schema 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 'value,expected', 10 | [ 11 | (complex(-1.23e-4, 567.89), '-0.000123+567.89j'), 12 | (complex(0, -1.23), '-1.23j'), 13 | (complex(1.5, 0), '1.5+0j'), 14 | (complex(1, 2), '1+2j'), 15 | (complex(0, 1), '1j'), 16 | (complex(0, 1e-500), '0j'), 17 | (complex(-float('inf'), 2), '-inf+2j'), 18 | (complex(float('inf'), 2), 'inf+2j'), 19 | (complex(float('nan'), 2), 'NaN+2j'), 20 | ], 21 | ) 22 | def test_complex_json(value, expected): 23 | v = SchemaSerializer(core_schema.complex_schema()) 24 | c = v.to_python(value) 25 | c_json = v.to_python(value, mode='json') 26 | json_str = v.to_json(value).decode() 27 | 28 | assert c_json == expected 29 | assert json_str == f'"{expected}"' 30 | 31 | if math.isnan(value.imag): 32 | assert math.isnan(c.imag) 33 | else: 34 | assert c.imag == value.imag 35 | 36 | if math.isnan(value.real): 37 | assert math.isnan(c.real) 38 | else: 39 | assert c.imag == value.imag 40 | 41 | 42 | def test_complex_inference() -> None: 43 | s = SchemaSerializer(core_schema.any_schema()) 44 | assert s.to_python(1 + 2j) == 1 + 2j 45 | assert s.to_json(1 + 2j) == b'"1+2j"' 46 | -------------------------------------------------------------------------------- /tests/serializers/test_decimal.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | import pytest 4 | 5 | from pydantic_core import SchemaSerializer, core_schema 6 | 7 | 8 | def test_decimal(): 9 | v = SchemaSerializer(core_schema.decimal_schema()) 10 | assert v.to_python(Decimal('123.456')) == Decimal('123.456') 11 | 12 | assert v.to_python(Decimal('123.456'), mode='json') == '123.456' 13 | assert v.to_json(Decimal('123.456')) == b'"123.456"' 14 | 15 | assert v.to_python(Decimal('123456789123456789123456789.123456789123456789123456789')) == Decimal( 16 | '123456789123456789123456789.123456789123456789123456789' 17 | ) 18 | assert ( 19 | v.to_json(Decimal('123456789123456789123456789.123456789123456789123456789')) 20 | == b'"123456789123456789123456789.123456789123456789123456789"' 21 | ) 22 | 23 | with pytest.warns( 24 | UserWarning, 25 | match=r'Expected `decimal` - serialized value may not be as expected \[input_value=123, input_type=int\]', 26 | ): 27 | assert v.to_python(123, mode='json') == 123 28 | 29 | with pytest.warns( 30 | UserWarning, 31 | match=r'Expected `decimal` - serialized value may not be as expected \[input_value=123, input_type=int\]', 32 | ): 33 | assert v.to_json(123) == b'123' 34 | 35 | 36 | def test_decimal_key(): 37 | v = SchemaSerializer(core_schema.dict_schema(core_schema.decimal_schema(), core_schema.decimal_schema())) 38 | assert v.to_python({Decimal('123.456'): Decimal('123.456')}) == {Decimal('123.456'): Decimal('123.456')} 39 | assert v.to_python({Decimal('123.456'): Decimal('123.456')}, mode='json') == {'123.456': '123.456'} 40 | assert v.to_json({Decimal('123.456'): Decimal('123.456')}) == b'{"123.456":"123.456"}' 41 | 42 | 43 | @pytest.mark.parametrize( 44 | 'value,expected', 45 | [ 46 | (Decimal('123.456'), '123.456'), 47 | (Decimal('Infinity'), 'Infinity'), 48 | (Decimal('-Infinity'), '-Infinity'), 49 | (Decimal('NaN'), 'NaN'), 50 | ], 51 | ) 52 | def test_decimal_json(value, expected): 53 | v = SchemaSerializer(core_schema.decimal_schema()) 54 | assert v.to_python(value, mode='json') == expected 55 | assert v.to_json(value).decode() == f'"{expected}"' 56 | 57 | 58 | def test_any_decimal_key(): 59 | v = SchemaSerializer(core_schema.dict_schema()) 60 | input_value = {Decimal('123.456'): 1} 61 | 62 | assert v.to_python(input_value, mode='json') == {'123.456': 1} 63 | assert v.to_json(input_value) == b'{"123.456":1}' 64 | -------------------------------------------------------------------------------- /tests/serializers/test_infer.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from pydantic_core import SchemaSerializer, core_schema 4 | 5 | 6 | # serializing enum calls methods in serializers::infer 7 | def test_infer_to_python(): 8 | class MyEnum(Enum): 9 | complex_ = complex(1, 2) 10 | 11 | v = SchemaSerializer(core_schema.enum_schema(MyEnum, list(MyEnum.__members__.values()))) 12 | assert v.to_python(MyEnum.complex_, mode='json') == '1+2j' 13 | 14 | 15 | def test_infer_serialize(): 16 | class MyEnum(Enum): 17 | complex_ = complex(1, 2) 18 | 19 | v = SchemaSerializer(core_schema.enum_schema(MyEnum, list(MyEnum.__members__.values()))) 20 | assert v.to_json(MyEnum.complex_) == b'"1+2j"' 21 | 22 | 23 | def test_infer_json_key(): 24 | class MyEnum(Enum): 25 | complex_ = {complex(1, 2): 1} 26 | 27 | v = SchemaSerializer(core_schema.enum_schema(MyEnum, list(MyEnum.__members__.values()))) 28 | assert v.to_json(MyEnum.complex_) == b'{"1+2j":1}' 29 | -------------------------------------------------------------------------------- /tests/serializers/test_json.py: -------------------------------------------------------------------------------- 1 | from pydantic_core import SchemaSerializer, core_schema 2 | 3 | 4 | def test_json_int(): 5 | s = SchemaSerializer(core_schema.json_schema(core_schema.int_schema())) 6 | 7 | assert s.to_python(1) == 1 8 | assert s.to_python(1, round_trip=True) == '1' 9 | assert s.to_python(1, mode='json') == 1 10 | assert s.to_python(1, mode='json', round_trip=True) == '1' 11 | assert s.to_json(1) == b'1' 12 | assert s.to_json(1, round_trip=True) == b'"1"' 13 | 14 | 15 | def test_list_json(): 16 | s = SchemaSerializer(core_schema.list_schema(core_schema.json_schema())) 17 | 18 | v = ['a', [1, 2], None] 19 | assert s.to_python(v) == v 20 | assert s.to_python(v, round_trip=True) == ['"a"', '[1,2]', 'null'] 21 | assert s.to_python(v, mode='json') == v 22 | assert s.to_python(v, mode='json', round_trip=True) == ['"a"', '[1,2]', 'null'] 23 | assert s.to_json(v) == b'["a",[1,2],null]' 24 | assert s.to_json(v, round_trip=True) == b'["\\"a\\"","[1,2]","null"]' 25 | 26 | 27 | def test_dict_key_json(): 28 | s = SchemaSerializer(core_schema.dict_schema(core_schema.json_schema(), core_schema.any_schema())) 29 | 30 | v = {(1, 2): 3, (4, 5): 9} 31 | assert s.to_python(v) == v 32 | assert s.to_python(v, round_trip=True) == {'[1,2]': 3, '[4,5]': 9} 33 | 34 | assert s.to_python(v, mode='json') == {'1,2': 3, '4,5': 9} 35 | assert s.to_python(v, mode='json', round_trip=True) == {'[1,2]': 3, '[4,5]': 9} 36 | 37 | assert s.to_json(v) == b'{"1,2":3,"4,5":9}' 38 | assert s.to_json(v, round_trip=True) == b'{"[1,2]":3,"[4,5]":9}' 39 | 40 | 41 | def test_custom_serializer(): 42 | s = SchemaSerializer(core_schema.any_schema(serialization=core_schema.simple_ser_schema('json'))) 43 | assert s.to_python({1: 2}) == {1: 2} 44 | assert s.to_python({1: 2}, mode='json') == {'1': 2} 45 | assert s.to_python({1: 2}, mode='json', round_trip=True) == '{"1":2}' 46 | assert s.to_json({1: 2}) == b'{"1":2}' 47 | assert s.to_json({1: 2}, round_trip=True) == b'"{\\"1\\":2}"' 48 | -------------------------------------------------------------------------------- /tests/serializers/test_json_or_python.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from pydantic_core import SchemaSerializer, core_schema 4 | 5 | 6 | def test_json_or_python(): 7 | def s1(v: int) -> int: 8 | return v + 1 9 | 10 | def s2(v: int) -> int: 11 | return v + 2 12 | 13 | s = SchemaSerializer( 14 | core_schema.json_or_python_schema( 15 | core_schema.int_schema(serialization=core_schema.plain_serializer_function_ser_schema(s1)), 16 | core_schema.int_schema(serialization=core_schema.plain_serializer_function_ser_schema(s2)), 17 | ) 18 | ) 19 | 20 | assert s.to_json(0) == b'1' 21 | assert s.to_python(0) == 2 22 | 23 | 24 | def test_json_or_python_enum_dict_key(): 25 | # See https://github.com/pydantic/pydantic/issues/6795 26 | class MyEnum(str, Enum): 27 | A = 'A' 28 | B = 'B' 29 | 30 | print(MyEnum('A')) 31 | 32 | s = SchemaSerializer( 33 | core_schema.dict_schema( 34 | core_schema.json_or_python_schema( 35 | core_schema.str_schema(), core_schema.no_info_after_validator_function(MyEnum, core_schema.str_schema()) 36 | ), 37 | core_schema.int_schema(), 38 | ) 39 | ) 40 | 41 | assert s.to_json({MyEnum.A: 1, MyEnum.B: 2}) == b'{"A":1,"B":2}' 42 | assert s.to_python({MyEnum.A: 1, MyEnum.B: 2}) == {MyEnum.A: 1, MyEnum.B: 2} 43 | -------------------------------------------------------------------------------- /tests/serializers/test_none.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaSerializer, core_schema 4 | 5 | all_scalars = ( 6 | 'int', 7 | 'bool', 8 | 'float', 9 | 'none', 10 | 'str', 11 | 'bytes', 12 | 'datetime', 13 | 'date', 14 | 'time', 15 | 'timedelta', 16 | 'url', 17 | 'multi-host-url', 18 | ) 19 | all_types = all_scalars + ('list', 'dict', 'set', 'frozenset') 20 | 21 | 22 | @pytest.mark.parametrize('schema_type', all_types) 23 | def test_none_fallback(schema_type): 24 | s = SchemaSerializer({'type': schema_type}) 25 | assert s.to_python(None) is None 26 | 27 | assert s.to_python(None, mode='json') is None 28 | 29 | assert s.to_json(None) == b'null' 30 | 31 | 32 | @pytest.mark.parametrize('schema_type', all_scalars) 33 | def test_none_fallback_key(schema_type): 34 | s = SchemaSerializer(core_schema.dict_schema({'type': schema_type}, core_schema.int_schema())) 35 | assert s.to_python({None: 1}) == {None: 1} 36 | 37 | assert s.to_python({None: 1}, mode='json') == {'None': 1} 38 | 39 | assert s.to_json({None: 1}) == b'{"None":1}' 40 | -------------------------------------------------------------------------------- /tests/serializers/test_nullable.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaSerializer, core_schema 4 | 5 | 6 | def test_nullable(): 7 | s = SchemaSerializer(core_schema.nullable_schema(core_schema.int_schema())) 8 | assert s.to_python(None) is None 9 | assert s.to_python(1) == 1 10 | assert s.to_python(None, mode='json') is None 11 | assert s.to_python(1, mode='json') == 1 12 | assert s.to_json(1) == b'1' 13 | assert s.to_json(None) == b'null' 14 | with pytest.warns( 15 | UserWarning, 16 | match=r"Expected `int` - serialized value may not be as expected \[input_value='aaa', input_type=str\]", 17 | ): 18 | assert s.to_json('aaa') == b'"aaa"' 19 | -------------------------------------------------------------------------------- /tests/serializers/test_other.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaSerializer, SchemaValidator, core_schema 4 | 5 | from ..conftest import plain_repr 6 | 7 | 8 | def test_chain(): 9 | s = SchemaSerializer(core_schema.chain_schema([core_schema.str_schema(), core_schema.int_schema()])) 10 | 11 | # insert_assert(plain_repr(s)) 12 | assert plain_repr(s) == 'SchemaSerializer(serializer=Int(IntSerializer),definitions=[])' 13 | 14 | assert s.to_python(1) == 1 15 | assert s.to_json(1) == b'1' 16 | 17 | 18 | def test_function_plain(): 19 | s = SchemaSerializer(core_schema.with_info_plain_validator_function(lambda v, info: v + 1)) 20 | # can't infer the type from plain function validators 21 | # insert_assert(plain_repr(s)) 22 | assert plain_repr(s) == 'SchemaSerializer(serializer=Any(AnySerializer),definitions=[])' 23 | 24 | 25 | def test_function_before(): 26 | s = SchemaSerializer( 27 | core_schema.with_info_before_validator_function(lambda v, info: v + 1, core_schema.int_schema()) 28 | ) 29 | # insert_assert(plain_repr(s)) 30 | assert plain_repr(s) == 'SchemaSerializer(serializer=Int(IntSerializer),definitions=[])' 31 | 32 | 33 | def test_function_after(): 34 | s = SchemaSerializer( 35 | core_schema.with_info_after_validator_function(lambda v, info: v + 1, core_schema.int_schema()) 36 | ) 37 | # insert_assert(plain_repr(s)) 38 | assert plain_repr(s) == 'SchemaSerializer(serializer=Int(IntSerializer),definitions=[])' 39 | 40 | 41 | def test_lax_or_strict(): 42 | s = SchemaSerializer(core_schema.lax_or_strict_schema(core_schema.int_schema(), core_schema.str_schema())) 43 | # insert_assert(plain_repr(s)) 44 | assert plain_repr(s) == 'SchemaSerializer(serializer=Str(StrSerializer),definitions=[])' 45 | 46 | assert s.to_json('abc') == b'"abc"' 47 | with pytest.warns( 48 | UserWarning, 49 | match=r'Expected `str` - serialized value may not be as expected \[input_value=123, input_type=int\]', 50 | ): 51 | assert s.to_json(123) == b'123' 52 | 53 | 54 | def test_lax_or_strict_custom_ser(): 55 | s = SchemaSerializer( 56 | core_schema.lax_or_strict_schema( 57 | core_schema.int_schema(), 58 | core_schema.str_schema(), 59 | serialization=core_schema.format_ser_schema('^5s', when_used='always'), 60 | ) 61 | ) 62 | 63 | assert s.to_python('abc') == ' abc ' 64 | assert s.to_python('abc', mode='json') == ' abc ' 65 | assert s.to_json('abc') == b'" abc "' 66 | 67 | 68 | def test_serialize_with_extra_on_superclass() -> None: 69 | class Parent: 70 | x: int 71 | 72 | class Other(Parent): 73 | y: str 74 | 75 | Parent.__pydantic_core_schema__ = core_schema.model_schema( 76 | Parent, 77 | core_schema.model_fields_schema( 78 | { 79 | 'x': core_schema.model_field(core_schema.int_schema()), 80 | } 81 | ), 82 | config=core_schema.CoreConfig(extra_fields_behavior='allow'), 83 | ) 84 | Parent.__pydantic_validator__ = SchemaValidator(Parent.__pydantic_core_schema__) 85 | Parent.__pydantic_serializer__ = SchemaSerializer(Parent.__pydantic_core_schema__) 86 | 87 | Other.__pydantic_core_schema__ = core_schema.model_schema( 88 | Other, 89 | core_schema.model_fields_schema( 90 | { 91 | 'x': core_schema.model_field(core_schema.int_schema()), 92 | 'y': core_schema.model_field(core_schema.str_schema()), 93 | } 94 | ), 95 | config=core_schema.CoreConfig(extra_fields_behavior='forbid'), 96 | ) 97 | Other.__pydantic_validator__ = SchemaValidator(Other.__pydantic_core_schema__) 98 | Other.__pydantic_serializer__ = SchemaSerializer(Other.__pydantic_core_schema__) 99 | 100 | other = Other.__pydantic_validator__.validate_python({'x': 1, 'y': 'some string'}) 101 | assert Parent.__pydantic_serializer__.to_python(other) == {'x': 1} 102 | -------------------------------------------------------------------------------- /tests/serializers/test_pickling.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | from datetime import timedelta 4 | 5 | import pytest 6 | 7 | from pydantic_core import core_schema 8 | from pydantic_core._pydantic_core import SchemaSerializer 9 | 10 | 11 | def repr_function(value, _info): 12 | return repr(value) 13 | 14 | 15 | def test_basic_schema_serializer(): 16 | s = SchemaSerializer(core_schema.dict_schema()) 17 | s = pickle.loads(pickle.dumps(s)) 18 | assert s.to_python({'a': 1, b'b': 2, 33: 3}) == {'a': 1, b'b': 2, 33: 3} 19 | assert s.to_python({'a': 1, b'b': 2, 33: 3, True: 4}, mode='json') == {'a': 1, 'b': 2, '33': 3, 'true': 4} 20 | assert s.to_json({'a': 1, b'b': 2, 33: 3, True: 4}) == b'{"a":1,"b":2,"33":3,"true":4}' 21 | 22 | assert s.to_python({(1, 2): 3}) == {(1, 2): 3} 23 | assert s.to_python({(1, 2): 3}, mode='json') == {'1,2': 3} 24 | assert s.to_json({(1, 2): 3}) == b'{"1,2":3}' 25 | 26 | 27 | @pytest.mark.parametrize( 28 | 'value,expected_python,expected_json', 29 | [(None, 'None', b'"None"'), (1, '1', b'"1"'), ([1, 2, 3], '[1, 2, 3]', b'"[1, 2, 3]"')], 30 | ) 31 | def test_schema_serializer_capturing_function(value, expected_python, expected_json): 32 | # Test a SchemaSerializer that captures a function. 33 | s = SchemaSerializer( 34 | core_schema.any_schema( 35 | serialization=core_schema.plain_serializer_function_ser_schema(repr_function, info_arg=True) 36 | ) 37 | ) 38 | s = pickle.loads(pickle.dumps(s)) 39 | assert s.to_python(value) == expected_python 40 | assert s.to_json(value) == expected_json 41 | assert s.to_python(value, mode='json') == json.loads(expected_json) 42 | 43 | 44 | def test_schema_serializer_containing_config(): 45 | s = SchemaSerializer(core_schema.timedelta_schema(), config={'ser_json_timedelta': 'float'}) 46 | s = pickle.loads(pickle.dumps(s)) 47 | 48 | assert s.to_python(timedelta(seconds=4, microseconds=500_000)) == timedelta(seconds=4, microseconds=500_000) 49 | assert s.to_python(timedelta(seconds=4, microseconds=500_000), mode='json') == 4.5 50 | assert s.to_json(timedelta(seconds=4, microseconds=500_000)) == b'4.5' 51 | 52 | 53 | # Should be defined at the module level for pickling to work: 54 | class Model: 55 | __pydantic_serializer__: SchemaSerializer 56 | __pydantic_complete__ = True 57 | 58 | 59 | def test_schema_serializer_not_reused_when_unpickling() -> None: 60 | s = SchemaSerializer( 61 | core_schema.model_schema( 62 | cls=Model, 63 | schema=core_schema.model_fields_schema(fields={}, model_name='Model'), 64 | config={'title': 'Model'}, 65 | ref='Model:123', 66 | ) 67 | ) 68 | 69 | Model.__pydantic_serializer__ = s 70 | assert 'Prebuilt' not in str(Model.__pydantic_serializer__) 71 | 72 | reconstructed = pickle.loads(pickle.dumps(Model.__pydantic_serializer__)) 73 | assert 'Prebuilt' not in str(reconstructed) 74 | -------------------------------------------------------------------------------- /tests/serializers/test_set_frozenset.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from dirty_equals import IsList 5 | 6 | from pydantic_core import SchemaSerializer, core_schema 7 | 8 | 9 | def test_set_any(): 10 | v = SchemaSerializer(core_schema.set_schema(core_schema.any_schema())) 11 | assert v.to_python({'a', 'b', 'c'}) == {'a', 'b', 'c'} 12 | assert v.to_python({'a', 'b', 'c'}, mode='json') == IsList('a', 'b', 'c', check_order=False) 13 | assert json.loads(v.to_json({'a', 'b', 'c'})) == IsList('a', 'b', 'c', check_order=False) 14 | 15 | 16 | def test_frozenset_any(): 17 | v = SchemaSerializer(core_schema.frozenset_schema(core_schema.any_schema())) 18 | fs = frozenset(['a', 'b', 'c']) 19 | output = v.to_python(fs) 20 | assert output == {'a', 'b', 'c'} 21 | assert type(output) == frozenset 22 | assert v.to_python(fs, mode='json') == IsList('a', 'b', 'c', check_order=False) 23 | assert json.loads(v.to_json(fs)) == IsList('a', 'b', 'c', check_order=False) 24 | 25 | 26 | @pytest.mark.parametrize( 27 | 'input_value,json_output,expected_type', 28 | [ 29 | ('apple', 'apple', r'set\[int\]'), 30 | ([1, 2, 3], [1, 2, 3], r'set\[int\]'), 31 | ((1, 2, 3), [1, 2, 3], r'set\[int\]'), 32 | ( 33 | frozenset([1, 2, 3]), 34 | IsList(1, 2, 3, check_order=False), 35 | r'set\[int\]', 36 | ), 37 | ({1, 2, 'a'}, IsList(1, 2, 'a', check_order=False), 'int'), 38 | ], 39 | ) 40 | def test_set_fallback(input_value, json_output, expected_type): 41 | v = SchemaSerializer(core_schema.set_schema(core_schema.int_schema())) 42 | assert v.to_python({1, 2, 3}) == {1, 2, 3} 43 | 44 | with pytest.warns( 45 | UserWarning, 46 | match=f'Expected `{expected_type}` - serialized value may not be as expected', 47 | ): 48 | assert v.to_python(input_value) == input_value 49 | 50 | with pytest.warns( 51 | UserWarning, 52 | match=f'Expected `{expected_type}` - serialized value may not be as expected', 53 | ): 54 | assert v.to_python(input_value, mode='json') == json_output 55 | 56 | with pytest.warns( 57 | UserWarning, 58 | match=f'Expected `{expected_type}` - serialized value may not be as expected', 59 | ): 60 | assert json.loads(v.to_json(input_value)) == json_output 61 | -------------------------------------------------------------------------------- /tests/serializers/test_timedelta.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | import pytest 4 | 5 | from pydantic_core import SchemaSerializer, core_schema 6 | 7 | try: 8 | import pandas 9 | except ImportError: 10 | pandas = None 11 | 12 | 13 | def test_timedelta(): 14 | v = SchemaSerializer(core_schema.timedelta_schema()) 15 | assert v.to_python(timedelta(days=2, hours=3, minutes=4)) == timedelta(days=2, hours=3, minutes=4) 16 | 17 | assert v.to_python(timedelta(days=2, hours=3, minutes=4), mode='json') == 'P2DT3H4M' 18 | assert v.to_json(timedelta(days=2, hours=3, minutes=4)) == b'"P2DT3H4M"' 19 | 20 | with pytest.warns( 21 | UserWarning, 22 | match=r'Expected `timedelta` - serialized value may not be as expected \[input_value=123, input_type=int\]', 23 | ): 24 | assert v.to_python(123, mode='json') == 123 25 | 26 | with pytest.warns( 27 | UserWarning, 28 | match=r'Expected `timedelta` - serialized value may not be as expected \[input_value=123, input_type=int\]', 29 | ): 30 | assert v.to_json(123) == b'123' 31 | 32 | 33 | def test_timedelta_float(): 34 | v = SchemaSerializer(core_schema.timedelta_schema(), config={'ser_json_timedelta': 'float'}) 35 | assert v.to_python(timedelta(seconds=4, microseconds=500_000)) == timedelta(seconds=4, microseconds=500_000) 36 | 37 | assert v.to_python(timedelta(seconds=4, microseconds=500_000), mode='json') == 4.5 38 | assert v.to_json(timedelta(seconds=4, microseconds=500_000)) == b'4.5' 39 | 40 | 41 | def test_timedelta_key(): 42 | v = SchemaSerializer(core_schema.dict_schema(core_schema.timedelta_schema(), core_schema.int_schema())) 43 | assert v.to_python({timedelta(days=2, hours=3, minutes=4): 1}) == {timedelta(days=2, hours=3, minutes=4): 1} 44 | assert v.to_python({timedelta(days=2, hours=3, minutes=4): 1}, mode='json') == {'P2DT3H4M': 1} 45 | assert v.to_json({timedelta(days=2, hours=3, minutes=4): 1}) == b'{"P2DT3H4M":1}' 46 | 47 | 48 | @pytest.mark.skipif(not pandas, reason='pandas not installed') 49 | def test_pandas(): 50 | v = SchemaSerializer(core_schema.timedelta_schema()) 51 | d = pandas.Timestamp('2023-01-01T02:00:00Z') - pandas.Timestamp('2023-01-01T00:00:00Z') 52 | assert v.to_python(d) == d 53 | assert v.to_python(d, mode='json') == 'PT2H' 54 | assert v.to_json(d) == b'"PT2H"' 55 | -------------------------------------------------------------------------------- /tests/serializers/test_uuid.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | import pytest 4 | 5 | from pydantic_core import SchemaSerializer, core_schema 6 | 7 | 8 | def test_uuid(): 9 | v = SchemaSerializer(core_schema.uuid_schema()) 10 | assert v.to_python(UUID('12345678-1234-5678-1234-567812345678')) == UUID('12345678-1234-5678-1234-567812345678') 11 | 12 | assert ( 13 | v.to_python(UUID('12345678-1234-5678-1234-567812345678'), mode='json') == '12345678-1234-5678-1234-567812345678' 14 | ) 15 | assert v.to_json(UUID('12345678-1234-5678-1234-567812345678')) == b'"12345678-1234-5678-1234-567812345678"' 16 | 17 | with pytest.warns( 18 | UserWarning, 19 | match=r'Expected `uuid` - serialized value may not be as expected \[input_value=123, input_type=int\]', 20 | ): 21 | assert v.to_python(123, mode='json') == 123 22 | 23 | with pytest.warns( 24 | UserWarning, 25 | match=r'Expected `uuid` - serialized value may not be as expected \[input_value=123, input_type=int\]', 26 | ): 27 | assert v.to_json(123) == b'123' 28 | 29 | 30 | def test_uuid_key(): 31 | v = SchemaSerializer(core_schema.dict_schema(core_schema.uuid_schema(), core_schema.uuid_schema())) 32 | assert v.to_python( 33 | {UUID('12345678-1234-5678-1234-567812345678'): UUID('12345678-1234-5678-1234-567812345678')} 34 | ) == {UUID('12345678-1234-5678-1234-567812345678'): UUID('12345678-1234-5678-1234-567812345678')} 35 | assert v.to_python( 36 | {UUID('12345678-1234-5678-1234-567812345678'): UUID('12345678-1234-5678-1234-567812345678')}, mode='json' 37 | ) == {'12345678-1234-5678-1234-567812345678': '12345678-1234-5678-1234-567812345678'} 38 | assert ( 39 | v.to_json({UUID('12345678-1234-5678-1234-567812345678'): UUID('12345678-1234-5678-1234-567812345678')}) 40 | == b'{"12345678-1234-5678-1234-567812345678":"12345678-1234-5678-1234-567812345678"}' 41 | ) 42 | 43 | 44 | @pytest.mark.parametrize( 45 | 'value,expected', 46 | [ 47 | (UUID('12345678-1234-5678-1234-567812345678'), '12345678-1234-5678-1234-567812345678'), 48 | (UUID('550e8400-e29b-41d4-a716-446655440000'), '550e8400-e29b-41d4-a716-446655440000'), 49 | (UUID('123e4567-e89b-12d3-a456-426655440000'), '123e4567-e89b-12d3-a456-426655440000'), 50 | (UUID('00000000-0000-0000-0000-000000000000'), '00000000-0000-0000-0000-000000000000'), 51 | ], 52 | ) 53 | def test_uuid_json(value, expected): 54 | v = SchemaSerializer(core_schema.uuid_schema()) 55 | assert v.to_python(value, mode='json') == expected 56 | assert v.to_json(value).decode() == f'"{expected}"' 57 | 58 | 59 | def test_any_uuid_key(): 60 | v = SchemaSerializer(core_schema.dict_schema()) 61 | input_value = {UUID('12345678-1234-5678-1234-567812345678'): 1} 62 | 63 | assert v.to_python(input_value, mode='json') == {'12345678-1234-5678-1234-567812345678': 1} 64 | assert v.to_json(input_value) == b'{"12345678-1234-5678-1234-567812345678":1}' 65 | -------------------------------------------------------------------------------- /tests/test_build.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | import pytest 4 | 5 | from pydantic_core import SchemaValidator 6 | from pydantic_core import core_schema as cs 7 | 8 | 9 | def test_schema_as_string(): 10 | v = SchemaValidator(cs.bool_schema()) 11 | assert v.validate_python('tRuE') is True 12 | 13 | 14 | @pytest.mark.parametrize('pickle_protocol', range(1, pickle.HIGHEST_PROTOCOL + 1)) 15 | def test_pickle(pickle_protocol: int) -> None: 16 | v1 = SchemaValidator(cs.bool_schema()) 17 | assert v1.validate_python('tRuE') is True 18 | p = pickle.dumps(v1, protocol=pickle_protocol) 19 | v2 = pickle.loads(p) 20 | assert v2.validate_python('tRuE') is True 21 | assert repr(v1) == repr(v2) 22 | 23 | 24 | def test_not_schema_definition_error(): 25 | schema = { 26 | 'type': 'typed-dict', 27 | 'fields': { 28 | f'f_{i}': {'type': 'typed-dict-field', 'schema': {'type': 'nullable', 'schema': {'type': 'int'}}} 29 | for i in range(101) 30 | }, 31 | } 32 | v = SchemaValidator(schema) 33 | assert repr(v).count('TypedDictField') == 101 34 | 35 | 36 | def test_try_self_schema_discriminator(): 37 | """Trying to use self-schema when it shouldn't be used""" 38 | v = SchemaValidator(cs.tagged_union_schema(choices={'int': cs.int_schema()}, discriminator='self-schema')) 39 | assert 'discriminator: LookupKey' in repr(v) 40 | 41 | 42 | def test_build_recursive_schema_from_defs() -> None: 43 | """ 44 | Validate a schema representing mutually recursive models, analogous to the following JSON schema: 45 | 46 | ```json 47 | { 48 | "$schema": "https://json-schema.org/draft/2019-09/schema", 49 | "oneOf": [{"$ref": "#/$defs/a"}], 50 | "$defs": { 51 | "a": { 52 | "type": "object", 53 | "properties": {"b": {"type": "array", "items": {"$ref": "#/$defs/a"}}}, 54 | "required": ["b"], 55 | }, 56 | "b": { 57 | "type": "object", 58 | "properties": {"a": {"type": "array", "items": {"$ref": "#/$defs/b"}}}, 59 | "required": ["a"], 60 | }, 61 | }, 62 | } 63 | ``` 64 | """ 65 | 66 | s = cs.definitions_schema( 67 | cs.definition_reference_schema(schema_ref='a'), 68 | [ 69 | cs.typed_dict_schema( 70 | {'b': cs.typed_dict_field(cs.list_schema(cs.definition_reference_schema('b')))}, ref='a' 71 | ), 72 | cs.typed_dict_schema( 73 | {'a': cs.typed_dict_field(cs.list_schema(cs.definition_reference_schema('a')))}, ref='b' 74 | ), 75 | ], 76 | ) 77 | 78 | SchemaValidator(s) 79 | -------------------------------------------------------------------------------- /tests/test_docstrings.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | try: 6 | from pytest_examples import CodeExample, EvalExample, find_examples 7 | except ImportError: 8 | # pytest_examples is not installed on emscripten 9 | CodeExample = EvalExample = None 10 | 11 | def find_examples(*_directories): 12 | return [] 13 | 14 | 15 | @pytest.mark.skipif(CodeExample is None or sys.platform not in {'linux', 'darwin'}, reason='Only on linux and macos') 16 | @pytest.mark.parametrize('example', find_examples('python/pydantic_core/core_schema.py'), ids=str) 17 | @pytest.mark.thread_unsafe # TODO investigate why pytest_examples seems to be thread unsafe here 18 | def test_docstrings(example: CodeExample, eval_example: EvalExample): 19 | eval_example.set_config(quotes='single') 20 | 21 | if eval_example.update_examples: 22 | eval_example.format(example) 23 | eval_example.run_print_update(example) 24 | else: 25 | eval_example.lint(example) 26 | eval_example.run_print_check(example) 27 | 28 | 29 | @pytest.mark.skipif(CodeExample is None or sys.platform not in {'linux', 'darwin'}, reason='Only on linux and macos') 30 | @pytest.mark.parametrize('example', find_examples('README.md'), ids=str) 31 | @pytest.mark.thread_unsafe # TODO investigate why pytest_examples seems to be thread unsafe here 32 | def test_readme(example: CodeExample, eval_example: EvalExample): 33 | eval_example.set_config(line_length=100, quotes='single') 34 | if eval_example.update_examples: 35 | eval_example.format(example) 36 | else: 37 | eval_example.lint(example) 38 | eval_example.run(example) 39 | -------------------------------------------------------------------------------- /tests/test_garbage_collection.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import sys 3 | from collections.abc import Iterable 4 | from typing import Any 5 | from weakref import WeakValueDictionary 6 | 7 | import pytest 8 | 9 | from pydantic_core import SchemaSerializer, SchemaValidator, core_schema 10 | 11 | from .conftest import assert_gc, is_free_threaded 12 | 13 | GC_TEST_SCHEMA_INNER = core_schema.definitions_schema( 14 | core_schema.definition_reference_schema(schema_ref='model'), 15 | [ 16 | core_schema.typed_dict_schema( 17 | {'x': core_schema.typed_dict_field(core_schema.definition_reference_schema(schema_ref='model'))}, 18 | ref='model', 19 | ) 20 | ], 21 | ) 22 | 23 | 24 | @pytest.mark.xfail(is_free_threaded and sys.version_info < (3, 14), reason='GC leaks on free-threaded (<3.14)') 25 | @pytest.mark.xfail( 26 | condition=platform.python_implementation() == 'PyPy', reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899' 27 | ) 28 | def test_gc_schema_serializer() -> None: 29 | # test for https://github.com/pydantic/pydantic/issues/5136 30 | class BaseModel: 31 | __schema__: SchemaSerializer 32 | 33 | def __init_subclass__(cls) -> None: 34 | cls.__schema__ = SchemaSerializer( 35 | core_schema.model_schema(cls, GC_TEST_SCHEMA_INNER), config={'ser_json_timedelta': 'float'} 36 | ) 37 | 38 | cache: WeakValueDictionary[int, Any] = WeakValueDictionary() 39 | 40 | for _ in range(10_000): 41 | 42 | class MyModel(BaseModel): 43 | pass 44 | 45 | cache[id(MyModel)] = MyModel 46 | 47 | del MyModel 48 | 49 | assert_gc(lambda: len(cache) == 0) 50 | 51 | 52 | @pytest.mark.xfail(is_free_threaded and sys.version_info < (3, 14), reason='GC leaks on free-threaded (<3.14)') 53 | @pytest.mark.xfail( 54 | condition=platform.python_implementation() == 'PyPy', reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899' 55 | ) 56 | def test_gc_schema_validator() -> None: 57 | # test for https://github.com/pydantic/pydantic/issues/5136 58 | class BaseModel: 59 | __validator__: SchemaValidator 60 | 61 | def __init_subclass__(cls) -> None: 62 | cls.__validator__ = SchemaValidator( 63 | schema=core_schema.model_schema(cls, GC_TEST_SCHEMA_INNER), 64 | config=core_schema.CoreConfig(extra_fields_behavior='allow'), 65 | ) 66 | 67 | cache: WeakValueDictionary[int, Any] = WeakValueDictionary() 68 | 69 | for _ in range(10_000): 70 | 71 | class MyModel(BaseModel): 72 | pass 73 | 74 | cache[id(MyModel)] = MyModel 75 | 76 | del MyModel 77 | 78 | assert_gc(lambda: len(cache) == 0) 79 | 80 | 81 | @pytest.mark.xfail( 82 | condition=platform.python_implementation() == 'PyPy', reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899' 83 | ) 84 | def test_gc_validator_iterator() -> None: 85 | # test for https://github.com/pydantic/pydantic/issues/9243 86 | class MyModel: 87 | iter: Iterable[int] 88 | 89 | v = SchemaValidator( 90 | core_schema.model_schema( 91 | MyModel, 92 | core_schema.model_fields_schema( 93 | {'iter': core_schema.model_field(core_schema.generator_schema(core_schema.int_schema()))} 94 | ), 95 | ) 96 | ) 97 | 98 | class MyIterable: 99 | def __iter__(self): 100 | return self 101 | 102 | def __next__(self): 103 | raise StopIteration() 104 | 105 | cache: WeakValueDictionary[int, Any] = WeakValueDictionary() 106 | 107 | for _ in range(10_000): 108 | iterable = MyIterable() 109 | cache[id(iterable)] = iterable 110 | v.validate_python({'iter': iterable}) 111 | del iterable 112 | 113 | assert_gc(lambda: len(cache) == 0) 114 | -------------------------------------------------------------------------------- /tests/test_isinstance.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import PydanticOmit, SchemaError, SchemaValidator, ValidationError, core_schema 4 | from pydantic_core import core_schema as cs 5 | 6 | from .conftest import PyAndJson 7 | 8 | 9 | def test_isinstance(): 10 | v = SchemaValidator(cs.int_schema()) 11 | assert v.validate_python(123) == 123 12 | assert v.isinstance_python(123) is True 13 | assert v.validate_python('123') == 123 14 | assert v.isinstance_python('123') is True 15 | 16 | with pytest.raises(ValidationError, match='Input should be a valid integer'): 17 | v.validate_python('foo') 18 | 19 | assert v.isinstance_python('foo') is False 20 | 21 | 22 | def test_isinstance_strict(): 23 | v = SchemaValidator(cs.int_schema(strict=True)) 24 | assert v.validate_python(123) == 123 25 | assert v.isinstance_python(123) is True 26 | 27 | with pytest.raises(ValidationError, match='Input should be a valid integer'): 28 | v.validate_python('123') 29 | 30 | assert v.isinstance_python('123') is False 31 | 32 | 33 | def test_internal_error(): 34 | v = SchemaValidator( 35 | cs.model_schema(cls=int, schema=cs.model_fields_schema(fields={'f': cs.model_field(schema=cs.int_schema())})) 36 | ) 37 | with pytest.raises(AttributeError, match="'int' object has no attribute '__dict__'"): 38 | v.validate_python({'f': 123}) 39 | 40 | with pytest.raises(AttributeError, match="'int' object has no attribute '__dict__'"): 41 | v.validate_json('{"f": 123}') 42 | 43 | with pytest.raises(AttributeError, match="'int' object has no attribute '__dict__'"): 44 | v.isinstance_python({'f': 123}) 45 | 46 | 47 | def test_omit(py_and_json: PyAndJson): 48 | def omit(v, info): 49 | if v == 'omit': 50 | raise PydanticOmit 51 | elif v == 'error': 52 | raise ValueError('error') 53 | else: 54 | return v 55 | 56 | v = py_and_json(core_schema.with_info_plain_validator_function(omit)) 57 | assert v.validate_test('foo') == 'foo' 58 | if v.validator_type == 'python': 59 | assert v.isinstance_test('foo') is True 60 | 61 | if v.validator_type == 'python': 62 | assert v.isinstance_test('error') is False 63 | with pytest.raises(SchemaError, match='Uncaught Omit error, please check your usage of `default` validators.'): 64 | v.validate_test('omit') 65 | -------------------------------------------------------------------------------- /tests/test_strict.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from typing import Any 5 | 6 | import pytest 7 | 8 | from pydantic_core import ValidationError 9 | 10 | from .conftest import Err, PyAndJson 11 | 12 | 13 | @pytest.mark.parametrize( 14 | 'strict_to_validator,strict_in_schema,input_value,expected', 15 | [ 16 | (False, False, 123, 123), 17 | (False, False, '123', 123), 18 | (None, False, 123, 123), 19 | (None, False, '123', 123), 20 | (True, False, 123, 123), 21 | (True, False, '123', Err('Input should be a valid integer [type=int_type')), 22 | (False, True, 123, 123), 23 | (False, True, '123', 123), 24 | (None, True, 123, 123), 25 | (None, True, '123', Err('Input should be a valid integer [type=int_type')), 26 | (True, True, 123, 123), 27 | (True, True, '123', Err('Input should be a valid integer [type=int_type')), 28 | (False, None, 123, 123), 29 | (False, None, '123', 123), 30 | (None, None, 123, 123), 31 | (None, None, '123', 123), 32 | (True, None, 123, 123), 33 | (True, None, '123', Err('Input should be a valid integer [type=int_type')), 34 | ], 35 | ) 36 | def test_int_strict_argument( 37 | py_and_json: PyAndJson, strict_to_validator: bool | None, strict_in_schema: bool | None, input_value, expected 38 | ): 39 | schema: dict[str, Any] = {'type': 'int'} 40 | if strict_in_schema is not None: 41 | schema['strict'] = strict_in_schema 42 | v = py_and_json(schema) 43 | if isinstance(expected, Err): 44 | assert v.isinstance_test(input_value, strict_to_validator) is False 45 | with pytest.raises(ValidationError, match=re.escape(expected.message)): 46 | v.validate_test(input_value, strict_to_validator) 47 | else: 48 | assert v.isinstance_test(input_value, strict_to_validator) is True 49 | assert v.validate_test(input_value, strict_to_validator) == expected 50 | -------------------------------------------------------------------------------- /tests/validators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydantic/pydantic-core/52e9a5309580bf53b9d85e069f956dcfca84998f/tests/validators/__init__.py -------------------------------------------------------------------------------- /tests/validators/arguments_v3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydantic/pydantic-core/52e9a5309580bf53b9d85e069f956dcfca84998f/tests/validators/arguments_v3/__init__.py -------------------------------------------------------------------------------- /tests/validators/arguments_v3/test_build_errors.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaError, SchemaValidator 4 | from pydantic_core import core_schema as cs 5 | 6 | 7 | def test_build_non_default_follows_default() -> None: 8 | with pytest.raises(SchemaError, match="Required parameter 'b' follows parameter with default"): 9 | SchemaValidator( 10 | schema=cs.arguments_v3_schema( 11 | [ 12 | cs.arguments_v3_parameter( 13 | name='a', 14 | schema=cs.with_default_schema(schema=cs.int_schema(), default_factory=lambda: 42), 15 | mode='positional_or_keyword', 16 | ), 17 | cs.arguments_v3_parameter(name='b', schema=cs.int_schema(), mode='positional_or_keyword'), 18 | ] 19 | ) 20 | ) 21 | 22 | 23 | def test_duplicate_parameter_name() -> None: 24 | with pytest.raises(SchemaError, match="Duplicate parameter 'test'"): 25 | SchemaValidator( 26 | schema=cs.arguments_v3_schema( 27 | [ 28 | cs.arguments_v3_parameter(name='test', schema=cs.int_schema()), 29 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema()), 30 | cs.arguments_v3_parameter(name='test', schema=cs.int_schema()), 31 | ] 32 | ) 33 | ) 34 | 35 | 36 | def test_invalid_positional_only_parameter_position() -> None: 37 | with pytest.raises(SchemaError, match="Positional only parameter 'test' cannot follow other parameter kinds"): 38 | SchemaValidator( 39 | schema=cs.arguments_v3_schema( 40 | [ 41 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='var_args'), 42 | cs.arguments_v3_parameter(name='test', schema=cs.int_schema(), mode='positional_only'), 43 | ] 44 | ) 45 | ) 46 | 47 | 48 | def test_invalid_positional_or_keyword_parameter_position() -> None: 49 | with pytest.raises( 50 | SchemaError, match="Positional or keyword parameter 'test' cannot follow variadic or keyword only parameters" 51 | ): 52 | SchemaValidator( 53 | schema=cs.arguments_v3_schema( 54 | [ 55 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='var_args'), 56 | cs.arguments_v3_parameter(name='test', schema=cs.int_schema(), mode='positional_or_keyword'), 57 | ] 58 | ) 59 | ) 60 | 61 | 62 | def test_invalid_var_args_parameter_position() -> None: 63 | with pytest.raises( 64 | SchemaError, match="Variadic positional parameter 'test' cannot follow variadic or keyword only parameters" 65 | ): 66 | SchemaValidator( 67 | schema=cs.arguments_v3_schema( 68 | [ 69 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='keyword_only'), 70 | cs.arguments_v3_parameter(name='test', schema=cs.int_schema(), mode='var_args'), 71 | ] 72 | ) 73 | ) 74 | 75 | 76 | def test_invalid_keyword_only_parameter_position() -> None: 77 | with pytest.raises( 78 | SchemaError, match="Keyword only parameter 'test' cannot follow variadic keyword only parameter" 79 | ): 80 | SchemaValidator( 81 | schema=cs.arguments_v3_schema( 82 | [ 83 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='var_kwargs_uniform'), 84 | cs.arguments_v3_parameter(name='test', schema=cs.int_schema(), mode='keyword_only'), 85 | ] 86 | ) 87 | ) 88 | -------------------------------------------------------------------------------- /tests/validators/arguments_v3/test_extra.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import ArgsKwargs, ValidationError 4 | from pydantic_core import core_schema as cs 5 | 6 | from ...conftest import PyAndJson 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ['input_value', 'err_type'], 11 | ( 12 | [ArgsKwargs((), {'a': 1, 'b': 2, 'c': 3}), 'unexpected_keyword_argument'], 13 | [ArgsKwargs((), {'a': 1, 'c': 3, 'extra': 'value'}), 'unexpected_keyword_argument'], 14 | [{'a': 1, 'b': 2, 'c': 3}, 'extra_forbidden'], 15 | [{'a': 1, 'c': 3, 'extra': 'value'}, 'extra_forbidden'], 16 | ), 17 | ) 18 | def test_extra_forbid(py_and_json: PyAndJson, input_value, err_type) -> None: 19 | v = py_and_json( 20 | cs.arguments_v3_schema( 21 | [ 22 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema()), 23 | cs.arguments_v3_parameter(name='b', schema=cs.int_schema(), alias='c'), 24 | ], 25 | extra_behavior='forbid', 26 | ), 27 | ) 28 | 29 | with pytest.raises(ValidationError) as exc_info: 30 | v.validate_test(input_value) 31 | 32 | error = exc_info.value.errors()[0] 33 | 34 | assert error['type'] == err_type 35 | 36 | 37 | @pytest.mark.parametrize( 38 | 'input_value', 39 | [ 40 | ArgsKwargs((), {'a': 1, 'b': 2, 'c': 3}), 41 | ArgsKwargs((), {'a': 1, 'c': 3, 'extra': 'value'}), 42 | {'a': 1, 'b': 2, 'c': 3}, 43 | {'a': 1, 'c': 3, 'extra': 'value'}, 44 | ], 45 | ) 46 | def test_extra_ignore(py_and_json: PyAndJson, input_value) -> None: 47 | v = py_and_json( 48 | cs.arguments_v3_schema( 49 | [ 50 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='keyword_only'), 51 | cs.arguments_v3_parameter(name='b', schema=cs.int_schema(), alias='c', mode='keyword_only'), 52 | ], 53 | extra_behavior='ignore', 54 | ), 55 | ) 56 | 57 | assert v.validate_test(input_value) == ((), {'a': 1, 'b': 3}) 58 | -------------------------------------------------------------------------------- /tests/validators/arguments_v3/test_keyword_only.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import ArgsKwargs, ValidationError 4 | from pydantic_core import core_schema as cs 5 | 6 | from ...conftest import PyAndJson 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 'input_value', 11 | [ 12 | ArgsKwargs((), {'a': 1, 'b': True}), 13 | ArgsKwargs((), {'a': 1}), 14 | {'a': 1, 'b': True}, 15 | {'a': 1}, 16 | ], 17 | ) 18 | def test_keyword_only(py_and_json: PyAndJson, input_value) -> None: 19 | """Test valid inputs against keyword-only parameters: 20 | 21 | ```python 22 | def func(*, a: int, b: bool = True): 23 | ... 24 | ``` 25 | """ 26 | v = py_and_json( 27 | cs.arguments_v3_schema( 28 | [ 29 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='keyword_only'), 30 | cs.arguments_v3_parameter( 31 | name='b', schema=cs.with_default_schema(cs.bool_schema(), default=True), mode='keyword_only' 32 | ), 33 | ] 34 | ) 35 | ) 36 | 37 | assert v.validate_test(input_value) == ((), {'a': 1, 'b': True}) 38 | 39 | 40 | @pytest.mark.parametrize( 41 | 'input_value', 42 | [ArgsKwargs((), {'a': 'not_an_int'}), {'a': 'not_an_int'}], 43 | ) 44 | def test_keyword_only_validation_error(py_and_json: PyAndJson, input_value) -> None: 45 | """Test invalid inputs against keyword-only parameters: 46 | 47 | ```python 48 | def func(*, a: int): 49 | ... 50 | 51 | func('not_an_int') 52 | ``` 53 | """ 54 | v = py_and_json( 55 | cs.arguments_v3_schema( 56 | [ 57 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='keyword_only'), 58 | ] 59 | ) 60 | ) 61 | 62 | with pytest.raises(ValidationError) as exc_info: 63 | v.validate_test(input_value) 64 | 65 | error = exc_info.value.errors()[0] 66 | 67 | assert error['type'] == 'int_parsing' 68 | assert error['loc'] == ('a',) 69 | 70 | 71 | @pytest.mark.parametrize( 72 | 'input_value', 73 | [ArgsKwargs((), {}), {}], 74 | ) 75 | def test_keyword_only_error_required(py_and_json: PyAndJson, input_value) -> None: 76 | """Test missing inputs against keyword-only parameters: 77 | 78 | ```python 79 | def func(*, a: int): 80 | ... 81 | 82 | func() 83 | ``` 84 | """ 85 | v = py_and_json( 86 | cs.arguments_v3_schema( 87 | [ 88 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='keyword_only'), 89 | ] 90 | ) 91 | ) 92 | 93 | with pytest.raises(ValidationError) as exc_info: 94 | v.validate_test(input_value) 95 | 96 | error = exc_info.value.errors()[0] 97 | 98 | assert error['type'] == 'missing_keyword_only_argument' 99 | assert error['loc'] == ('a',) 100 | -------------------------------------------------------------------------------- /tests/validators/arguments_v3/test_positional_only.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import ArgsKwargs, ValidationError 4 | from pydantic_core import core_schema as cs 5 | 6 | from ...conftest import PyAndJson 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 'input_value', 11 | [ 12 | ArgsKwargs((1, True)), 13 | ArgsKwargs((1,)), 14 | {'a': 1, 'b': True}, 15 | {'a': 1}, 16 | ], 17 | ) 18 | def test_positional_only(py_and_json: PyAndJson, input_value) -> None: 19 | """Test valid inputs against positional-only parameters: 20 | 21 | ```python 22 | def func(a: int, b: bool = True, /): 23 | ... 24 | ``` 25 | """ 26 | v = py_and_json( 27 | cs.arguments_v3_schema( 28 | [ 29 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='positional_only'), 30 | cs.arguments_v3_parameter( 31 | name='b', schema=cs.with_default_schema(cs.bool_schema(), default=True), mode='positional_only' 32 | ), 33 | ] 34 | ) 35 | ) 36 | 37 | assert v.validate_test(input_value) == ((1, True), {}) 38 | 39 | 40 | def test_positional_only_validation_error(py_and_json: PyAndJson) -> None: 41 | """Test invalid inputs against positional-only parameters: 42 | 43 | ```python 44 | def func(a: int, /): 45 | ... 46 | 47 | func('not_an_int') 48 | ``` 49 | """ 50 | v = py_and_json( 51 | cs.arguments_v3_schema( 52 | [ 53 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='positional_only'), 54 | ] 55 | ) 56 | ) 57 | 58 | with pytest.raises(ValidationError) as exc_info: 59 | v.validate_test(ArgsKwargs(('not_an_int',), {})) 60 | 61 | error = exc_info.value.errors()[0] 62 | 63 | assert error['type'] == 'int_parsing' 64 | assert error['loc'] == (0,) 65 | 66 | with pytest.raises(ValidationError) as exc_info: 67 | v.validate_test({'a': 'not_an_int'}) 68 | 69 | error = exc_info.value.errors()[0] 70 | 71 | assert error['type'] == 'int_parsing' 72 | assert error['loc'] == ('a',) 73 | 74 | 75 | def test_positional_only_error_required(py_and_json: PyAndJson) -> None: 76 | """Test missing inputs against positional-only parameters: 77 | 78 | ```python 79 | def func(a: int, /): 80 | ... 81 | 82 | func() 83 | ``` 84 | """ 85 | v = py_and_json( 86 | cs.arguments_v3_schema( 87 | [ 88 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='positional_only'), 89 | ] 90 | ) 91 | ) 92 | 93 | with pytest.raises(ValidationError) as exc_info: 94 | v.validate_test(ArgsKwargs((), {})) 95 | 96 | error = exc_info.value.errors()[0] 97 | 98 | assert error['type'] == 'missing_positional_only_argument' 99 | assert error['loc'] == (0,) 100 | 101 | with pytest.raises(ValidationError) as exc_info: 102 | v.validate_test({}) 103 | 104 | error = exc_info.value.errors()[0] 105 | 106 | assert error['type'] == 'missing_positional_only_argument' 107 | assert error['loc'] == ('a',) 108 | -------------------------------------------------------------------------------- /tests/validators/arguments_v3/test_positional_or_keyword.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import ArgsKwargs, ValidationError 4 | from pydantic_core import core_schema as cs 5 | 6 | from ...conftest import PyAndJson 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ['input_value', 'expected'], 11 | ( 12 | [ArgsKwargs((1, True)), ((1, True), {})], 13 | [ArgsKwargs((1,)), ((1,), {'b': True})], 14 | [ArgsKwargs((1,), {'b': True}), ((1,), {'b': True})], 15 | [ArgsKwargs((), {'a': 1, 'b': True}), ((), {'a': 1, 'b': True})], 16 | [{'a': 1, 'b': True}, ((1, True), {})], 17 | [{'a': 1}, ((1, True), {})], 18 | ), 19 | ) 20 | def test_positional_or_keyword(py_and_json: PyAndJson, input_value, expected) -> None: 21 | """Test valid inputs against positional-or-keyword parameters: 22 | 23 | ```python 24 | def func(a: int, b: bool = True): 25 | ... 26 | ``` 27 | """ 28 | v = py_and_json( 29 | cs.arguments_v3_schema( 30 | [ 31 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='positional_or_keyword'), 32 | cs.arguments_v3_parameter( 33 | name='b', 34 | schema=cs.with_default_schema(cs.bool_schema(), default=True), 35 | mode='positional_or_keyword', 36 | ), 37 | ] 38 | ) 39 | ) 40 | 41 | assert v.validate_test(input_value) == expected 42 | 43 | 44 | @pytest.mark.parametrize( 45 | ['input_value', 'err_loc'], 46 | ( 47 | [ArgsKwargs(('not_an_int',), {}), (0,)], 48 | [ArgsKwargs((), {'a': 'not_an_int'}), ('a',)], 49 | [{'a': 'not_an_int'}, ('a',)], 50 | ), 51 | ) 52 | def test_positional_or_keyword_validation_error(py_and_json: PyAndJson, input_value, err_loc) -> None: 53 | """Test invalid inputs against positional-or-keyword parameters: 54 | 55 | ```python 56 | def func(a: int): 57 | ... 58 | 59 | func('not_an_int') 60 | ``` 61 | """ 62 | v = py_and_json( 63 | cs.arguments_v3_schema( 64 | [ 65 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='positional_or_keyword'), 66 | ] 67 | ) 68 | ) 69 | 70 | with pytest.raises(ValidationError) as exc_info: 71 | v.validate_test(input_value) 72 | 73 | error = exc_info.value.errors()[0] 74 | 75 | assert error['type'] == 'int_parsing' 76 | assert error['loc'] == err_loc 77 | 78 | 79 | @pytest.mark.parametrize( 80 | 'input_value', 81 | [ArgsKwargs((), {}), {}], 82 | ) 83 | def test_positional_only_error_required(py_and_json: PyAndJson, input_value) -> None: 84 | """Test missing inputs against positional-or-keyword parameters: 85 | 86 | ```python 87 | def func(a: int): 88 | ... 89 | 90 | func() 91 | ``` 92 | """ 93 | v = py_and_json( 94 | cs.arguments_v3_schema( 95 | [ 96 | cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='positional_or_keyword'), 97 | ] 98 | ) 99 | ) 100 | 101 | with pytest.raises(ValidationError) as exc_info: 102 | v.validate_test(input_value) 103 | 104 | error = exc_info.value.errors()[0] 105 | 106 | assert error['type'] == 'missing_argument' 107 | assert error['loc'] == ('a',) 108 | -------------------------------------------------------------------------------- /tests/validators/arguments_v3/test_var_args.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import ArgsKwargs, ValidationError 4 | from pydantic_core import core_schema as cs 5 | 6 | from ...conftest import PyAndJson 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ['input_value', 'expected'], 11 | ( 12 | [ArgsKwargs(()), ((), {})], 13 | [ArgsKwargs((1, 2, 3)), ((1, 2, 3), {})], 14 | [{'args': ()}, ((), {})], 15 | [{'args': (1, 2, 3)}, ((1, 2, 3), {})], 16 | # Also validates against other sequence types, as long as it is 17 | # possible to validate it as a tuple: 18 | [{'args': [1, 2, 3]}, ((1, 2, 3), {})], 19 | ), 20 | ) 21 | def test_var_args(py_and_json: PyAndJson, input_value, expected) -> None: 22 | """Test valid inputs against var-args parameters: 23 | 24 | ```python 25 | def func(*args: int): 26 | ... 27 | ``` 28 | """ 29 | v = py_and_json( 30 | cs.arguments_v3_schema( 31 | [ 32 | cs.arguments_v3_parameter(name='args', schema=cs.int_schema(), mode='var_args'), 33 | ] 34 | ) 35 | ) 36 | 37 | assert v.validate_test(input_value) == expected 38 | 39 | 40 | @pytest.mark.parametrize( 41 | ['input_value', 'err_loc'], 42 | ( 43 | [ArgsKwargs(('not_an_int',)), (0,)], 44 | [ 45 | ArgsKwargs( 46 | ( 47 | 1, 48 | 'not_an_int', 49 | ) 50 | ), 51 | (1,), 52 | ], 53 | [{'args': ['not_an_int']}, ('args', 0)], 54 | [{'args': [1, 'not_an_int']}, ('args', 1)], 55 | ), 56 | ) 57 | def test_var_args_validation_error(py_and_json: PyAndJson, input_value, err_loc) -> None: 58 | """Test invalid inputs against var-args parameters: 59 | 60 | ```python 61 | def func(*args: int): 62 | ... 63 | 64 | func(1, 'not_an_int') 65 | ``` 66 | """ 67 | v = py_and_json( 68 | cs.arguments_v3_schema( 69 | [ 70 | cs.arguments_v3_parameter(name='args', schema=cs.int_schema(), mode='var_args'), 71 | ] 72 | ) 73 | ) 74 | 75 | with pytest.raises(ValidationError) as exc_info: 76 | v.validate_test(input_value) 77 | 78 | error = exc_info.value.errors()[0] 79 | 80 | assert error['type'] == 'int_parsing' 81 | assert error['loc'] == err_loc 82 | 83 | 84 | def test_var_args_invalid_tuple(py_and_json: PyAndJson) -> None: 85 | """Test invalid tuple-like input against var-args parameters in mapping validation mode.""" 86 | v = py_and_json( 87 | cs.arguments_v3_schema( 88 | [ 89 | cs.arguments_v3_parameter(name='args', schema=cs.int_schema(), mode='var_args'), 90 | ] 91 | ) 92 | ) 93 | 94 | with pytest.raises(ValidationError) as exc_info: 95 | v.validate_test({'args': 'not_a_tuple'}) 96 | 97 | error = exc_info.value.errors()[0] 98 | 99 | assert error['type'] == 'tuple_type' 100 | assert error['loc'] == ('args',) 101 | -------------------------------------------------------------------------------- /tests/validators/arguments_v3/test_var_kwargs_uniform.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import ArgsKwargs, ValidationError 4 | from pydantic_core import core_schema as cs 5 | 6 | from ...conftest import PyAndJson 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ['input_value', 'expected'], 11 | ( 12 | [ArgsKwargs(()), ((), {})], 13 | [ArgsKwargs((), {'a': 1, 'b': 2}), ((), {'a': 1, 'b': 2})], 14 | [{}, ((), {})], 15 | [{'kwargs': {'a': 1, 'b': 2}}, ((), {'a': 1, 'b': 2})], 16 | ), 17 | ) 18 | def test_var_kwargs(py_and_json: PyAndJson, input_value, expected) -> None: 19 | """Test valid inputs against var-args parameters (uniform): 20 | 21 | ```python 22 | def func(**kwargs: int): 23 | ... 24 | ``` 25 | """ 26 | v = py_and_json( 27 | cs.arguments_v3_schema( 28 | [ 29 | cs.arguments_v3_parameter(name='kwargs', schema=cs.int_schema(), mode='var_kwargs_uniform'), 30 | ] 31 | ) 32 | ) 33 | 34 | assert v.validate_test(input_value) == expected 35 | 36 | 37 | @pytest.mark.parametrize( 38 | ['input_value', 'err_loc'], 39 | ( 40 | [ArgsKwargs((), {'a': 'not_an_int'}), ('a',)], 41 | [ArgsKwargs((), {'a': 1, 'b': 'not_an_int'}), ('b',)], 42 | [{'kwargs': {'a': 'not_an_int'}}, ('kwargs', 'a')], 43 | [{'kwargs': {'a': 1, 'b': 'not_an_int'}}, ('kwargs', 'b')], 44 | ), 45 | ) 46 | def test_var_kwargs_validation_error(py_and_json: PyAndJson, input_value, err_loc) -> None: 47 | """Test invalid inputs against var-args parameters (uniform): 48 | 49 | ```python 50 | def func(**kwargs: int): 51 | ... 52 | 53 | func(a='not_an_int') 54 | ``` 55 | """ 56 | v = py_and_json( 57 | cs.arguments_v3_schema( 58 | [ 59 | cs.arguments_v3_parameter(name='kwargs', schema=cs.int_schema(), mode='var_kwargs_uniform'), 60 | ] 61 | ) 62 | ) 63 | 64 | with pytest.raises(ValidationError) as exc_info: 65 | v.validate_test(input_value) 66 | 67 | error = exc_info.value.errors()[0] 68 | 69 | assert error['type'] == 'int_parsing' 70 | assert error['loc'] == err_loc 71 | 72 | 73 | def test_var_kwargs_invalid_dict(py_and_json: PyAndJson) -> None: 74 | """Test invalid dict-like input against var-kwargs parameters in mapping validation mode.""" 75 | v = py_and_json( 76 | cs.arguments_v3_schema( 77 | [ 78 | cs.arguments_v3_parameter(name='kwargs', schema=cs.int_schema(), mode='var_kwargs_uniform'), 79 | ] 80 | ) 81 | ) 82 | 83 | with pytest.raises(ValidationError) as exc_info: 84 | v.validate_test({'kwargs': 'not_a_dict'}) 85 | 86 | error = exc_info.value.errors()[0] 87 | 88 | assert error['type'] == 'dict_type' 89 | assert error['loc'] == ('kwargs',) 90 | -------------------------------------------------------------------------------- /tests/validators/test_callable.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaValidator, ValidationError 4 | from pydantic_core import core_schema as cs 5 | 6 | 7 | def func(): 8 | return 42 9 | 10 | 11 | class Foo: 12 | pass 13 | 14 | 15 | class CallableClass: 16 | def __call__(self, *args, **kwargs): 17 | pass 18 | 19 | 20 | def test_callable(): 21 | v = SchemaValidator(cs.callable_schema()) 22 | assert v.validate_python(func) == func 23 | assert v.isinstance_python(func) is True 24 | 25 | with pytest.raises(ValidationError) as exc_info: 26 | v.validate_python(42) 27 | 28 | assert exc_info.value.errors(include_url=False) == [ 29 | {'type': 'callable_type', 'loc': (), 'msg': 'Input should be callable', 'input': 42} 30 | ] 31 | 32 | 33 | @pytest.mark.parametrize( 34 | 'input_value,expected', 35 | [ 36 | (func, True), 37 | (lambda: 42, True), 38 | (lambda x: 2 * 42, True), 39 | (dict, True), 40 | (Foo, True), 41 | (Foo(), False), 42 | (4, False), 43 | ('ddd', False), 44 | ([], False), 45 | ((1,), False), 46 | (CallableClass, True), 47 | (CallableClass(), True), 48 | ], 49 | ) 50 | def test_callable_cases(input_value, expected): 51 | v = SchemaValidator(cs.callable_schema()) 52 | assert v.isinstance_python(input_value) == expected 53 | 54 | 55 | def test_repr(): 56 | v = SchemaValidator(cs.union_schema(choices=[cs.int_schema(), cs.callable_schema()])) 57 | assert v.isinstance_python(4) is True 58 | assert v.isinstance_python(func) is True 59 | assert v.isinstance_python('foo') is False 60 | 61 | with pytest.raises(ValidationError, match=r'callable\s+Input should be callable'): 62 | v.validate_python('foo') 63 | -------------------------------------------------------------------------------- /tests/validators/test_chain.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | import pytest 4 | 5 | from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema 6 | from pydantic_core import core_schema as cs 7 | 8 | from ..conftest import PyAndJson 9 | 10 | 11 | def test_chain(): 12 | validator = SchemaValidator( 13 | cs.chain_schema( 14 | steps=[cs.str_schema(), core_schema.with_info_plain_validator_function(lambda v, info: Decimal(v))] 15 | ) 16 | ) 17 | 18 | assert validator.validate_python('1.44') == Decimal('1.44') 19 | assert validator.validate_python(b'1.44') == Decimal('1.44') 20 | 21 | 22 | def test_chain_many(): 23 | validator = SchemaValidator( 24 | cs.chain_schema( 25 | steps=[ 26 | core_schema.with_info_plain_validator_function(lambda v, info: f'{v}-1'), 27 | core_schema.with_info_plain_validator_function(lambda v, info: f'{v}-2'), 28 | core_schema.with_info_plain_validator_function(lambda v, info: f'{v}-3'), 29 | core_schema.with_info_plain_validator_function(lambda v, info: f'{v}-4'), 30 | ] 31 | ) 32 | ) 33 | 34 | assert validator.validate_python('input') == 'input-1-2-3-4' 35 | 36 | 37 | def test_chain_error(): 38 | validator = SchemaValidator(cs.chain_schema(steps=[cs.str_schema(), cs.int_schema()])) 39 | 40 | assert validator.validate_python('123') == 123 41 | assert validator.validate_python(b'123') == 123 42 | 43 | with pytest.raises(ValidationError) as exc_info: 44 | validator.validate_python('abc') 45 | # insert_assert(exc_info.value.errors(include_url=False)) 46 | assert exc_info.value.errors(include_url=False) == [ 47 | { 48 | 'type': 'int_parsing', 49 | 'loc': (), 50 | 'msg': 'Input should be a valid integer, unable to parse string as an integer', 51 | 'input': 'abc', 52 | } 53 | ] 54 | 55 | 56 | @pytest.mark.parametrize( 57 | 'input_value,expected', [('1.44', Decimal('1.44')), (1, Decimal(1)), (1.44, pytest.approx(1.44))] 58 | ) 59 | def test_json(py_and_json: PyAndJson, input_value, expected): 60 | validator = py_and_json( 61 | { 62 | 'type': 'chain', 63 | 'steps': [ 64 | {'type': 'union', 'choices': [{'type': 'str'}, {'type': 'float'}]}, 65 | core_schema.with_info_plain_validator_function(lambda v, info: Decimal(v)), 66 | ], 67 | } 68 | ) 69 | output = validator.validate_test(input_value) 70 | assert output == expected 71 | assert isinstance(output, Decimal) 72 | 73 | 74 | def test_flatten(): 75 | validator = SchemaValidator( 76 | cs.chain_schema( 77 | steps=[ 78 | core_schema.with_info_plain_validator_function(lambda v, info: f'{v}-1'), 79 | cs.chain_schema( 80 | steps=[ 81 | { 82 | 'type': 'function-plain', 83 | 'function': {'type': 'with-info', 'function': lambda v, info: f'{v}-2'}, 84 | }, 85 | { 86 | 'type': 'function-plain', 87 | 'function': {'type': 'with-info', 'function': lambda v, info: f'{v}-3'}, 88 | }, 89 | ] 90 | ), 91 | ] 92 | ) 93 | ) 94 | 95 | assert validator.validate_python('input') == 'input-1-2-3' 96 | assert validator.title == 'chain[function-plain[()],function-plain[()],function-plain[()]]' 97 | 98 | 99 | def test_chain_empty(): 100 | with pytest.raises(SchemaError, match='One or more steps are required for a chain validator'): 101 | SchemaValidator(cs.chain_schema(steps=[])) 102 | 103 | 104 | def test_chain_one(): 105 | validator = SchemaValidator( 106 | cs.chain_schema(steps=[core_schema.with_info_plain_validator_function(lambda v, info: f'{v}-1')]) 107 | ) 108 | assert validator.validate_python('input') == 'input-1' 109 | assert validator.title == 'function-plain[()]' 110 | -------------------------------------------------------------------------------- /tests/validators/test_custom_error.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema 4 | 5 | from ..conftest import PyAndJson 6 | 7 | 8 | def test_custom_error(py_and_json: PyAndJson): 9 | v = py_and_json( 10 | core_schema.custom_error_schema(core_schema.int_schema(), 'foobar', custom_error_message='Hello there') 11 | ) 12 | assert v.validate_test(1) == 1 13 | 14 | with pytest.raises(ValidationError) as exc_info: 15 | v.validate_test('foobar') 16 | # insert_assert(exc_info.value.errors(include_url=False)) 17 | assert exc_info.value.errors(include_url=False) == [ 18 | {'type': 'foobar', 'loc': (), 'msg': 'Hello there', 'input': 'foobar'} 19 | ] 20 | 21 | 22 | def test_custom_error_type(py_and_json: PyAndJson): 23 | v = py_and_json(core_schema.custom_error_schema(core_schema.int_schema(), 'recursion_loop')) 24 | assert v.validate_test(1) == 1 25 | 26 | with pytest.raises(ValidationError) as exc_info: 27 | v.validate_test('X') 28 | # insert_assert(exc_info.value.errors(include_url=False)) 29 | assert exc_info.value.errors(include_url=False) == [ 30 | {'type': 'recursion_loop', 'loc': (), 'msg': 'Recursion error - cyclic reference detected', 'input': 'X'} 31 | ] 32 | 33 | 34 | def test_custom_error_invalid(): 35 | msg = "custom_error_message should not be provided if 'custom_error_type' matches a known error" 36 | with pytest.raises(SchemaError, match=msg): 37 | SchemaValidator( 38 | schema=core_schema.custom_error_schema( 39 | core_schema.int_schema(), 'recursion_loop', custom_error_message='xxx' 40 | ) 41 | ) 42 | -------------------------------------------------------------------------------- /tests/validators/test_is_subclass.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema 4 | 5 | 6 | class Foo: 7 | pass 8 | 9 | 10 | class Foobar(Foo): 11 | pass 12 | 13 | 14 | class Bar: 15 | pass 16 | 17 | 18 | def test_is_subclass_basic(): 19 | v = SchemaValidator(core_schema.is_subclass_schema(Foo)) 20 | assert v.validate_python(Foo) == Foo 21 | with pytest.raises(ValidationError) as exc_info: 22 | v.validate_python(Bar) 23 | # insert_assert(exc_info.value.errors(include_url=False)) 24 | assert exc_info.value.errors(include_url=False) == [ 25 | { 26 | 'type': 'is_subclass_of', 27 | 'loc': (), 28 | 'msg': 'Input should be a subclass of Foo', 29 | 'input': Bar, 30 | 'ctx': {'class': 'Foo'}, 31 | } 32 | ] 33 | 34 | 35 | @pytest.mark.parametrize( 36 | 'input_value,valid', 37 | [ 38 | (Foo, True), 39 | (Foobar, True), 40 | (Bar, False), 41 | (type, False), 42 | (1, False), 43 | ('foo', False), 44 | (Foo(), False), 45 | (Foobar(), False), 46 | (Bar(), False), 47 | ], 48 | ) 49 | def test_is_subclass(input_value, valid): 50 | v = SchemaValidator(core_schema.is_subclass_schema(Foo)) 51 | assert v.isinstance_python(input_value) == valid 52 | 53 | 54 | def test_not_parent(): 55 | v = SchemaValidator(core_schema.is_subclass_schema(Foobar)) 56 | assert v.isinstance_python(Foobar) 57 | assert not v.isinstance_python(Foo) 58 | 59 | 60 | def test_invalid_type(): 61 | with pytest.raises(SchemaError, match="TypeError: 'Foo' object cannot be converted to 'PyType"): 62 | SchemaValidator(core_schema.is_subclass_schema(Foo())) 63 | 64 | 65 | def test_custom_repr(): 66 | v = SchemaValidator(core_schema.is_subclass_schema(Foo, cls_repr='Spam')) 67 | assert v.validate_python(Foo) == Foo 68 | with pytest.raises(ValidationError) as exc_info: 69 | v.validate_python(Bar) 70 | # insert_assert(exc_info.value.errors(include_url=False)) 71 | assert exc_info.value.errors(include_url=False) == [ 72 | { 73 | 'type': 'is_subclass_of', 74 | 'loc': (), 75 | 'msg': 'Input should be a subclass of Spam', 76 | 'input': Bar, 77 | 'ctx': {'class': 'Spam'}, 78 | } 79 | ] 80 | 81 | 82 | def test_is_subclass_json() -> None: 83 | v = SchemaValidator(core_schema.is_subclass_schema(Foo)) 84 | with pytest.raises(ValidationError) as exc_info: 85 | v.validate_json("'Foo'") 86 | assert exc_info.value.errors()[0]['type'] == 'needs_python_object' 87 | -------------------------------------------------------------------------------- /tests/validators/test_json_or_python.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaValidator, ValidationError 4 | from pydantic_core import core_schema as cs 5 | 6 | 7 | def test_json_or_python(): 8 | class Foo(str): 9 | def __eq__(self, o: object) -> bool: 10 | if isinstance(o, Foo) and super().__eq__(o): 11 | return True 12 | return False 13 | 14 | s = cs.json_or_python_schema( 15 | json_schema=cs.no_info_after_validator_function(Foo, cs.str_schema()), python_schema=cs.is_instance_schema(Foo) 16 | ) 17 | v = SchemaValidator(s) 18 | 19 | assert v.validate_python(Foo('abc')) == Foo('abc') 20 | with pytest.raises(ValidationError) as exc_info: 21 | v.validate_python('abc') 22 | assert exc_info.value.errors(include_url=False) == [ 23 | { 24 | 'type': 'is_instance_of', 25 | 'loc': (), 26 | 'msg': 'Input should be an instance of test_json_or_python..Foo', 27 | 'input': 'abc', 28 | 'ctx': {'class': 'test_json_or_python..Foo'}, 29 | } 30 | ] 31 | 32 | assert v.validate_json('"abc"') == Foo('abc') 33 | -------------------------------------------------------------------------------- /tests/validators/test_lax_or_strict.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaValidator, ValidationError, core_schema 4 | 5 | 6 | def test_lax_or_strict(): 7 | v = SchemaValidator(core_schema.lax_or_strict_schema(core_schema.str_schema(), core_schema.int_schema())) 8 | # validator is default - lax so with no runtime arg, we're in lax mode, and we use the string validator 9 | assert v.validate_python('aaa') == 'aaa' 10 | # the strict validator is itself lax 11 | assert v.validate_python(b'aaa') == 'aaa' 12 | # in strict mode 13 | assert v.validate_python(123, strict=True) == 123 14 | with pytest.raises(ValidationError) as exc_info: 15 | v.validate_python('123', strict=True) 16 | 17 | # location is not changed 18 | # insert_assert(exc_info.value.errors(include_url=False)) 19 | assert exc_info.value.errors(include_url=False) == [ 20 | {'type': 'int_type', 'loc': (), 'msg': 'Input should be a valid integer', 'input': '123'} 21 | ] 22 | 23 | 24 | def test_lax_or_strict_default_strict(): 25 | v = SchemaValidator( 26 | core_schema.lax_or_strict_schema(core_schema.str_schema(), core_schema.int_schema(), strict=True) 27 | ) 28 | assert v.validate_python('aaa', strict=False) == 'aaa' 29 | assert v.validate_python(b'aaa', strict=False) == 'aaa' 30 | # in strict mode 31 | assert v.validate_python(123) == 123 32 | assert v.validate_python(123, strict=True) == 123 33 | # the int validator isn't strict since it wasn't configured that way and strictness wasn't overridden at runtime 34 | assert v.validate_python('123') == 123 35 | 36 | # but it is if we set `strict` to True 37 | with pytest.raises(ValidationError, match='Input should be a valid integer'): 38 | v.validate_python('123', strict=True) 39 | -------------------------------------------------------------------------------- /tests/validators/test_none.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_core import SchemaValidator, ValidationError 4 | from pydantic_core import core_schema as cs 5 | 6 | 7 | def test_python_none(): 8 | v = SchemaValidator(cs.none_schema()) 9 | assert v.validate_python(None) is None 10 | with pytest.raises(ValidationError) as exc_info: 11 | v.validate_python(1) 12 | assert exc_info.value.errors(include_url=False) == [ 13 | {'type': 'none_required', 'loc': (), 'msg': 'Input should be None', 'input': 1} 14 | ] 15 | 16 | 17 | def test_json_none(): 18 | v = SchemaValidator(cs.none_schema()) 19 | assert v.validate_json('null') is None 20 | with pytest.raises(ValidationError) as exc_info: 21 | v.validate_json('1') 22 | assert exc_info.value.errors(include_url=False) == [ 23 | {'type': 'none_required', 'loc': (), 'msg': 'Input should be null', 'input': 1} 24 | ] 25 | -------------------------------------------------------------------------------- /tests/validators/test_nullable.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import weakref 3 | 4 | import pytest 5 | 6 | from pydantic_core import SchemaValidator, ValidationError, core_schema 7 | 8 | from ..conftest import assert_gc 9 | 10 | 11 | def test_nullable(): 12 | v = SchemaValidator(core_schema.nullable_schema(schema=core_schema.int_schema())) 13 | assert v.validate_python(None) is None 14 | assert v.validate_python(1) == 1 15 | assert v.validate_python('123') == 123 16 | with pytest.raises(ValidationError) as exc_info: 17 | v.validate_python('hello') 18 | assert exc_info.value.errors(include_url=False) == [ 19 | { 20 | 'type': 'int_parsing', 21 | 'loc': (), 22 | 'msg': 'Input should be a valid integer, unable to parse string as an integer', 23 | 'input': 'hello', 24 | } 25 | ] 26 | 27 | 28 | def test_union_nullable_bool_int(): 29 | v = SchemaValidator( 30 | core_schema.union_schema( 31 | choices=[ 32 | core_schema.nullable_schema(schema=core_schema.bool_schema()), 33 | core_schema.nullable_schema(schema=core_schema.int_schema()), 34 | ] 35 | ) 36 | ) 37 | assert v.validate_python(None) is None 38 | assert v.validate_python(True) is True 39 | assert v.validate_python(1) == 1 40 | 41 | 42 | @pytest.mark.xfail( 43 | condition=platform.python_implementation() == 'PyPy', reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899' 44 | ) 45 | def test_leak_nullable(): 46 | def fn(): 47 | def validate(v, info): 48 | return v 49 | 50 | schema = core_schema.with_info_plain_validator_function(validate) 51 | schema = core_schema.nullable_schema(schema) 52 | 53 | # If any of the Rust validators don't implement traversal properly, 54 | # there will be an undetectable cycle created by this assignment 55 | # which will keep Defaulted alive 56 | validate.__pydantic_validator__ = SchemaValidator(schema) 57 | 58 | return validate 59 | 60 | cycle = fn() 61 | ref = weakref.ref(cycle) 62 | assert ref() is not None 63 | 64 | del cycle 65 | 66 | assert_gc(lambda: ref() is None) 67 | -------------------------------------------------------------------------------- /tests/validators/test_pickling.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import re 3 | from datetime import datetime, timedelta, timezone 4 | 5 | import pytest 6 | 7 | from pydantic_core import core_schema 8 | from pydantic_core._pydantic_core import SchemaValidator, ValidationError 9 | 10 | 11 | def test_basic_schema_validator(): 12 | v = SchemaValidator( 13 | {'type': 'dict', 'strict': True, 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}} 14 | ) 15 | v = pickle.loads(pickle.dumps(v)) 16 | assert v.validate_python({'1': 2, '3': 4}) == {1: 2, 3: 4} 17 | assert v.validate_python({}) == {} 18 | with pytest.raises(ValidationError, match=re.escape('[type=dict_type, input_value=[], input_type=list]')): 19 | v.validate_python([]) 20 | 21 | 22 | def test_schema_validator_containing_config(): 23 | """ 24 | Verify that the config object is not lost during (de)serialization. 25 | """ 26 | v = SchemaValidator( 27 | core_schema.model_fields_schema({'f': core_schema.model_field(core_schema.str_schema())}), 28 | config=core_schema.CoreConfig(extra_fields_behavior='allow'), 29 | ) 30 | v = pickle.loads(pickle.dumps(v)) 31 | 32 | m, model_extra, fields_set = v.validate_python({'f': 'x', 'extra_field': '123'}) 33 | assert m == {'f': 'x'} 34 | # If the config was lost during (de)serialization, the below checks would fail as 35 | # the default behavior is to ignore extra fields. 36 | assert model_extra == {'extra_field': '123'} 37 | assert fields_set == {'f', 'extra_field'} 38 | 39 | v.validate_assignment(m, 'f', 'y') 40 | assert m == {'f': 'y'} 41 | 42 | 43 | def test_schema_validator_tz_pickle() -> None: 44 | """ 45 | https://github.com/pydantic/pydantic-core/issues/589 46 | """ 47 | v = SchemaValidator(core_schema.datetime_schema()) 48 | original = datetime(2022, 6, 8, 12, 13, 14, tzinfo=timezone(timedelta(hours=-12, minutes=-15))) 49 | validated = v.validate_python('2022-06-08T12:13:14-12:15') 50 | assert validated == original 51 | assert pickle.loads(pickle.dumps(validated)) == validated == original 52 | 53 | 54 | # Should be defined at the module level for pickling to work: 55 | class Model: 56 | __pydantic_validator__: SchemaValidator 57 | __pydantic_complete__ = True 58 | 59 | 60 | def test_schema_validator_not_reused_when_unpickling() -> None: 61 | s = SchemaValidator( 62 | core_schema.model_schema( 63 | cls=Model, 64 | schema=core_schema.model_fields_schema(fields={}, model_name='Model'), 65 | config={'title': 'Model'}, 66 | ref='Model:123', 67 | ) 68 | ) 69 | 70 | Model.__pydantic_validator__ = s 71 | assert 'Prebuilt' not in str(Model.__pydantic_validator__) 72 | 73 | reconstructed = pickle.loads(pickle.dumps(Model.__pydantic_validator__)) 74 | assert 'Prebuilt' not in str(reconstructed) 75 | -------------------------------------------------------------------------------- /wasm-preview/README.md: -------------------------------------------------------------------------------- 1 | # Demonstration of pydantic-core unit tests running in the browser 2 | 3 | To run tests in your browser, go [here](https://githubproxy.samuelcolvin.workers.dev/pydantic/pydantic-core/blob/main/wasm-preview/index.html). 4 | 5 | To test with a specific version of pydantic-core, add a query parameter `?pydantic_core_version=...` to the URL, e.g. `?pydantic_core_version=v2.4.0`, defaults to latest release. 6 | 7 | This doesn't work for version of pydantic-core before v0.23.0 as before that we built 3.10 binaries, and pyodide now rust 3.11. 8 | 9 | If the output appears to stop prematurely, try looking in the developer console for more details. 10 | 11 | For pydantic-core versions prior to `2.2.0`, tests will freeze at at 10-15% of the way through on Chrome due to a suspected V8 bug, see [pyodide/pyodide#3792](https://github.com/pyodide/pyodide/issues/3792) for more information. 12 | -------------------------------------------------------------------------------- /wasm-preview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pydantic-core unit tests 6 | 39 | 40 | 41 |
42 |

pydantic-core unit tests

43 | 47 |
48 |
loading...
49 |
50 |
51 | 52 | 53 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /wasm-preview/run_tests.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import importlib 3 | import re 4 | import sys 5 | import traceback 6 | from io import BytesIO 7 | from pathlib import Path 8 | from zipfile import ZipFile 9 | 10 | import micropip 11 | import pyodide 12 | import pytest 13 | 14 | # this seems to be required for me on M1 Mac 15 | sys.setrecursionlimit(200) 16 | 17 | 18 | async def main(tests_zip: str, tag_name: str): 19 | print(f'Using pyodide version: {pyodide.__version__}') 20 | print(f'Extracting test files (size: {len(tests_zip):,})...') 21 | # File saved on the GH release 22 | pydantic_core_wheel = ( 23 | 'https://githubproxy.samuelcolvin.workers.dev/pydantic/pydantic-core/releases/' 24 | f'download/{tag_name}/pydantic_core-{tag_name.lstrip("v")}-cp312-cp312-emscripten_3_1_58_wasm32.whl' 25 | ) 26 | zip_file = ZipFile(BytesIO(base64.b64decode(tests_zip))) 27 | count = 0 28 | for name in zip_file.namelist(): 29 | if name.endswith('.py'): 30 | path, subs = re.subn(r'^pydantic-core-.+?/tests/', 'tests/', name) 31 | if subs: 32 | count += 1 33 | path = Path(path) 34 | path.parent.mkdir(parents=True, exist_ok=True) 35 | with zip_file.open(name, 'r') as f: 36 | path.write_bytes(f.read()) 37 | 38 | print(f'Mounted {count} test files, installing dependencies...') 39 | 40 | await micropip.install( 41 | [ 42 | 'dirty-equals', 43 | 'hypothesis', 44 | 'pytest-speed', 45 | 'pytest-mock', 46 | 'tzdata', 47 | 'inline-snapshot<0.21', 48 | pydantic_core_wheel, 49 | ] 50 | ) 51 | importlib.invalidate_caches() 52 | 53 | # print('installed packages:') 54 | # print(micropip.list()) 55 | print('Running tests...') 56 | pytest.main() 57 | 58 | 59 | try: 60 | await main(tests_zip, pydantic_core_version) # noqa: F821,F704 61 | except Exception: 62 | traceback.print_exc() 63 | raise 64 | -------------------------------------------------------------------------------- /wasm-preview/worker.js: -------------------------------------------------------------------------------- 1 | let chunks = []; 2 | let last_post = 0; 3 | 4 | function print(tty) { 5 | if (tty.output && tty.output.length > 0) { 6 | chunks.push(tty.output); 7 | tty.output = []; 8 | const now = performance.now(); 9 | if (now - last_post > 100) { 10 | post(); 11 | last_post = now; 12 | } 13 | } 14 | } 15 | 16 | function post() { 17 | self.postMessage(chunks); 18 | chunks = []; 19 | } 20 | 21 | function make_tty_ops() { 22 | return { 23 | put_char(tty, val) { 24 | if (val !== null) { 25 | tty.output.push(val); 26 | } 27 | if (val === null || val === 10) { 28 | print(tty); 29 | } 30 | }, 31 | fsync(tty) { 32 | print(tty); 33 | }, 34 | }; 35 | } 36 | 37 | function setupStreams(FS, TTY) { 38 | let mytty = FS.makedev(FS.createDevice.major++, 0); 39 | let myttyerr = FS.makedev(FS.createDevice.major++, 0); 40 | TTY.register(mytty, make_tty_ops()); 41 | TTY.register(myttyerr, make_tty_ops()); 42 | FS.mkdev('/dev/mytty', mytty); 43 | FS.mkdev('/dev/myttyerr', myttyerr); 44 | FS.unlink('/dev/stdin'); 45 | FS.unlink('/dev/stdout'); 46 | FS.unlink('/dev/stderr'); 47 | FS.symlink('/dev/mytty', '/dev/stdin'); 48 | FS.symlink('/dev/mytty', '/dev/stdout'); 49 | FS.symlink('/dev/myttyerr', '/dev/stderr'); 50 | FS.closeStream(0); 51 | FS.closeStream(1); 52 | FS.closeStream(2); 53 | FS.open('/dev/stdin', 0); 54 | FS.open('/dev/stdout', 1); 55 | FS.open('/dev/stderr', 1); 56 | } 57 | 58 | async function get(url, mode) { 59 | const r = await fetch(url); 60 | if (r.ok) { 61 | if (mode === 'text') { 62 | return await r.text(); 63 | } else if (mode === 'json') { 64 | return await r.json(); 65 | } else { 66 | const blob = await r.blob(); 67 | let buffer = await blob.arrayBuffer(); 68 | return btoa(new Uint8Array(buffer).reduce((data, byte) => data + String.fromCharCode(byte), '')); 69 | } 70 | } else { 71 | let text = await r.text(); 72 | console.error('unexpected response', r, text); 73 | throw new Error(`${r.status}: ${text}`); 74 | } 75 | } 76 | 77 | async function main() { 78 | const query_args = new URLSearchParams(location.search); 79 | let pydantic_core_version = query_args.get('pydantic_core_version'); 80 | if (!pydantic_core_version) { 81 | const latest_release = await get('https://api.github.com/repos/pydantic/pydantic-core/releases/latest', 'json'); 82 | pydantic_core_version = latest_release.tag_name; 83 | } 84 | self.postMessage(`Running tests against latest pydantic-core release (${pydantic_core_version}).\n`); 85 | self.postMessage(`Downloading repo archive to get tests...\n`); 86 | const zip_url = `https://githubproxy.samuelcolvin.workers.dev/pydantic/pydantic-core/archive/refs/tags/${pydantic_core_version}.zip`; 87 | try { 88 | const [python_code, tests_zip] = await Promise.all([ 89 | get(`./run_tests.py?v=${Date.now()}`, 'text'), 90 | // e4cf2e2 commit matches the pydantic-core wheel being used, so tests should pass 91 | get(zip_url, 'blob'), 92 | importScripts('https://cdn.jsdelivr.net/pyodide/v0.26.3/full/pyodide.js'), 93 | ]); 94 | 95 | const pyodide = await loadPyodide(); 96 | const {FS} = pyodide; 97 | setupStreams(FS, pyodide._module.TTY); 98 | FS.mkdir('/test_dir'); 99 | FS.chdir('/test_dir'); 100 | await pyodide.loadPackage(['micropip', 'pytest', 'numpy', 'pygments']); 101 | if (pydantic_core_version < '2.0.0') await pyodide.loadPackage(['typing-extensions']); 102 | await pyodide.runPythonAsync(python_code, {globals: pyodide.toPy({pydantic_core_version, tests_zip})}); 103 | post(); 104 | } catch (err) { 105 | console.error(err); 106 | self.postMessage(`Error: ${err}\n`); 107 | } 108 | } 109 | 110 | main(); 111 | --------------------------------------------------------------------------------