|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import abc |
4 | | -from collections.abc import Callable |
| 4 | +from abc import ABCMeta |
5 | 5 | from collections.abc import Iterable |
6 | | -from collections.abc import Iterator |
| 6 | +from typing import TYPE_CHECKING |
7 | 7 | from typing import Any |
8 | 8 | from typing import ClassVar |
9 | 9 | from typing import Generic |
10 | 10 | from typing import Protocol |
11 | 11 | from typing import TypeVar |
12 | 12 | from typing import runtime_checkable |
13 | 13 |
|
| 14 | +if TYPE_CHECKING: |
| 15 | + try: |
| 16 | + from pydantic import GetCoreSchemaHandler |
| 17 | + from pydantic_core import CoreSchema |
| 18 | + from pydantic_core.core_schema import ValidatorFunctionWrapHandler |
| 19 | + except ImportError: |
| 20 | + pass |
| 21 | + |
14 | 22 | from typing_extensions import Self |
15 | 23 |
|
16 | 24 | from . import _hypothesis |
| 25 | +from ._utils.compat import require_pydantic |
17 | 26 | from ._utils.misc import BoundType |
18 | 27 | from ._utils.misc import UnresolvedClassAttribute |
19 | 28 | from ._utils.misc import fully_qualified_name |
@@ -43,6 +52,16 @@ def parse(cls, instance: object) -> Self: ... |
43 | 52 | V = TypeVar("V", bound=SupportsParse) |
44 | 53 |
|
45 | 54 |
|
| 55 | +def _is_protocol(tp: type) -> bool: |
| 56 | + """Check if type is a Protocol.""" |
| 57 | + return isinstance(tp, type) and getattr(tp, "_is_protocol", False) |
| 58 | + |
| 59 | + |
| 60 | +def _is_abc(tp: type) -> bool: |
| 61 | + """Check if type is an ABC.""" |
| 62 | + return isinstance(tp, ABCMeta) |
| 63 | + |
| 64 | + |
46 | 65 | class PhantomMeta(abc.ABCMeta): |
47 | 66 | """ |
48 | 67 | Metaclass that defers __instancecheck__ to derived classes and prevents actual |
@@ -84,9 +103,51 @@ def parse(cls: type[Derived], instance: object) -> Derived: |
84 | 103 | def __instancecheck__(cls, instance: object) -> bool: ... |
85 | 104 |
|
86 | 105 | @classmethod |
87 | | - def __get_validators__(cls: type[Derived]) -> Iterator[Callable[[object], Derived]]: |
88 | | - """Hook that makes phantom types compatible with pydantic.""" |
89 | | - yield cls.parse |
| 106 | + def _validate( |
| 107 | + cls: type[Derived], |
| 108 | + value: Any, |
| 109 | + handler: ValidatorFunctionWrapHandler, |
| 110 | + ) -> Derived: |
| 111 | + """Pydantic V2 wrap validator.""" |
| 112 | + require_pydantic() |
| 113 | + |
| 114 | + from pydantic_core import PydanticCustomError |
| 115 | + |
| 116 | + try: |
| 117 | + validated = handler(value) |
| 118 | + return cls.parse(validated) |
| 119 | + except Exception as exc: |
| 120 | + raise PydanticCustomError( |
| 121 | + "value_error", |
| 122 | + f"value is not a valid {cls.__name__}", |
| 123 | + ) from exc |
| 124 | + |
| 125 | + @classmethod |
| 126 | + def __get_pydantic_core_schema__( |
| 127 | + cls: type[Derived], source: type[Any], handler: GetCoreSchemaHandler |
| 128 | + ) -> CoreSchema: |
| 129 | + """Pydantic V2 hook for core schema generation.""" |
| 130 | + require_pydantic() |
| 131 | + |
| 132 | + from pydantic.errors import PydanticSchemaGenerationError |
| 133 | + from pydantic_core.core_schema import any_schema |
| 134 | + from pydantic_core.core_schema import is_instance_schema |
| 135 | + from pydantic_core.core_schema import no_info_wrap_validator_function |
| 136 | + |
| 137 | + bound = getattr(cls, "__bound__", object) |
| 138 | + |
| 139 | + bound_schema: CoreSchema |
| 140 | + if _is_protocol(bound) or (_is_abc(bound) and bound is not cls): |
| 141 | + # For protocols and ABCs, use is_instance_schema |
| 142 | + bound_schema = is_instance_schema(bound) |
| 143 | + else: |
| 144 | + # For concrete types, let Pydantic generate the schema |
| 145 | + try: |
| 146 | + bound_schema = handler.generate_schema(bound) |
| 147 | + except PydanticSchemaGenerationError: |
| 148 | + bound_schema = any_schema() |
| 149 | + |
| 150 | + return no_info_wrap_validator_function(cls._validate, bound_schema) |
90 | 151 |
|
91 | 152 |
|
92 | 153 | class AbstractInstanceCheck(TypeError): ... |
|
0 commit comments