8000 Add pipeline API by adriangb · Pull Request #9459 · pydantic/pydantic · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
e7eb43c
Add pipeline API
adriangb May 20, 2024
7d78407
port to python 3.10
adriangb May 20, 2024
6a1622d
port to python 3.10
adriangb May 20, 2024
947e920
fix syntax
adriangb May 20, 2024
b3fe1 8000 c4
handle slots
adriangb May 20, 2024
a6d56cc
Remove match
adriangb May 21, 2024
42505df
Remove match
adriangb May 21, 2024
b2855d0
ignore warning
adriangb May 21, 2024
aafb856
fix import
adriangb May 21, 2024
0bd8b39
fix union
adriangb May 21, 2024
697833f
fix union
adriangb May 21, 2024
d767731
sort imports
adriangb May 21, 2024
8abb6e4
move
adriangb May 21, 2024
217b11d
move
adriangb May 21, 2024
89c46a1
add missing file
adriangb May 21, 2024
6e91a32
namespace
adriangb May 23, 2024
8e4d535
initial tests
sydney-runkle May 29, 2024
ada5853
add more operators
adriangb May 30, 2024
8742e9e
Add json schema tests, add section mapping existing validators
adriangb May 31, 2024
f55b6e1
move things around for expeirmental pattern
sydney-runkle May 31, 2024
7132bae
fix docs tests
sydney-runkle May 31, 2024
0444fc9
maybe fix 3.9 test
sydney-runkle May 31, 2024
1a8e505
use typing Pattern
sydney-runkle May 31, 2024
d979841
add PydanticExperimentalWarning
sydney-runkle May 31, 2024
fadf3bb
ignore warnings, for some reason pytestmark wasn't working
sydney-runkle May 31, 2024
1699f35
3.8 friendly removesuffix
sydney-runkle May 31, 2024
d0a9372
Apply docs suggestions from code review
sydney-runkle Jun 4, 2024
bed0752
add __all__
adriangb Jun 4, 2024
eb61549
rename class to pipeline
adriangb Jun 4, 2024
a18a4df
get rid of on_lambda_err
adriangb Jun 4, 2024
34663fe
pr feedback
adriangb Jun 4, 2024
dff9ad9
make transform use the field type instead of any
adriangb Jun 4, 2024
479ab3c
add import
adriangb Jun 4, 2024
7b49219
rename parse() -> validate_as()
adriangb Jun 4, 2024
51bcad6
rename internal classes
adriangb Jun 4, 2024
13b1721
make Pipeline _Pipeline
adriangb Jun 4, 2024
b8573b5
Remove namespaces
adriangb Jun 4, 2024
888c4ed
more test
adriangb Jun 4, 2024
141c8b6
use ellipsis
sydney-runkle Jun 4, 2024
9d4194b
updating imports from internal test
sydney-runkle Jun 4, 2024
128d4ea
maybe fixing zoneinfo tests, switching up validate_as annotation again
sydney-runkle Jun 4, 2024
1c7302d
docs and linting
sydney-runkle Jun 4, 2024
88dcb75
removing tzinfo stuff :(
sydney-runkle Jun 5, 2024
19a3ee6
a bit more explanation
sydney-runkle Jun 5, 2024
0652472
api docs update
sydney-runkle Jun 5, 2024
4ccf4e5
Additional Test Cases for Experimental Pipeline API (#9566)
dAIsySHEng1 Jun 5, 2024
bad0a1a
fix common predicates + add tests
sydney-runkle Jun 5, 2024
8000
a9d1099
remove unneeded line
sydney-runkle Jun 5, 2024
14e9944
update to version policy docs
sydney-runkle Jun 5, 2024
42a2708
skip linting
sydney-runkle Jun 5, 2024
021604f
fix type hint for _Pipeline.then
adriangb Jun 5, 2024
38a2730
Apply suggestions from code review
sydney-runkle Jun 5, 2024
0c36b7c
Update pydantic/experimental/pipeline.py
sydney-runkle Jun 5, 2024
8d46b21
add public todo
sydney-runkle Jun 5, 2024
a46c2e3
move predicate up
sydney-runkle Jun 5, 2024
7386d69
new idea for overload
sydney-runkle Jun 5, 2024
dc07b50
test fixes
sydney-runkle Jun 5, 2024
cbb216b
update test cases with comments
sydney-runkle Jun 5, 2024
581cbe8
no freeze notes
sydney-runkle Jun 5, 2024
c3a008f
suggested frozen change
sydney-runkle Jun 5, 2024
26c5325
add test
adriangb Jun 5, 2024
166df3d
add more assertions
adriangb Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/api/experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
::: pydantic.experimental.pipeline
options:
members:
- _Pipeline
116 changes: 116 additions & 0 deletions docs/concepts/experimental.md
8000
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Experimental Features

In this section you will find documentation for new, experimental features in Pydantic. These features are subject to change or removal, and we are looking for feedback and suggestions before making them a permanent part of Pydantic.

See our [Version Policy](../version-policy.md#experimental-features) for more information on experimental features.

## Feedback

We welcome feedback on experimental features! Please open an issue on the [Pydantic GitHub repository](https://github.com/pydantic/pydantic/issues/new/choose) to share your thoughts, requests, or suggestions.

We also encourage you to read through existing feedback and add your thoughts to existing issues.

## Warnings on Import

When you import an experimental feature from the `experimental` module, you'll see a warning message that the feature is experimental. You can disable this warning with the following:

```python
import warnings

from pydantic import PydanticExperimentalWarning

warnings.filterwarnings('ignore', category=PydanticExperimentalWarning)
```

## Pipeline API

Pydantic v2.8.0 introduced an experimental "pipeline" API that allows composing of parsing (validation), constraints and transformations in a more type-safe manner than existing APIs. This API is subject to change or removal, we are looking for feedback and suggestions before making it a permanent part of Pydantic.

??? api "API Documentation"
[`pydantic.experimental.pipeline`][pydantic.experimental.pipeline]<br>

Generally, the pipeline API is used to define a sequence of steps to apply to incoming data during validation. The pipeline API is designed to be more type-safe and composable than the existing Pydantic API.

Each step in the pipeline can be:
* A validation step that runs pydantic validation on the provided type
* A transformation step that modifies the data
* A constraint step that checks the data against a condition
* A predicate step that checks the data against a condition and raises an error if it returns `False`

<!-- TODO: (@sydney-runkle) add more documentation once we solidify the API during the experimental phase -->

```python
from __future__ import annotations

from datetime import datetime

from typing_extensions import Annotated

from pydantic import BaseModel
from pydantic.experimental.pipeline import validate_as, validate_as_deferred


class User(BaseModel):
name: Annotated[str, validate_as(str).str_lower()] # (1)!
age: Annotated[int, validate_as(int).gt(0)] # (2)!
username: Annotated[str, validate_as(str).str_pattern(r'[a-z]+')] # (3)!
password: Annotated[
str,
validate_as(str)
.transform(str.lower)
.predicate(lambda x: x != 'password'), # (4)!
]
favorite_number: Annotated[ # (5)!
int,
(validate_as(int) | validate_as(str).str_strip().validate_as(int)).gt(
0
),
]
friends: Annotated[list[User], validate_as(...).len(0, 100)] # (6)!
family: Annotated[ # (7)!
list[User],
validate_as_deferred(lambda: list[User]).transform(lambda x: x[1:]),
]
bio: Annotated[
datetime,
validate_as(int)
.transform(lambda x: x / 1_000_000)
.validate_as(...), # (8)!
]
```

1. Lowercase a string.
2. Constrain an integer to be greater than zero.
3. Constrain a string to match a regex pattern.
4. You can also use the lower level transform, constrain and predicate methods.
5. Use the `|` or `&` operators to combine steps (like a logical OR or AND).
6. Calling `validate_as(...)` with `Ellipsis`, `...` as the first positional argument implies `validate_as(<field type>)`. Use `validate_as(Any)` to accept any type.
7. For recursive types you can use `validate_as_deferred` to reference the type itself before it's defined.
8. You can call `validate_as()` before or after other steps to do pre or post processing.

### Mapping from `BeforeValidator`, `AfterValidator` and `WrapValidator`

The `validate_as` method is a more type-safe way to define `BeforeValidator`, `AfterValidator` and `WrapValidator`:

```python
from typing_extensions import Annotated

from pydantic.experimental.pipeline import transform, validate_as

# BeforeValidator
Annotated[int, validate_as(str).str_strip().validate_as(...)] # (1)!
# AfterValidator
Annotated[int, transform(lambda x: x * 2)] # (2)!
# WrapValidator
Annotated[
int,
validate_as(str)
.str_strip()
.validate_as(...)
.transform(lambda x: x * 2), # (3)!
]
```

1. Strip whitespace from a string before parsing it as an integer.
2. Multiply an integer by 2 after parsing it.
3. Strip whitespace from a string, validate it as an integer, then multiply it by 2.
48 changes: 48 additions & 0 deletions docs/version-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,54 @@ In all cases we will aim to minimize churn and do so only when justified by the

We expect to make new major releases roughly once a year going forward, although as mentioned above, any associated breaking changes should be trivial to fix compared to the V1-to-V2 transition.

## Experimental Features

At Pydantic, we like to move quickly and innovate! To that end, we may introduce experimental features in minor releases.

!!! abstract "Usage Documentation"
To learn more about our current experimental features, see the [experimental features documentation](./concepts/experimental.md).

Please keep in mind, experimental features are active works in progress. If these features are successful, they'll eventually beocme part of Pydantic. If unsuccessful, said features will be removed with little notice. While in its experimental phase, a feature's API and behaviors may not be stable, and it's very possible that changes made to the feature will not be backward-compatible.

### Naming Conventions

We use one of the following naming conventions to indicate that a feature is experimental:

1. The feature is located in the `experimental` module. In this case, you can access the feature like this:

```python test="skip" lint="skip"
from pydantic.experimental import feature_name
```

2. The feature is located in the main module, but prefixed with `experimental_`. This case occurs when we add a new field, argument, or method to an existing data structure already within the main `pydantic` module.

New features with these naming conventions are subject to change or removal, and we are looking for feedback and suggestions before making them a permanent part of Pydantic. See the [feedback section](./concepts/experimental.md#feedback) for more information.

### Importing Experimental Features

When you import an experimental feature from the `experimental` module, you'll see a warning message that the feature is experimental. You can disable this warning with the following:

```python
import warnings

from pydantic import PydanticExperimentalWarning

warnings.filterwarnings('ignore', category=PydanticExperimentalWarning)
```

### Lifecycle of Experimental Features

1. A new feature is added, either in the `experimental` module or with the `experimental_` prefix.
2. The behavior is often modified during patch/minor releases, with potential API/behavior changes.
3. If the feature is successful, we promote it to Pydantic with the following steps:
a. If it was in the `experimental` module, the feature is cloned to Pydantic's main module. The original experimental feature still remains in the `experimental` module, but it will show a warning when used. If the feature was already in the main Pydantic module, we create a copy of the feature without the `experimental_` prefix, so the feature exists with both the official and experimental names. A deprecation warning is attached to the experimental version.
b. At some point, the code of the experimental feature is removed, but there will still be a stub of the feature that provides an error message with appropriate instructions.
c. As a last step, the experimental version of the feature is entirely removed from the codebase.

If the feature is unsuccessful or unpopular, it's removed with little notice. A stub will remain in the location of the deprecated feature with an error message.

Thanks to [streamlit](https://docs.streamlit.io/develop/quick-reference/prerelease) for the inspiration for the lifecycle and naming conventions of our new experimental feature patterns.

## Support for Python versions

Pydantic will drop support for a Python version when the following conditions are met:
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ nav:
- Settings Management: concepts/pydantic_settings.md
- Performance: concepts/performance.md
- Pydantic Plugins: concepts/plugins.md
- Experimental: concepts/experimental.md
- API Documentation:
- Pydantic:
- BaseModel: api/base_model.md
Expand All @@ -124,6 +125,7 @@ nav:
- Version Information: api/version.md
- Pydantic Plugins: api/plugin.md
- Annotated Handlers: api/annotated_handlers.md
- Experimental: api/experimental.md
- Pydantic Core:
- pydantic_core: api/pydantic_core.md
- pydantic_core.core_schema: api/pydantic_core_schema.md
Expand Down
9 changes: 8 additions & 1 deletion pydantic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@
from .type_adapter import TypeAdapter
from .types import *
from .validate_call_decorator import validate_call
from .warnings import PydanticDeprecatedSince20, PydanticDeprecatedSince26, PydanticDeprecationWarning
from .warnings import (
PydanticDeprecatedSince20,
PydanticDeprecatedSince26,
PydanticDeprecationWarning,
PydanticExperimentalWarning,
)

# this encourages pycharm to import `ValidationError` from here, not pydantic_core
ValidationError = pydantic_core.ValidationError
Expand Down Expand Up @@ -203,6 +208,7 @@
'PydanticDeprecatedSince20',
'PydanticDeprecatedSince26',
'PydanticDeprecationWarning',
'PydanticExperimentalWarning',
# annotated handlers
'GetCoreSchemaHandler',
'GetJsonSchemaHandler',
Expand Down Expand Up @@ -354,6 +360,7 @@
'PydanticDeprecatedSince20': (__spec__.parent, '.warnings'),
'PydanticDeprecatedSince26': (__spec__.parent, '.warnings'),
'PydanticDeprecationWarning': (__spec__.parent, '.warnings'),
'PydanticExperimentalWarning': (__spec__.parent, '.warnings'),
# annotated handlers
'GetCoreSchemaHandler': (__spec__.parent, '.annotated_handlers'),
'GetJsonSchemaHandler': (__spec__.parent, '.annotated_handlers'),
Expand Down
2 changes: 1 addition & 1 deletion pydantic/_internal/_generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1805,7 +1805,7 @@ def _apply_annotations(
pydantic_js_annotation_functions: list[GetJsonSchemaFunction] = []

def inner_handler(obj: Any) -> CoreSchema:
from_property = self._generate_schema_from_property(obj, obj)
from_property = self._generate_schema_from_property(obj, source_type)
if from_property is None:
schema = self._generate_schema_inner(obj)
else:
Expand Down
3 changes: 0 additions & 3 deletions pydantic/_internal/_internal_dataclass.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import sys
from typing import Any, Dict

dataclass_kwargs: Dict[str, Any]

# `slots` is available on Python >= 3.10
if sys.version_info >= (3, 10):
Expand Down
10 changes: 10 additions & 0 deletions pydantic/experimental/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""The "experimental" module of pydantic contains potential new features that are subject to change."""

import warnings

from pydantic.warnings import PydanticExperimentalWarning

warnings.warn(
'This module is experimental, its contents are subject to change and deprecation.',
category=PydanticExperimentalWarning,
)
Loading
0