PEP 746: Use an attribute instead of a method (#3892)
This commit is contained in:
parent
5393dd6a15
commit
49b5935190
|
@ -38,69 +38,56 @@ 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
|
||||
ignored by type checkers.
|
||||
|
||||
This use case comes up in libraries like :pypi:`pydantic`, which use
|
||||
``Annotated`` to attach validation and conversion information to fields.
|
||||
This use case comes up in libraries like :pypi:`pydantic` and :pypi:`msgspec`, which use
|
||||
``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
|
||||
=============
|
||||
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.
|
||||
Objects that implement this protocol have a method named ``__supports_type__``
|
||||
that takes a single positional argument and returns ``bool``::
|
||||
Objects that implement this protocol have an attribute called ``__supports_type__``
|
||||
that specifies whether the metadata is valid for a given type::
|
||||
|
||||
class Int64:
|
||||
def __supports_type__(self, obj: int) -> bool:
|
||||
return isinstance(obj, int)
|
||||
__supports_type__: 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 dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
class SupportsType[T](Protocol):
|
||||
def __supports_type__(self, obj: T, /) -> bool:
|
||||
...
|
||||
@dataclass
|
||||
class Gt:
|
||||
value: int
|
||||
__supports_type__: ClassVar[int]
|
||||
|
||||
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:
|
||||
|
||||
* 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;
|
||||
and, with ``T`` instantiated to a value ``v``, a call to ``M.__supports_type__(v)`` type checks without errors;
|
||||
and that call does not evaluate to ``Literal[False]``.
|
||||
* The metadata element evaluates to an object ``M`` that has a ``__supports_type__`` attribute;
|
||||
and ``T`` is assignable to the type of ``M.__supports_type__``.
|
||||
|
||||
The body of the ``__supports_type__`` method is not used to check the validity of the metadata
|
||||
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::
|
||||
To support generic ``Gt`` metadata, one might write::
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
class SupportsGt[T](Protocol):
|
||||
def __gt__(self, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class Gt[T]:
|
||||
__supports_type__: ClassVar[SupportsGt[T]]
|
||||
|
||||
def __init__(self, value: T) -> None:
|
||||
self.value = value
|
||||
|
||||
def __supports_type__(self, obj: SupportsGt[T], /) -> bool:
|
||||
return obj > self.value
|
||||
|
||||
x1: Annotated[int, Gt(0)] = 1 # OK
|
||||
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
|
||||
|
||||
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
|
||||
=======================
|
||||
|
||||
|
@ -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
|
||||
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
|
||||
===============
|
||||
|
||||
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
|
||||
=========
|
||||
|
|
Loading…
Reference in New Issue