Skip to content

Commit b69e8dd

Browse files
committed
fix review idea
Signed-off-by: Manjusaka <me@manjusaka.me>
1 parent f2a51fc commit b69e8dd

File tree

2 files changed

+116
-98
lines changed

2 files changed

+116
-98
lines changed

Doc/library/annotationlib.rst

Lines changed: 29 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,8 @@ Functions
385385
using ``getattr()`` and ``dict.get()`` for safety.
386386
* Supports objects that provide their own :attr:`~object.__annotate__` method,
387387
such as :class:`functools.partial` and :class:`functools.partialmethod`.
388-
See :ref:`below <functools-objects-annotations>` for details on using
389-
:func:`!get_annotations` with :mod:`functools` objects.
388+
See the :mod:`functools` module documentation for details on how these
389+
objects support annotations.
390390

391391
*eval_str* controls whether or not values of type :class:`!str` are
392392
replaced with the result of calling :func:`eval` on those values:
@@ -442,93 +442,33 @@ Functions
442442

443443
.. versionadded:: 3.14
444444

445-
.. _functools-objects-annotations:
446-
447-
Using :func:`!get_annotations` with :mod:`functools` objects
448-
--------------------------------------------------------------
449-
450-
:func:`get_annotations` has special support for :class:`functools.partial`
451-
and :class:`functools.partialmethod` objects. When called on these objects,
452-
it returns only the annotations for parameters that have not been bound by
453-
the partial application, along with the return annotation if present.
454-
455-
For :class:`functools.partial` objects, positional arguments bind to parameters
456-
in order, and the annotations for those parameters are excluded from the result:
457-
458-
.. doctest::
459-
460-
>>> from functools import partial
461-
>>> def func(a: int, b: str, c: float) -> bool:
462-
... return True
463-
>>> partial_func = partial(func, 1) # Binds 'a'
464-
>>> get_annotations(partial_func)
465-
{'b': <class 'str'>, 'c': <class 'float'>, 'return': <class 'bool'>}
466-
467-
Keyword arguments in :class:`functools.partial` set default values but do not
468-
remove parameters from the signature, so their annotations are retained:
469-
470-
.. doctest::
471-
472-
>>> partial_func_kw = partial(func, b="hello") # Sets default for 'b'
473-
>>> get_annotations(partial_func_kw)
474-
{'a': <class 'int'>, 'b': <class 'str'>, 'c': <class 'float'>, 'return': <class 'bool'>}
475-
476-
For :class:`functools.partialmethod` objects accessed through a class (unbound),
477-
the first parameter (usually ``self`` or ``cls``) is preserved, and subsequent
478-
parameters are handled similarly to :class:`functools.partial`:
479-
480-
.. doctest::
481-
482-
>>> from functools import partialmethod
483-
>>> class MyClass:
484-
... def method(self, a: int, b: str) -> bool:
485-
... return True
486-
... partial_method = partialmethod(method, 1) # Binds 'a'
487-
>>> get_annotations(MyClass.partial_method)
488-
{'b': <class 'str'>, 'return': <class 'bool'>}
489-
490-
When a :class:`functools.partialmethod` is accessed through an instance (bound),
491-
it becomes a :class:`functools.partial` object and is handled accordingly:
492-
493-
.. doctest::
494-
495-
>>> obj = MyClass()
496-
>>> get_annotations(obj.partial_method) # Same as above, 'self' is also bound
497-
{'b': <class 'str'>, 'return': <class 'bool'>}
498-
499-
This behavior ensures that :func:`get_annotations` returns annotations that
500-
accurately reflect the signature of the partial or partialmethod object, as
501-
determined by :func:`inspect.signature`.
502-
503-
If :func:`!get_annotations` cannot reliably determine which parameters are bound
504-
(for example, if :func:`inspect.signature` raises an error), it will raise a
505-
:exc:`TypeError` rather than returning incorrect annotations. This ensures that
506-
you either get correct annotations or a clear error, never incorrect annotations:
507-
508-
.. doctest::
509-
510-
>>> from functools import partial
511-
>>> import inspect
512-
>>> def func(a: int, b: str) -> bool:
513-
... return True
514-
>>> partial_func = partial(func, 1)
515-
>>> # Simulate a case where signature inspection fails
516-
>>> original_sig = inspect.signature
517-
>>> def broken_signature(obj):
518-
... if isinstance(obj, partial):
519-
... raise ValueError("Cannot inspect signature")
520-
... return original_sig(obj)
521-
>>> inspect.signature = broken_signature
522-
>>> try:
523-
... get_annotations(partial_func)
524-
... except TypeError as e:
525-
... print(f"Got expected error: {e}")
526-
... finally:
527-
... inspect.signature = original_sig
528-
Got expected error: Cannot compute annotations for ...: unable to determine signature
529-
530-
This design prevents the common error of returning annotations that include
531-
parameters which have already been bound by the partial application.
445+
Supporting annotations in custom objects
446+
-------------------------------------------
447+
448+
Objects can support annotation introspection by implementing the :attr:`~object.__annotate__`
449+
protocol. When an object provides an :attr:`!__annotate__` attribute, :func:`get_annotations`
450+
will call it to retrieve the annotations for that object. The :attr:`!__annotate__` function
451+
should accept a single argument, a member of the :class:`Format` enum, and return a dictionary
452+
mapping annotation names to their values in the requested format.
453+
454+
This mechanism allows objects to dynamically compute their annotations based on their state.
455+
For example, :class:`functools.partial` and :class:`functools.partialmethod` objects use
456+
:attr:`!__annotate__` to provide annotations that reflect only the unbound parameters,
457+
excluding parameters that have been filled by the partial application. See the
458+
:mod:`functools` module documentation for details on how these specific objects handle
459+
annotations.
460+
461+
Other examples of objects that implement :attr:`!__annotate__` include:
462+
463+
* :class:`typing.TypedDict` classes created through the functional syntax
464+
* Generic classes and functions with type parameters
465+
466+
When implementing :attr:`!__annotate__` for custom objects, the function should handle
467+
all three primary formats (:attr:`~Format.VALUE`, :attr:`~Format.FORWARDREF`, and
468+
:attr:`~Format.STRING`) by either returning appropriate values or raising
469+
:exc:`NotImplementedError` to fall back to default behavior. Helper functions like
470+
:func:`annotations_to_string` and :func:`call_annotate_function` can assist with
471+
implementing format support.
532472

533473

534474
Recipes

Doc/library/functools.rst

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -829,15 +829,93 @@ callable, weak referenceable, and can have attributes. There are some important
829829
differences. For instance, the :attr:`~definition.__name__` and :attr:`~definition.__doc__` attributes
830830
are not created automatically.
831831

832-
However, :class:`partial` objects do support the :attr:`~object.__annotate__` protocol for
833-
annotation introspection. When accessed, :attr:`!__annotate__` returns only the annotations
834-
for parameters that have not been bound by the partial application, along with the return
835-
annotation. This behavior is consistent with :func:`inspect.signature` and allows tools like
836-
:func:`annotationlib.get_annotations` to work correctly with partial objects. See the
837-
:mod:`annotationlib` module documentation for more information on working with annotations
838-
on partial objects.
839-
840-
:class:`partialmethod` objects similarly support :attr:`~object.__annotate__` for unbound methods.
832+
Annotation support
833+
^^^^^^^^^^^^^^^^^^
834+
835+
:class:`partial` and :class:`partialmethod` objects support the :attr:`~object.__annotate__` protocol for
836+
annotation introspection. This allows tools like :func:`annotationlib.get_annotations` to retrieve
837+
annotations that accurately reflect the signature of the partial or partialmethod object.
838+
839+
For :class:`partial` objects, :func:`annotationlib.get_annotations` returns only the annotations
840+
for parameters that have not been bound by the partial application, along with the return annotation
841+
if present. Positional arguments bind to parameters in order, and the annotations for those parameters
842+
are excluded from the result:
843+
844+
.. doctest::
845+
846+
>>> from functools import partial
847+
>>> from annotationlib import get_annotations
848+
>>> def func(a: int, b: str, c: float) -> bool:
849+
... return True
850+
>>> partial_func = partial(func, 1) # Binds 'a'
851+
>>> get_annotations(partial_func)
852+
{'b': <class 'str'>, 'c': <class 'float'>, 'return': <class 'bool'>}
853+
854+
Keyword arguments in :class:`partial` set default values but do not remove parameters from the
855+
signature, so their annotations are retained:
856+
857+
.. doctest::
858+
859+
>>> partial_func_kw = partial(func, b="hello") # Sets default for 'b'
860+
>>> get_annotations(partial_func_kw)
861+
{'a': <class 'int'>, 'b': <class 'str'>, 'c': <class 'float'>, 'return': <class 'bool'>}
862+
863+
For :class:`partialmethod` objects accessed through a class (unbound), the first parameter
864+
(usually ``self`` or ``cls``) is preserved, and subsequent parameters are handled similarly
865+
to :class:`partial`:
866+
867+
.. doctest::
868+
869+
>>> from functools import partialmethod
870+
>>> class MyClass:
871+
... def method(self, a: int, b: str) -> bool:
872+
... return True
873+
... partial_method = partialmethod(method, 1) # Binds 'a'
874+
>>> get_annotations(MyClass.partial_method)
875+
{'b': <class 'str'>, 'return': <class 'bool'>}
876+
877+
When a :class:`partialmethod` is accessed through an instance (bound), it becomes a
878+
:class:`partial` object and is handled accordingly:
879+
880+
.. doctest::
881+
882+
>>> obj = MyClass()
883+
>>> get_annotations(obj.partial_method) # Same as above, 'self' is also bound
884+
{'b': <class 'str'>, 'return': <class 'bool'>}
885+
886+
This behavior ensures that :func:`annotationlib.get_annotations` returns annotations that
887+
accurately reflect the signature of the partial or partialmethod object, as determined by
888+
:func:`inspect.signature`.
889+
890+
If :func:`annotationlib.get_annotations` cannot reliably determine which parameters are bound
891+
(for example, if :func:`inspect.signature` raises an error), it will raise a :exc:`TypeError`
892+
rather than returning incorrect annotations. This ensures that you either get correct annotations
893+
or a clear error, never incorrect annotations:
894+
895+
.. doctest::
896+
897+
>>> from functools import partial
898+
>>> import inspect
899+
>>> def func(a: int, b: str) -> bool:
900+
... return True
901+
>>> partial_func = partial(func, 1)
902+
>>> # Simulate a case where signature inspection fails
903+
>>> original_sig = inspect.signature
904+
>>> def broken_signature(obj):
905+
... if isinstance(obj, partial):
906+
... raise ValueError("Cannot inspect signature")
907+
... return original_sig(obj)
908+
>>> inspect.signature = broken_signature
909+
>>> try:
910+
... get_annotations(partial_func)
911+
... except TypeError as e:
912+
... print(f"Got expected error: {e}")
913+
... finally:
914+
... inspect.signature = original_sig
915+
Got expected error: Cannot compute annotations for ...: unable to determine signature
916+
917+
This design prevents the common error of returning annotations that include parameters which
918+
have already been bound by the partial application.
841919

842920
.. versionadded:: next
843921
Added :attr:`~object.__annotate__` support to :class:`partial` and :class:`partialmethod`.

0 commit comments

Comments
 (0)