Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add types.OnErrorOmit #8222

Merged
merged 6 commits into from Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions pydantic/__init__.py
Expand Up @@ -205,6 +205,7 @@
'ValidatorFunctionWrapHandler',
'FieldSerializationInfo',
'SerializerFunctionWrapHandler',
'OnErrorOmit',
)

# A mapping of {<member name>: (package, <module name>)} defining dynamic imports
Expand Down Expand Up @@ -328,6 +329,7 @@
'Tag': (__package__, '.types'),
'Discriminator': (__package__, '.types'),
'JsonValue': (__package__, '.types'),
'OnErrorOmit': (__package__, '.types'),
# type_adapter
'TypeAdapter': (__package__, '.type_adapter'),
# warnings
Expand Down
22 changes: 22 additions & 0 deletions pydantic/types.py
Expand Up @@ -104,9 +104,13 @@
'Tag',
'Discriminator',
'JsonValue',
'OnErrorOmit',
)


T = TypeVar('T')


@_dataclasses.dataclass
class Strict(_fields.PydanticMetadata, BaseMetadata):
"""Usage docs: https://docs.pydantic.dev/2.6/concepts/strict_mode/#strict-mode-with-annotated-strict
Expand Down Expand Up @@ -2800,3 +2804,21 @@ class Model(BaseModel):
_AllowAnyJson,
],
)


class _OnErrorOmit:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should we make this a dataclass?

@dataclass
class OnError(Generic[T]):
    on_error: Literal['omit', 'raise', 'default']
    default: Any | Callable[[], T] = None

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like you want to fully expose the with_default_value options, but IMO even if we do that later on this use case is simple and common enough to warrant it's own annotation with no options so you don't need to do Annotated[..., OnError(on_error='omit')].

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR welcome for anyone to add OnError as described by me above.

@classmethod
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
# there is no actual default value here but we use with_default_schema since it already has the on_error
# behavior implemented and it would be no more efficient to implement it on every other validator
# or as a standalone validator
return core_schema.with_default_schema(schema=handler(source_type), on_error='omit')


OnErrorOmit = Annotated[T, _OnErrorOmit]
"""
When used as an item in a list, the key type in a dict, optional values of a TypedDict, etc.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key or value? we should probably add a test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did

this annotation omits the item from the iteration if there is any error validating it.
That is, instead of a ValidationError being propagated up and the entire iterable being discarded
adriangb marked this conversation as resolved.
Show resolved Hide resolved
any invalid items are discarded and the valid ones are returned.
"""
47 changes: 43 additions & 4 deletions tests/test_types.py
Expand Up @@ -40,14 +40,15 @@
import pytest
from dirty_equals import HasRepr, IsFloatNan, IsOneOf, IsStr
from pydantic_core import CoreSchema, PydanticCustomError, SchemaError, core_schema
from typing_extensions import Annotated, Literal, TypedDict, get_args
from typing_extensions import Annotated, Literal, NotRequired, TypedDict, get_args

from pydantic import (
UUID1,
UUID3,
UUID4,
UUID5,
AfterValidator,
AllowInfNan,
AwareDatetime,
Base64Bytes,
Base64Str,
Expand All @@ -64,6 +65,8 @@
FutureDate,
FutureDatetime,
GetCoreSchemaHandler,
GetPydanticSchema,
ImportString,
InstanceOf,
Json,
JsonValue,
Expand All @@ -76,20 +79,24 @@
NonNegativeInt,
NonPositiveFloat,
NonPositiveInt,
OnErrorOmit,
PastDate,
PastDatetime,
PositiveFloat,
PositiveInt,
PydanticInvalidForJsonSchema,
PydanticSchemaGenerationError,
SecretBytes,
SecretStr,
SerializeAsAny,
SkipValidation,
Strict,
StrictBool,
StrictBytes,
StrictFloat,
StrictInt,
StrictStr,
StringConstraints,
Tag,
TypeAdapter,
ValidationError,
Expand All @@ -107,9 +114,6 @@
validate_call,
)
from pydantic.dataclasses import dataclass as pydantic_dataclass
from pydantic.errors import PydanticSchemaGenerationError
from pydantic.functional_validators import AfterValidator
from pydantic.types import AllowInfNan, GetPydanticSchema, ImportString, Strict, StringConstraints

try:
import email_validator
Expand Down Expand Up @@ -6129,3 +6133,38 @@ class MyModel(BaseModel):

round_trip_value = json.loads(MyModel(val=True).model_dump_json())['val']
assert round_trip_value is True, round_trip_value


def test_on_error_omit() -> None:
OmittableInt = OnErrorOmit[int]

class MyTypedDict(TypedDict):
a: NotRequired[OmittableInt]
b: NotRequired[OmittableInt]

class Model(BaseModel):
a_list: list[OmittableInt]
a_dict: dict[OmittableInt, OmittableInt]
a_typed_dict: MyTypedDict

actual = Model(
a_list=[1, 2, 'a', 3],
a_dict={1: 1, 2: 2, 'a': 'a', 'b': 0, 3: 'c', 4: 4},
a_typed_dict=MyTypedDict(a=1, b='xyz'), # type: ignore
)

expected = Model(a_list=[1, 2, 3], a_dict={1: 1, 2: 2, 4: 4}, a_typed_dict=MyTypedDict(a=1))

assert actual == expected

samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved

def test_on_error_omit_top_level() -> None:
ta = TypeAdapter(OnErrorOmit[int])

assert ta.validate_python(1) == 1
assert ta.validate_python('1') == 1

# we might want to just raise the OmitError or convert it to a ValidationError
# if it hits the top level, but this documents the current behavior at least
with pytest.raises(SchemaError, match='Uncaught Omit error'):
ta.validate_python('a')