Skip to content

Commit

Permalink
Add types.OnErrorOmit (pydantic#8222)
Browse files Browse the repository at this point in the history
  • Loading branch information
adriangb authored Nov 24, 2023
1 parent 9ab33eb commit afa3d4c
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 4 deletions.
2 changes: 2 additions & 0 deletions pydantic/__init__.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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:
@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.
this annotation omits the item from the iteration if there is any error validating it.
That is, instead of a [`ValidationError`][pydantic_core.ValidationError] being propagated up and the entire iterable being discarded
any invalid items are discarded and the valid ones are returned.
"""
47 changes: 43 additions & 4 deletions tests/test_types.py
Original file line number Diff line number Diff line change
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


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')

0 comments on commit afa3d4c

Please sign in to comment.