Skip to content

Commit

Permalink
Add InstanceOf type/annotation (#5778)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmontagu committed May 25, 2023
1 parent 91caede commit e78dd92
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 1 deletion.
22 changes: 22 additions & 0 deletions pydantic/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from ._internal import _fields, _internal_dataclass, _known_annotated_metadata, _validators
from ._internal._core_metadata import build_metadata_dict
from ._internal._generics import get_origin
from ._internal._internal_dataclass import slots_dataclass
from ._migration import getattr_migration
from .annotated import GetCoreSchemaHandler
Expand Down Expand Up @@ -994,6 +995,27 @@ def encode_str(self, value: str) -> str:
Base64Bytes = Annotated[bytes, EncodedBytes(encoder=Base64Encoder)]
Base64Str = Annotated[str, EncodedStr(encoder=Base64Encoder)]


if TYPE_CHECKING:
# If we add configurable attributes to IsInstance, we'd probably need to stop hiding it from type checkers like this
InstanceOf = Annotated[AnyType, ...] # `IsInstance[Sequence]` will be recognized by type checkers as `Sequence`

else:

@_internal_dataclass.slots_dataclass
class InstanceOf:
@classmethod
def __class_getitem__(cls, item: AnyType) -> AnyType:
return Annotated[item, cls()]

@classmethod
def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
# use the generic _origin_ as the second argument to isinstance when appropriate
python_schema = core_schema.is_instance_schema(get_origin(source) or source)
json_schema = handler(source)
return core_schema.json_or_python_schema(python_schema=python_schema, json_schema=json_schema)


__getattr__ = getattr_migration(__name__)


Expand Down
32 changes: 31 additions & 1 deletion tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
from pydantic.annotated import GetCoreSchemaHandler
from pydantic.errors import PydanticSchemaGenerationError
from pydantic.json_schema import GetJsonSchemaHandler, JsonSchemaValue
from pydantic.types import AllowInfNan, ImportString, SecretField, SkipValidation, Strict, TransformSchema
from pydantic.types import AllowInfNan, ImportString, InstanceOf, SecretField, SkipValidation, Strict, TransformSchema

try:
import email_validator
Expand Down Expand Up @@ -5190,3 +5190,33 @@ class Model(BaseModel):
foo: Literal['foo']

assert Model(foo='foo').foo == 'foo'


def test_is_instance_annotation():
class Model(BaseModel):
x: InstanceOf[Sequence[int]] # Note: the generic parameter gets ignored by runtime validation

class MyList(list):
pass

assert Model(x='abc').x == 'abc'
assert type(Model(x=MyList([1, 2, 3])).x) is MyList

with pytest.raises(ValidationError) as exc_info:
Model(x=1)
assert exc_info.value.errors(include_url=False) == [
{
'ctx': {'class': 'Sequence'},
'input': 1,
'loc': ('x',),
'msg': 'Input should be an instance of Sequence',
'type': 'is_instance_of',
}
]

assert Model.model_validate_json('{"x": [1,2,3]}').x == [1, 2, 3]
with pytest.raises(ValidationError) as exc_info:
Model.model_validate_json('{"x": "abc"}')
assert exc_info.value.errors(include_url=False) == [
{'input': 'abc', 'loc': ('x',), 'msg': 'Input should be a valid array', 'type': 'list_type'}
]

0 comments on commit e78dd92

Please sign in to comment.