PEP 746: Use an attribute instead of a method (#3892)

This commit is contained in:
Adrian Garcia Badaracco 2024-08-11 11:49:33 -05:00 committed by GitHub
parent 5393dd6a15
commit 49b5935190
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 31 additions and 35 deletions

View File

@ -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
=========