PEP 746: Use an attribute instead of a method (#3892)
This commit is contained in:
parent
5393dd6a15
commit
49b5935190
|
@ -38,41 +38,39 @@ it would not make sense to write, for example, ``Annotated[list[str], struct2.ct
|
||||||
Yet the type system provides no way to enforce this. The metadata are completely
|
Yet the type system provides no way to enforce this. The metadata are completely
|
||||||
ignored by type checkers.
|
ignored by type checkers.
|
||||||
|
|
||||||
This use case comes up in libraries like :pypi:`pydantic`, which use
|
This use case comes up in libraries like :pypi:`pydantic` and :pypi:`msgspec`, which use
|
||||||
``Annotated`` to attach validation and conversion information to fields.
|
``Annotated`` to attach validation and conversion information to fields or :pypi:`fastapi`,
|
||||||
|
which uses ``Annotated`` to mark parameters as extracted from headers, query strings or
|
||||||
|
dependency injection.
|
||||||
|
|
||||||
Specification
|
Specification
|
||||||
=============
|
=============
|
||||||
This PEP introduces a protocol that can be used by static and runtime type checkers to validate
|
This PEP introduces a protocol that can be used by static and runtime type checkers to validate
|
||||||
the consistency between ``Annotated`` metadata and a given type.
|
the consistency between ``Annotated`` metadata and a given type.
|
||||||
Objects that implement this protocol have a method named ``__supports_type__``
|
Objects that implement this protocol have an attribute called ``__supports_type__``
|
||||||
that takes a single positional argument and returns ``bool``::
|
that specifies whether the metadata is valid for a given type::
|
||||||
|
|
||||||
class Int64:
|
class Int64:
|
||||||
def __supports_type__(self, obj: int) -> bool:
|
__supports_type__: int
|
||||||
return isinstance(obj, int)
|
|
||||||
|
|
||||||
The protocol being introduced would be defined as follows if it were to be defined in code form::
|
The attribute may also be marked as a ``ClassVar`` to avoid interaction with dataclasses::
|
||||||
|
|
||||||
from typing import Protocol
|
from dataclasses import dataclass
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
class SupportsType[T](Protocol):
|
@dataclass
|
||||||
def __supports_type__(self, obj: T, /) -> bool:
|
class Gt:
|
||||||
...
|
value: int
|
||||||
|
__supports_type__: ClassVar[int]
|
||||||
|
|
||||||
When a static type checker encounters a type expression of the form ``Annotated[T, M1, M2, ...]``,
|
When a static type checker encounters a type expression of the form ``Annotated[T, M1, M2, ...]``,
|
||||||
it should enforce that for each metadata element in ``M1, M2, ...``, one of the following is true:
|
it should enforce that for each metadata element in ``M1, M2, ...``, one of the following is true:
|
||||||
|
|
||||||
* The metadata element evaluates to an object that does not have a ``__supports_type__`` attribute; or
|
* The metadata element evaluates to an object that does not have a ``__supports_type__`` attribute; or
|
||||||
* The metadata element evaluates to an object ``M`` that implements the ``SupportsType`` protocol;
|
* The metadata element evaluates to an object ``M`` that has a ``__supports_type__`` attribute;
|
||||||
and, with ``T`` instantiated to a value ``v``, a call to ``M.__supports_type__(v)`` type checks without errors;
|
and ``T`` is assignable to the type of ``M.__supports_type__``.
|
||||||
and that call does not evaluate to ``Literal[False]``.
|
|
||||||
|
|
||||||
The body of the ``__supports_type__`` method is not used to check the validity of the metadata
|
To support generic ``Gt`` metadata, one might write::
|
||||||
and static type checkers can ignore it. However, tools that use the annotation at
|
|
||||||
runtime may call the method to check that a particular value is valid.
|
|
||||||
|
|
||||||
For example, to support a generic ``Gt`` metadata, one might write::
|
|
||||||
|
|
||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
|
|
||||||
|
@ -81,26 +79,15 @@ For example, to support a generic ``Gt`` metadata, one might write::
|
||||||
...
|
...
|
||||||
|
|
||||||
class Gt[T]:
|
class Gt[T]:
|
||||||
|
__supports_type__: ClassVar[SupportsGt[T]]
|
||||||
|
|
||||||
def __init__(self, value: T) -> None:
|
def __init__(self, value: T) -> None:
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
def __supports_type__(self, obj: SupportsGt[T], /) -> bool:
|
|
||||||
return obj > self.value
|
|
||||||
|
|
||||||
x1: Annotated[int, Gt(0)] = 1 # OK
|
x1: Annotated[int, Gt(0)] = 1 # OK
|
||||||
x2: Annotated[str, Gt(0)] = 0 # type checker error: str is not assignable to SupportsGt[int]
|
x2: Annotated[str, Gt(0)] = 0 # type checker error: str is not assignable to SupportsGt[int]
|
||||||
x3: Annotated[int, Gt(1)] = 0 # OK for static type checkers; runtime type checkers may flag this
|
x3: Annotated[int, Gt(1)] = 0 # OK for static type checkers; runtime type checkers may flag this
|
||||||
|
|
||||||
Implementations may be generic and may use overloads that return ``Literal[True]`` or ``Literal[False]``
|
|
||||||
to indicate if the metadata is valid for the given type.
|
|
||||||
|
|
||||||
Implementations may raise a NotImplementedError if they cannot determine if the metadata is valid for the given type.
|
|
||||||
Tools calling ``__supports_type__`` at runtime should catch this exception and treat it as if ``__supports_type__``
|
|
||||||
was not present; they should not take this as an indication that the metadata is invalid for the type.
|
|
||||||
|
|
||||||
Tools that use the metadata at runtime may choose to ignore the implementation of ``__supports_type__``; this PEP does not
|
|
||||||
specify how the method should be used at runtime, only that it may be available for use.
|
|
||||||
|
|
||||||
Backwards Compatibility
|
Backwards Compatibility
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
@ -150,10 +137,19 @@ does not generally use marker base classes. In addition, it provides less flexib
|
||||||
the current proposal: it would not allow overloads, and it would require metadata objects
|
the current proposal: it would not allow overloads, and it would require metadata objects
|
||||||
to add a new base class, which may make their runtime implementation more complex.
|
to add a new base class, which may make their runtime implementation more complex.
|
||||||
|
|
||||||
|
Using a method instead of an attribute for ``__supports_type__``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We considered using a method instead of an attribute for the protocol, so that this method can be used
|
||||||
|
at runtime to check the validity of the metadata and to support overloads or returning boolean literals.
|
||||||
|
However, using a method adds boilerplate to the implementation and the value of the runtime use cases or
|
||||||
|
more complex scenarios involving overloads and returning boolean literals was not clear.
|
||||||
|
|
||||||
Acknowledgments
|
Acknowledgments
|
||||||
===============
|
===============
|
||||||
|
|
||||||
We thank Eric Traut for suggesting the idea of using a protocol.
|
We thank Eric Traut for suggesting the idea of using a protocol and implementing provisional support in Pyright.
|
||||||
|
Thank you to Jelle Zijlstra for sponsoring this PEP.
|
||||||
|
|
||||||
Copyright
|
Copyright
|
||||||
=========
|
=========
|
||||||
|
|
Loading…
Reference in New Issue