diff --git a/.github/actions/build-pgo-wheel/action.yml b/.github/actions/build-pgo-wheel/action.yml new file mode 100644 index 000000000..0e0ee4e99 --- /dev/null +++ b/.github/actions/build-pgo-wheel/action.yml @@ -0,0 +1,74 @@ +name: Build PGO wheel +description: Builds a PGO-optimized wheel +inputs: + interpreter: + description: 'Interpreter to build the wheel for' + required: true + rust-toolchain: + description: 'Rust toolchain to use' + required: true +outputs: + wheel: + description: 'Path to the built wheel' + value: ${{ steps.find_wheel.outputs.path }} +runs: + using: "composite" + steps: + - name: prepare self schema + shell: bash + # generate up front so that we don't have to do this inside the docker container + run: uv run python generate_self_schema.py + + - name: prepare profiling directory + shell: bash + # making this ahead of the compile ensures that the local user can write to this + # directory; the maturin action (on linux) runs in docker so would create as root + run: mkdir -p ${{ github.workspace }}/profdata + + - name: build initial wheel + uses: PyO3/maturin-action@v1 + with: + manylinux: auto + args: > + --release + --out pgo-wheel + --interpreter ${{ inputs.interpreter }} + rust-toolchain: ${{ inputs.rust-toolchain }} + docker-options: -e CI + env: + RUSTFLAGS: '-Cprofile-generate=${{ github.workspace }}/profdata' + + - name: detect rust host + run: echo RUST_HOST=$(rustc -Vv | grep host | cut -d ' ' -f 2) >> "$GITHUB_ENV" + shell: bash + + - name: generate pgo data + run: | + uv sync --group testing + uv pip install pydantic-core --no-index --no-deps --find-links pgo-wheel --force-reinstall + uv run pytest tests/benchmarks + RUST_HOST=$(rustc -Vv | grep host | cut -d ' ' -f 2) + rustup run ${{ inputs.rust-toolchain }} bash -c 'echo LLVM_PROFDATA=$RUSTUP_HOME/toolchains/$RUSTUP_TOOLCHAIN/lib/rustlib/$RUST_HOST/bin/llvm-profdata >> "$GITHUB_ENV"' + shell: bash + + - name: merge pgo data + run: ${{ env.LLVM_PROFDATA }} merge -o ${{ github.workspace }}/merged.profdata ${{ github.workspace }}/profdata + shell: pwsh # because it handles paths on windows better, and works well enough on unix for this step + + - name: build pgo-optimized wheel + uses: PyO3/maturin-action@v1 + with: + manylinux: auto + args: > + --release + --out dist + --interpreter ${{ inputs.interpreter }} + rust-toolchain: ${{inputs.rust-toolchain}} + docker-options: -e CI + env: + RUSTFLAGS: '-Cprofile-use=${{ github.workspace }}/merged.profdata' + + - name: find built wheel + id: find_wheel + run: echo "path=$(ls dist/*.whl)" | tee -a "$GITHUB_OUTPUT" + shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f7e38a43..a08a25ea2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -556,50 +556,12 @@ jobs: # FIXME: Unpin when Python 3.8 support is dropped. (3.9 requires Windows 10) toolchain: ${{ (matrix.os == 'windows' && '1.77') || 'stable' }} - - run: pip install -U 'ruff==0.5.0' typing_extensions - - # generate self-schema now, so we don't have to do so inside docker in maturin build - - run: python generate_self_schema.py - - - name: build initial wheel - uses: PyO3/maturin-action@v1 + - name: Build PGO wheel + id: pgo-wheel + uses: ./.github/actions/build-pgo-wheel with: - manylinux: auto - args: > - --release - --out pgo-wheel - --interpreter ${{ matrix.interpreter }} + interpreter: ${{ env.UV_PYTHON }} rust-toolchain: ${{ steps.rust-toolchain.outputs.name }} - docker-options: -e CI - env: - RUSTFLAGS: '-Cprofile-generate=${{ github.workspace }}/profdata' - - - name: detect rust host - run: echo RUST_HOST=$(rustc -Vv | grep host | cut -d ' ' -f 2) >> "$GITHUB_ENV" - shell: bash - - - name: generate pgo data - run: | - uv sync --group testing - uv pip install pydantic-core --no-index --no-deps --find-links pgo-wheel --force-reinstall - uv run pytest tests/benchmarks - rustup run ${{ steps.rust-toolchain.outputs.name }} bash -c 'echo LLVM_PROFDATA=$RUSTUP_HOME/toolchains/$RUSTUP_TOOLCHAIN/lib/rustlib/${{ env.RUST_HOST }}/bin/llvm-profdata >> "$GITHUB_ENV"' - - - name: merge pgo data - run: ${{ env.LLVM_PROFDATA }} merge -o ${{ github.workspace }}/merged.profdata ${{ github.workspace }}/profdata - - - name: build pgo-optimized wheel - uses: PyO3/maturin-action@v1 - with: - manylinux: auto - args: > - --release - --out dist - --interpreter ${{ matrix.interpreter }} - rust-toolchain: ${{steps.rust-toolchain.outputs.name}} - docker-options: -e CI - env: - RUSTFLAGS: '-Cprofile-use=${{ github.workspace }}/merged.profdata' - run: ${{ matrix.ls || 'ls -lh' }} dist/ diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 9ee91d25f..b8ddb3076 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -30,40 +30,36 @@ jobs: with: enable-cache: true - - name: install deps + - name: Install deps run: | uv sync --group testing uv pip uninstall pytest-speed uv pip install pytest-benchmark==4.0.0 pytest-codspeed - - name: install rust stable + - name: Install rust stable id: rust-toolchain uses: dtolnay/rust-toolchain@stable with: components: llvm-tools - - name: cache rust + - name: Cache rust uses: Swatinem/rust-cache@v2 - - name: Compile pydantic-core for profiling - run: make build-profiling + - name: Build PGO wheel + id: pgo-wheel + uses: ./.github/actions/build-pgo-wheel + with: + interpreter: ${{ env.UV_PYTHON }} + rust-toolchain: ${{ steps.rust-toolchain.outputs.name }} env: - CONST_RANDOM_SEED: 0 # Fix the compile time RNG seed - RUSTFLAGS: "-Cprofile-generate=${{ github.workspace }}/profdata" - - - name: Gather pgo data - run: uv run pytest tests/benchmarks + # make sure profiling information is present + CARGO_PROFILE_RELEASE_DEBUG: "line-tables-only" + CARGO_PROFILE_RELEASE_STRIP: false - - name: Prepare merged pgo data - run: rustup run stable bash -c '$RUSTUP_HOME/toolchains/$RUSTUP_TOOLCHAIN/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata merge -o ${{ github.workspace }}/merged.profdata ${{ github.workspace }}/profdata' - - - name: Compile pydantic-core for benchmarking - run: make build-profiling - env: - CONST_RANDOM_SEED: 0 # Fix the compile time RNG seed - RUSTFLAGS: "-Cprofile-use=${{ github.workspace }}/merged.profdata" + - name: Install PGO wheel + run: uv pip install ${{ steps.pgo-wheel.outputs.wheel }} --force-reinstall - name: Run CodSpeed benchmarks uses: CodSpeedHQ/action@v3 with: - run: uv run pytest tests/benchmarks/ --codspeed + run: uv run --group=codspeed pytest tests/benchmarks/ --codspeed diff --git a/Cargo.lock b/Cargo.lock index 08a290071..6026418bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -425,7 +425,7 @@ dependencies = [ [[package]] name = "pydantic-core" -version = "2.27.1" +version = "2.27.2" dependencies = [ "ahash", "base64", diff --git a/Cargo.toml b/Cargo.toml index 3b3b8a9bc..661240c90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pydantic-core" -version = "2.27.1" +version = "2.27.2" edition = "2021" license = "MIT" homepage = "https://github.com/pydantic/pydantic-core" diff --git a/pyproject.toml b/pyproject.toml index de04a3991..9edafc5cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,10 @@ wasm = [ 'maturin>=1,<2', 'ruff', ] +codspeed = [ + # codspeed is only run on CI, with latest version of CPython + 'pytest-codspeed; python_version == "3.13" and implementation_name == "cpython"', +] all = [ { include-group = 'testing' }, diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 0fcfe9cab..e588e7721 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -1970,6 +1970,7 @@ class _ValidatorFunctionSchema(TypedDict, total=False): class BeforeValidatorFunctionSchema(_ValidatorFunctionSchema, total=False): type: Required[Literal['function-before']] + json_schema_input_schema: CoreSchema def no_info_before_validator_function( @@ -1977,6 +1978,7 @@ def no_info_before_validator_function( schema: CoreSchema, *, ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, ) -> BeforeValidatorFunctionSchema: @@ -2002,6 +2004,7 @@ def fn(v: bytes) -> str: function: The validator function to call schema: The schema to validate the output of the validator function ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ @@ -2010,6 +2013,7 @@ def fn(v: bytes) -> str: function={'type': 'no-info', 'function': function}, schema=schema, ref=ref, + json_schema_input_schema=json_schema_input_schema, metadata=metadata, serialization=serialization, ) @@ -2021,6 +2025,7 @@ def with_info_before_validator_function( *, field_name: str | None = None, ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, ) -> BeforeValidatorFunctionSchema: @@ -2050,6 +2055,7 @@ def fn(v: bytes, info: core_schema.ValidationInfo) -> str: field_name: The name of the field schema: The schema to validate the output of the validator function ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ @@ -2058,6 +2064,7 @@ def fn(v: bytes, info: core_schema.ValidationInfo) -> str: function=_dict_not_none(type='with-info', function=function, field_name=field_name), schema=schema, ref=ref, + json_schema_input_schema=json_schema_input_schema, metadata=metadata, serialization=serialization, ) @@ -2072,6 +2079,7 @@ def no_info_after_validator_function( schema: CoreSchema, *, ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, ) -> AfterValidatorFunctionSchema: @@ -2095,6 +2103,7 @@ def fn(v: str) -> str: function: The validator function to call after the schema is validated schema: The schema to validate before the validator function ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ @@ -2103,6 +2112,7 @@ def fn(v: str) -> str: function={'type': 'no-info', 'function': function}, schema=schema, ref=ref, + json_schema_input_schema=json_schema_input_schema, metadata=metadata, serialization=serialization, ) @@ -2188,6 +2198,7 @@ class WrapValidatorFunctionSchema(TypedDict, total=False): function: Required[WrapValidatorFunction] schema: Required[CoreSchema] ref: str + json_schema_input_schema: CoreSchema metadata: Dict[str, Any] serialization: SerSchema @@ -2197,6 +2208,7 @@ def no_info_wrap_validator_function( schema: CoreSchema, *, ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, ) -> WrapValidatorFunctionSchema: @@ -2225,6 +2237,7 @@ def fn( function: The validator function to call schema: The schema to validate the output of the validator function ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ @@ -2232,6 +2245,7 @@ def fn( type='function-wrap', function={'type': 'no-info', 'function': function}, schema=schema, + json_schema_input_schema=json_schema_input_schema, ref=ref, metadata=metadata, serialization=serialization, @@ -2243,6 +2257,7 @@ def with_info_wrap_validator_function( schema: CoreSchema, *, field_name: str | None = None, + json_schema_input_schema: CoreSchema | None = None, ref: str | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, @@ -2273,6 +2288,7 @@ def fn( function: The validator function to call schema: The schema to validate the output of the validator function field_name: The name of the field this validators is applied to, if any + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type ref: optional unique identifier of the schema, used to reference the schema in other places metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema @@ -2281,6 +2297,7 @@ def fn( type='function-wrap', function=_dict_not_none(type='with-info', function=function, field_name=field_name), schema=schema, + json_schema_input_schema=json_schema_input_schema, ref=ref, metadata=metadata, serialization=serialization, @@ -2291,6 +2308,7 @@ class PlainValidatorFunctionSchema(TypedDict, total=False): type: Required[Literal['function-plain']] function: Required[ValidationFunction] ref: str + json_schema_input_schema: CoreSchema metadata: Dict[str, Any] serialization: SerSchema @@ -2299,6 +2317,7 @@ def no_info_plain_validator_function( function: NoInfoValidatorFunction, *, ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, ) -> PlainValidatorFunctionSchema: @@ -2320,6 +2339,7 @@ def fn(v: str) -> str: Args: function: The validator function to call ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ @@ -2327,6 +2347,7 @@ def fn(v: str) -> str: type='function-plain', function={'type': 'no-info', 'function': function}, ref=ref, + json_schema_input_schema=json_schema_input_schema, metadata=metadata, serialization=serialization, ) @@ -2337,6 +2358,7 @@ def with_info_plain_validator_function( *, field_name: str | None = None, ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, ) -> PlainValidatorFunctionSchema: @@ -2359,6 +2381,7 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str: function: The validator function to call field_name: The name of the field this validators is applied to, if any ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ @@ -2366,6 +2389,7 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str: type='function-plain', function=_dict_not_none(type='with-info', function=function, field_name=field_name), ref=ref, + json_schema_input_schema=json_schema_input_schema, metadata=metadata, serialization=serialization, ) diff --git a/src/errors/validation_exception.rs b/src/errors/validation_exception.rs index 6fc0e96bd..bd01592b3 100644 --- a/src/errors/validation_exception.rs +++ b/src/errors/validation_exception.rs @@ -582,7 +582,7 @@ struct ValidationErrorSerializer<'py> { input_type: &'py InputType, } -impl<'py> Serialize for ValidationErrorSerializer<'py> { +impl Serialize for ValidationErrorSerializer<'_> { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -614,7 +614,7 @@ struct PyLineErrorSerializer<'py> { input_type: &'py InputType, } -impl<'py> Serialize for PyLineErrorSerializer<'py> { +impl Serialize for PyLineErrorSerializer<'_> { fn serialize(&self, serializer: S) -> Result where S: Serializer, diff --git a/src/input/datetime.rs b/src/input/datetime.rs index 577a91014..837bf0906 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -24,7 +24,7 @@ pub enum EitherDate<'a> { Py(Bound<'a, PyDate>), } -impl<'a> From for EitherDate<'a> { +impl From for EitherDate<'_> { fn from(date: Date) -> Self { Self::Raw(date) } @@ -45,21 +45,32 @@ pub fn pydate_as_date(py_date: &Bound<'_, PyAny>) -> PyResult { }) } -impl<'a> EitherDate<'a> { +impl<'py> EitherDate<'py> { + pub fn try_into_py(self, py: Python<'py>, input: &(impl Input<'py> + ?Sized)) -> ValResult { + match self { + Self::Raw(date) => { + if date.year == 0 { + return Err(ValError::new( + ErrorType::DateParsing { + error: Cow::Borrowed("year 0 is out of range"), + context: None, + }, + input, + )); + }; + let py_date = PyDate::new_bound(py, date.year.into(), date.month, date.day)?; + Ok(py_date.into()) + } + Self::Py(py_date) => Ok(py_date.into()), + } + } + pub fn as_raw(&self) -> PyResult { match self { Self::Raw(date) => Ok(date.clone()), Self::Py(py_date) => pydate_as_date(py_date), } } - - pub fn try_into_py(self, py: Python<'_>) -> PyResult { - let date = match self { - Self::Py(date) => Ok(date), - Self::Raw(date) => PyDate::new_bound(py, date.year.into(), date.month, date.day), - }?; - Ok(date.into_py(py)) - } } #[cfg_attr(debug_assertions, derive(Debug))] @@ -68,7 +79,7 @@ pub enum EitherTime<'a> { Py(Bound<'a, PyTime>), } -impl<'a> From