From 49b59351903fba968b2672a7eb390f304a8ce4d6 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sun, 11 Aug 2024 11:49:33 -0500 Subject: [PATCH] PEP 746: Use an attribute instead of a method (#3892) --- peps/pep-0746.rst | 66 ++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/peps/pep-0746.rst b/peps/pep-0746.rst index c2b203ccd..f2e0f1f82 100644 --- a/peps/pep-0746.rst +++ b/peps/pep-0746.rst @@ -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 =========