mirror of
https://github.com/python/cpython.git
synced 2025-12-23 09:19:18 +00:00
Merge 09b0bbb5a6 into f9704f1d84
This commit is contained in:
commit
8e4d109281
6 changed files with 586 additions and 4 deletions
|
|
@ -383,6 +383,10 @@ Functions
|
|||
doesn't have its own annotations dict, returns an empty dict.
|
||||
* All accesses to object members and dict values are done
|
||||
using ``getattr()`` and ``dict.get()`` for safety.
|
||||
* Supports objects that provide their own :attr:`~object.__annotate__` descriptor,
|
||||
such as :class:`functools.partial` and :class:`functools.partialmethod`.
|
||||
See the :mod:`functools` module documentation for details on how these
|
||||
objects support annotations.
|
||||
|
||||
*eval_str* controls whether or not values of type :class:`!str` are
|
||||
replaced with the result of calling :func:`eval` on those values:
|
||||
|
|
@ -404,10 +408,12 @@ Functions
|
|||
``sys.modules[obj.__module__].__dict__`` and *locals* defaults
|
||||
to the *obj* class namespace.
|
||||
* If *obj* is a callable, *globals* defaults to
|
||||
:attr:`obj.__globals__ <function.__globals__>`,
|
||||
although if *obj* is a wrapped function (using
|
||||
:func:`functools.update_wrapper`) or a :class:`functools.partial` object,
|
||||
it is unwrapped until a non-wrapped function is found.
|
||||
:attr:`obj.__globals__ <function.__globals__>`.
|
||||
If *obj* has a ``__wrapped__`` attribute (such as functions
|
||||
decorated with :func:`functools.update_wrapper`), or if it is a
|
||||
:class:`functools.partial` object, it is unwrapped by following the
|
||||
``__wrapped__`` attribute or :attr:`~functools.partial.func` attribute
|
||||
repeatedly to find the underlying wrapped function's globals.
|
||||
|
||||
Calling :func:`!get_annotations` is best practice for accessing the
|
||||
annotations dict of any object. See :ref:`annotations-howto` for
|
||||
|
|
@ -436,6 +442,36 @@ Functions
|
|||
|
||||
.. versionadded:: 3.14
|
||||
|
||||
.. _support-annotations-custom-objects:
|
||||
|
||||
Supporting annotations in custom objects
|
||||
-------------------------------------------
|
||||
|
||||
Objects can support annotation introspection by implementing the :attr:`~object.__annotate__`
|
||||
protocol. When an object's class provides an :attr:`!__annotate__` descriptor, :func:`get_annotations`
|
||||
will call it to retrieve the annotations for that object. The :attr:`!__annotate__` function
|
||||
should accept a single argument, a member of the :class:`Format` enum, and return a dictionary
|
||||
mapping annotation names to their values in the requested format.
|
||||
|
||||
This mechanism allows objects to dynamically compute their annotations based on their state.
|
||||
For example, :class:`functools.partial` and :class:`functools.partialmethod` objects use
|
||||
:attr:`!__annotate__` to provide annotations that reflect only the unbound parameters,
|
||||
excluding parameters that have been filled by the partial application. See the
|
||||
:mod:`functools` module documentation for details on how these specific objects handle
|
||||
annotations.
|
||||
|
||||
Other examples of objects that implement :attr:`!__annotate__` include:
|
||||
|
||||
* :class:`typing.TypedDict` classes created through the functional syntax
|
||||
* Generic classes and functions with type parameters
|
||||
|
||||
When implementing :attr:`!__annotate__` for custom objects, the function should handle
|
||||
all three primary formats (:attr:`~Format.VALUE`, :attr:`~Format.FORWARDREF`, and
|
||||
:attr:`~Format.STRING`) by either returning appropriate values or raising
|
||||
:exc:`NotImplementedError` to fall back to default behavior. Helper functions like
|
||||
:func:`annotations_to_string` and :func:`call_annotate_function` can assist with
|
||||
implementing format support.
|
||||
|
||||
|
||||
Recipes
|
||||
-------
|
||||
|
|
|
|||
|
|
@ -832,3 +832,94 @@ have three read-only attributes:
|
|||
callable, weak referenceable, and can have attributes. There are some important
|
||||
differences. For instance, the :attr:`~definition.__name__` and :attr:`~definition.__doc__` attributes
|
||||
are not created automatically.
|
||||
|
||||
Annotation support
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:class:`partial` and :class:`partialmethod` objects support the :attr:`~object.__annotate__` protocol for
|
||||
annotation introspection. This allows tools like :func:`annotationlib.get_annotations` to retrieve
|
||||
annotations that accurately reflect the signature of the partial or partialmethod object.
|
||||
|
||||
For :class:`partial` objects, :func:`annotationlib.get_annotations` returns only the annotations
|
||||
for parameters that have not been bound by the partial application, along with the return annotation
|
||||
if present. Positional arguments bind to parameters in order, and the annotations for those parameters
|
||||
are excluded from the result:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from functools import partial
|
||||
>>> from annotationlib import get_annotations
|
||||
>>> def func(a: int, b: str, c: float) -> bool:
|
||||
... return True
|
||||
>>> partial_func = partial(func, 1) # Binds 'a'
|
||||
>>> get_annotations(partial_func)
|
||||
{'b': <class 'str'>, 'c': <class 'float'>, 'return': <class 'bool'>}
|
||||
|
||||
Keyword arguments in :class:`partial` set default values but do not remove parameters from the
|
||||
signature, so their annotations are retained:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> partial_func_kw = partial(func, b="hello") # Sets default for 'b'
|
||||
>>> get_annotations(partial_func_kw)
|
||||
{'a': <class 'int'>, 'b': <class 'str'>, 'c': <class 'float'>, 'return': <class 'bool'>}
|
||||
|
||||
For :class:`partialmethod` objects accessed through a class (unbound), the first parameter
|
||||
(usually ``self`` or ``cls``) is preserved, and subsequent parameters are handled similarly
|
||||
to :class:`partial`:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from functools import partialmethod
|
||||
>>> class MyClass:
|
||||
... def method(self, a: int, b: str) -> bool:
|
||||
... return True
|
||||
... partial_method = partialmethod(method, 1) # Binds 'a'
|
||||
>>> get_annotations(MyClass.partial_method)
|
||||
{'b': <class 'str'>, 'return': <class 'bool'>}
|
||||
|
||||
When a :class:`partialmethod` is accessed through an instance (bound), it becomes a
|
||||
:class:`partial` object and is handled accordingly:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> obj = MyClass()
|
||||
>>> get_annotations(obj.partial_method) # Same as above, 'self' is also bound
|
||||
{'b': <class 'str'>, 'return': <class 'bool'>}
|
||||
|
||||
This behavior ensures that :func:`annotationlib.get_annotations` returns annotations that
|
||||
accurately reflect the signature of the partial or partialmethod object, as determined by
|
||||
:func:`inspect.signature`.
|
||||
|
||||
If :func:`annotationlib.get_annotations` cannot reliably determine which parameters are bound
|
||||
(for example, if :func:`inspect.signature` raises an error), it will raise a :exc:`TypeError`
|
||||
rather than returning incorrect annotations. This ensures that you either get correct annotations
|
||||
or a clear error, never incorrect annotations:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from functools import partial
|
||||
>>> import inspect
|
||||
>>> def func(a: int, b: str) -> bool:
|
||||
... return True
|
||||
>>> partial_func = partial(func, 1)
|
||||
>>> # Simulate a case where signature inspection fails
|
||||
>>> original_sig = inspect.signature
|
||||
>>> def broken_signature(obj):
|
||||
... if isinstance(obj, partial):
|
||||
... raise ValueError("Cannot inspect signature")
|
||||
... return original_sig(obj)
|
||||
>>> inspect.signature = broken_signature
|
||||
>>> try:
|
||||
... get_annotations(partial_func)
|
||||
... except TypeError as e:
|
||||
... print(f"Got expected error: {e}")
|
||||
... finally:
|
||||
... inspect.signature = original_sig
|
||||
Got expected error: Cannot compute annotations for ...: unable to determine signature
|
||||
|
||||
This design prevents the common error of returning annotations that include parameters which
|
||||
have already been bound by the partial application.
|
||||
|
||||
.. versionadded:: next
|
||||
Added :attr:`~object.__annotate__` support to :class:`partial` and :class:`partialmethod`.
|
||||
|
|
|
|||
150
Lib/functools.py
150
Lib/functools.py
|
|
@ -365,6 +365,152 @@ def _partial_repr(self):
|
|||
args.extend(f"{k}={v!r}" for k, v in self.keywords.items())
|
||||
return f"{module}.{qualname}({', '.join(args)})"
|
||||
|
||||
|
||||
################################################################################
|
||||
### _partial_annotate() - compute annotations for partial objects
|
||||
################################################################################
|
||||
|
||||
def _partial_annotate(partial_obj, format):
|
||||
"""Helper function to compute annotations for a partial object.
|
||||
|
||||
This is called by the __annotate__ descriptor defined in C.
|
||||
Returns annotations for the wrapped function, but only for parameters
|
||||
that haven't been bound by the partial application.
|
||||
"""
|
||||
import inspect
|
||||
from annotationlib import get_annotations
|
||||
|
||||
# Get annotations from the wrapped function
|
||||
func_annotations = get_annotations(partial_obj.func, format=format)
|
||||
|
||||
if not func_annotations:
|
||||
return {}
|
||||
|
||||
# Get the signature to determine which parameters are bound
|
||||
try:
|
||||
sig = inspect.signature(partial_obj, annotation_format=format)
|
||||
except (ValueError, TypeError) as e:
|
||||
# If we can't get signature, we can't reliably determine which
|
||||
# parameters are bound. Raise an error rather than returning
|
||||
# incorrect annotations.
|
||||
raise TypeError(
|
||||
f"Cannot compute annotations for {partial_obj!r}: "
|
||||
f"unable to determine signature"
|
||||
) from e
|
||||
|
||||
# Build new annotations dict with only unbound parameters
|
||||
# (parameters first, then return)
|
||||
new_annotations = {}
|
||||
|
||||
# Only include annotations for parameters that still exist in partial's signature
|
||||
for param_name in sig.parameters:
|
||||
if param_name in func_annotations:
|
||||
new_annotations[param_name] = func_annotations[param_name]
|
||||
|
||||
# Add return annotation at the end
|
||||
if 'return' in func_annotations:
|
||||
new_annotations['return'] = func_annotations['return']
|
||||
|
||||
return new_annotations
|
||||
|
||||
|
||||
################################################################################
|
||||
### _partialmethod_annotate() - compute annotations for partialmethod objects
|
||||
################################################################################
|
||||
|
||||
def _partialmethod_annotate(partialmethod_obj, format):
|
||||
"""Helper function to compute annotations for a partialmethod object.
|
||||
|
||||
This is called when accessing annotations on an unbound partialmethod
|
||||
(via the __partialmethod__ attribute).
|
||||
Returns annotations for the wrapped function, but only for parameters
|
||||
that haven't been bound by the partial application. The first parameter
|
||||
(usually 'self' or 'cls') is kept since partialmethod is unbound.
|
||||
"""
|
||||
import inspect
|
||||
from annotationlib import get_annotations
|
||||
|
||||
# Get annotations from the wrapped function
|
||||
func_annotations = get_annotations(partialmethod_obj.func, format=format)
|
||||
|
||||
if not func_annotations:
|
||||
return {}
|
||||
|
||||
# For partialmethod, we need to simulate the signature calculation
|
||||
# The first parameter (self/cls) should remain, but bound args should be removed
|
||||
try:
|
||||
# Get the function signature
|
||||
func_sig = inspect.signature(partialmethod_obj.func, annotation_format=format)
|
||||
func_params = list(func_sig.parameters.keys())
|
||||
|
||||
if not func_params:
|
||||
return func_annotations
|
||||
|
||||
# Calculate which parameters are bound by the partialmethod
|
||||
partial_args = partialmethod_obj.args or ()
|
||||
partial_keywords = partialmethod_obj.keywords or {}
|
||||
|
||||
# Build new annotations dict in proper order
|
||||
# (parameters first, then return)
|
||||
new_annotations = {}
|
||||
|
||||
# The first parameter (self/cls) is always kept for unbound partialmethod
|
||||
first_param = func_params[0]
|
||||
if first_param in func_annotations:
|
||||
new_annotations[first_param] = func_annotations[first_param]
|
||||
|
||||
# For partialmethod, positional args bind to parameters AFTER the first one
|
||||
# So if func is (self, a, b, c) and partialmethod.args=(1,)
|
||||
# Then 'self' stays, 'a' is bound, 'b' and 'c' remain
|
||||
|
||||
# We need to account for Placeholders which create "holes"
|
||||
# For example: partialmethod(func, 1, Placeholder, 3) binds 'a' and 'c' but not 'b'
|
||||
|
||||
remaining_params = func_params[1:]
|
||||
|
||||
# Track which positions are filled by Placeholder
|
||||
placeholder_positions = set()
|
||||
for i, arg in enumerate(partial_args):
|
||||
if arg is Placeholder:
|
||||
placeholder_positions.add(i)
|
||||
|
||||
# Number of non-Placeholder positional args
|
||||
# This doesn't directly tell us which params are bound due to Placeholders
|
||||
|
||||
for i, param_name in enumerate(remaining_params):
|
||||
# Check if this position has a Placeholder
|
||||
if i in placeholder_positions:
|
||||
# This parameter is deferred by Placeholder, keep it
|
||||
if param_name in func_annotations:
|
||||
new_annotations[param_name] = func_annotations[param_name]
|
||||
continue
|
||||
|
||||
# Check if this position is beyond the partial_args
|
||||
if i >= len(partial_args):
|
||||
# This parameter is not bound at all, keep it
|
||||
if param_name in func_annotations:
|
||||
new_annotations[param_name] = func_annotations[param_name]
|
||||
continue
|
||||
|
||||
# Otherwise, this position is bound (not a Placeholder and within bounds)
|
||||
# Skip it
|
||||
|
||||
# Add return annotation at the end
|
||||
if 'return' in func_annotations:
|
||||
new_annotations['return'] = func_annotations['return']
|
||||
|
||||
return new_annotations
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
# If we can't process the signature, we can't reliably determine
|
||||
# which parameters are bound. Raise an error rather than returning
|
||||
# incorrect annotations (which would include bound parameters).
|
||||
raise TypeError(
|
||||
f"Cannot compute annotations for {partialmethod_obj!r}: "
|
||||
f"unable to determine which parameters are bound"
|
||||
) from e
|
||||
|
||||
|
||||
# Purely functional, no descriptor behaviour
|
||||
class partial:
|
||||
"""New function with partial application of the given arguments
|
||||
|
|
@ -467,6 +613,8 @@ class partialmethod:
|
|||
return self.func(cls_or_self, *pto_args, *args, **keywords)
|
||||
_method.__isabstractmethod__ = self.__isabstractmethod__
|
||||
_method.__partialmethod__ = self
|
||||
# Set __annotate__ to delegate to the partialmethod's __annotate__
|
||||
_method.__annotate__ = self.__annotate__
|
||||
return _method
|
||||
|
||||
def __get__(self, obj, cls=None):
|
||||
|
|
@ -492,6 +640,8 @@ class partialmethod:
|
|||
def __isabstractmethod__(self):
|
||||
return getattr(self.func, "__isabstractmethod__", False)
|
||||
|
||||
__annotate__ = _partialmethod_annotate
|
||||
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2200,6 +2200,282 @@ class TestForwardRefClass(unittest.TestCase):
|
|||
pass
|
||||
|
||||
|
||||
class TestFunctoolsPartialMethod(unittest.TestCase):
|
||||
"""Tests for get_annotations() with functools.partialmethod objects."""
|
||||
|
||||
def test_partialmethod_unbound(self):
|
||||
"""Test unbound partialmethod."""
|
||||
class MyClass:
|
||||
def method(self, a: int, b: str, c: float) -> bool:
|
||||
return True
|
||||
|
||||
partial_method = functools.partialmethod(method, 1)
|
||||
|
||||
result = get_annotations(MyClass.partial_method)
|
||||
|
||||
# 'a' is bound, but 'self' should remain (unbound method)
|
||||
expected = {'self': type(None).__class__, 'b': str, 'c': float, 'return': bool}
|
||||
# Note: 'self' might not have an annotation in the original function
|
||||
# So we check what parameters remain
|
||||
self.assertIn('b', result)
|
||||
self.assertIn('c', result)
|
||||
self.assertIn('return', result)
|
||||
self.assertNotIn('a', result)
|
||||
|
||||
def test_partialmethod_bound(self):
|
||||
"""Test bound partialmethod (which becomes a partial object)."""
|
||||
class MyClass:
|
||||
def method(self, a: int, b: str, c: float) -> bool:
|
||||
return True
|
||||
|
||||
partial_method = functools.partialmethod(method, 1)
|
||||
|
||||
obj = MyClass()
|
||||
result = get_annotations(obj.partial_method)
|
||||
|
||||
# 'self' and 'a' are bound, only b, c remain
|
||||
expected = {'b': str, 'c': float, 'return': bool}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partialmethod_with_keyword(self):
|
||||
"""Test partialmethod with keyword argument."""
|
||||
class MyClass:
|
||||
def method(self, a: int, b: str, c: float) -> bool:
|
||||
return True
|
||||
|
||||
partial_method = functools.partialmethod(method, b="hello")
|
||||
|
||||
result = get_annotations(MyClass.partial_method)
|
||||
|
||||
# Keyword args don't remove params, but 'a' might be affected
|
||||
self.assertIn('b', result)
|
||||
self.assertIn('c', result)
|
||||
self.assertIn('return', result)
|
||||
|
||||
def test_partialmethod_classmethod(self):
|
||||
"""Test partialmethod with classmethod."""
|
||||
class MyClass:
|
||||
@classmethod
|
||||
def method(cls, a: int, b: str) -> bool:
|
||||
return True
|
||||
|
||||
partial_method = functools.partialmethod(method, 1)
|
||||
|
||||
result = get_annotations(MyClass.partial_method)
|
||||
|
||||
# 'a' is bound, 'cls' and 'b' should remain
|
||||
self.assertIn('b', result)
|
||||
self.assertIn('return', result)
|
||||
self.assertNotIn('a', result)
|
||||
|
||||
def test_partialmethod_no_annotations(self):
|
||||
"""Test partialmethod without annotations."""
|
||||
class MyClass:
|
||||
def method(self, a, b, c):
|
||||
return True
|
||||
|
||||
partial_method = functools.partialmethod(method, 1)
|
||||
|
||||
result = get_annotations(MyClass.partial_method)
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_partialmethod_with_placeholder(self):
|
||||
"""Test partialmethod with Placeholder."""
|
||||
class MyClass:
|
||||
def method(self, a: int, b: str, c: float) -> bool:
|
||||
return True
|
||||
|
||||
# Bind 'a', defer 'b', bind 'c'
|
||||
partial_method = functools.partialmethod(method, 1, functools.Placeholder, 3.0)
|
||||
|
||||
result = get_annotations(MyClass.partial_method)
|
||||
|
||||
# 'self' stays, 'a' and 'c' are bound, 'b' remains
|
||||
# For unbound partialmethod, we expect 'self' if annotated, plus remaining params
|
||||
# Since 'self' isn't annotated, only 'b' and 'return' remain
|
||||
self.assertIn('b', result)
|
||||
self.assertIn('return', result)
|
||||
self.assertNotIn('a', result)
|
||||
self.assertNotIn('c', result)
|
||||
|
||||
def test_partialmethod_with_multiple_placeholders(self):
|
||||
"""Test partialmethod with multiple Placeholders."""
|
||||
class MyClass:
|
||||
def method(self, a: int, b: str, c: float, d: list) -> bool:
|
||||
return True
|
||||
|
||||
# Bind 'a', defer 'b', defer 'c', bind 'd'
|
||||
partial_method = functools.partialmethod(method, 1, functools.Placeholder, functools.Placeholder, [])
|
||||
|
||||
result = get_annotations(MyClass.partial_method)
|
||||
|
||||
# 'b' and 'c' remain unbound, 'a' and 'd' are bound
|
||||
self.assertIn('b', result)
|
||||
self.assertIn('c', result)
|
||||
self.assertIn('return', result)
|
||||
self.assertNotIn('a', result)
|
||||
self.assertNotIn('d', result)
|
||||
|
||||
|
||||
class TestFunctoolsPartial(unittest.TestCase):
|
||||
"""Tests for get_annotations() with functools.partial objects."""
|
||||
|
||||
def test_partial_basic(self):
|
||||
"""Test basic partial with positional argument."""
|
||||
def foo(a: int, b: str, c: float) -> bool:
|
||||
return True
|
||||
|
||||
partial_foo = functools.partial(foo, 1)
|
||||
result = get_annotations(partial_foo)
|
||||
|
||||
# 'a' is bound, so only b, c, and return should remain
|
||||
expected = {'b': str, 'c': float, 'return': bool}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partial_with_keyword(self):
|
||||
"""Test partial with keyword argument."""
|
||||
def foo(a: int, b: str, c: float) -> bool:
|
||||
return True
|
||||
|
||||
partial_foo = functools.partial(foo, b="hello")
|
||||
result = get_annotations(partial_foo)
|
||||
|
||||
# Keyword arguments don't remove parameters from signature
|
||||
expected = {'a': int, 'b': str, 'c': float, 'return': bool}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partial_all_args_bound(self):
|
||||
"""Test partial with all arguments bound."""
|
||||
def foo(a: int, b: str) -> bool:
|
||||
return True
|
||||
|
||||
partial_foo = functools.partial(foo, 1, "hello")
|
||||
result = get_annotations(partial_foo)
|
||||
|
||||
# Only return annotation should remain
|
||||
expected = {'return': bool}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partial_no_annotations(self):
|
||||
"""Test partial of function without annotations."""
|
||||
def foo(a, b, c):
|
||||
return True
|
||||
|
||||
partial_foo = functools.partial(foo, 1)
|
||||
result = get_annotations(partial_foo)
|
||||
|
||||
# Should return empty dict
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_nested_partial(self):
|
||||
"""Test nested partial applications."""
|
||||
def foo(a: int, b: str, c: float, d: list) -> bool:
|
||||
return True
|
||||
|
||||
partial1 = functools.partial(foo, 1)
|
||||
partial2 = functools.partial(partial1, "hello")
|
||||
result = get_annotations(partial2)
|
||||
|
||||
# a and b are bound, c and d remain
|
||||
expected = {'c': float, 'd': list, 'return': bool}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partial_no_return_annotation(self):
|
||||
"""Test partial without return annotation."""
|
||||
def foo(a: int, b: str):
|
||||
pass
|
||||
|
||||
partial_foo = functools.partial(foo, 1)
|
||||
result = get_annotations(partial_foo)
|
||||
|
||||
# Only b should remain
|
||||
expected = {'b': str}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partial_format_string(self):
|
||||
"""Test partial with STRING format."""
|
||||
def foo(a: int, b: str) -> bool:
|
||||
return True
|
||||
|
||||
partial_foo = functools.partial(foo, 1)
|
||||
result = get_annotations(partial_foo, format=Format.STRING)
|
||||
|
||||
# Should return strings
|
||||
expected = {'b': 'str', 'return': 'bool'}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partial_format_forwardref(self):
|
||||
"""Test partial with FORWARDREF format."""
|
||||
def foo(a: UndefinedType1, b: UndefinedType2) -> UndefinedReturnType:
|
||||
return True
|
||||
|
||||
partial_foo = functools.partial(foo, 1)
|
||||
result = get_annotations(partial_foo, format=Format.FORWARDREF)
|
||||
|
||||
# Should return forward references for undefined types
|
||||
expected = {
|
||||
'b': support.EqualToForwardRef('UndefinedType2', owner=foo),
|
||||
'return': support.EqualToForwardRef('UndefinedReturnType', owner=foo)
|
||||
}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partial_with_placeholder(self):
|
||||
"""Test partial with Placeholder for deferred argument."""
|
||||
def foo(a: int, b: str, c: float) -> bool:
|
||||
return True
|
||||
|
||||
# Placeholder in the middle: bind 'a', defer 'b', bind 'c'
|
||||
partial_foo = functools.partial(foo, 1, functools.Placeholder, 3.0)
|
||||
result = get_annotations(partial_foo)
|
||||
|
||||
# Only 'b' remains unbound (Placeholder defers it), 'a' and 'c' are bound
|
||||
# So we should have 'b' and 'return'
|
||||
expected = {'b': str, 'return': bool}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partial_with_multiple_placeholders(self):
|
||||
"""Test partial with multiple Placeholders."""
|
||||
def foo(a: int, b: str, c: float, d: list) -> bool:
|
||||
return True
|
||||
|
||||
# Bind 'a', defer 'b', defer 'c', bind 'd'
|
||||
partial_foo = functools.partial(foo, 1, functools.Placeholder, functools.Placeholder, [])
|
||||
result = get_annotations(partial_foo)
|
||||
|
||||
# 'b' and 'c' remain unbound, 'a' and 'd' are bound
|
||||
expected = {'b': str, 'c': float, 'return': bool}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_partial_placeholder_at_start(self):
|
||||
"""Test partial with Placeholder at the start."""
|
||||
def foo(a: int, b: str, c: float) -> bool:
|
||||
return True
|
||||
|
||||
# Defer 'a', bind 'b' and 'c'
|
||||
partial_foo = functools.partial(foo, functools.Placeholder, "hello", 3.0)
|
||||
result = get_annotations(partial_foo)
|
||||
|
||||
# Only 'a' remains unbound
|
||||
expected = {'a': int, 'return': bool}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_nested_partial_with_placeholder(self):
|
||||
"""Test nested partial applications with Placeholder."""
|
||||
def foo(a: int, b: str, c: float, d: list) -> bool:
|
||||
return True
|
||||
|
||||
# First partial: bind 'a', defer 'b', bind 'c'
|
||||
# (can't have trailing Placeholder)
|
||||
partial1 = functools.partial(foo, 1, functools.Placeholder, 3.0)
|
||||
# Second partial: provide 'b'
|
||||
partial2 = functools.partial(partial1, "hello")
|
||||
result = get_annotations(partial2)
|
||||
|
||||
# 'a', 'b', and 'c' are bound, only 'd' remains
|
||||
expected = {'d': list, 'return': bool}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
class TestAnnotationLib(unittest.TestCase):
|
||||
def test__all__(self):
|
||||
support.check__all__(self, annotationlib)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
Support ``__annotate__`` for :class:`functools.partial`
|
||||
and :class:`functools.partialmethod`
|
||||
|
|
@ -360,6 +360,32 @@ partial_descr_get(PyObject *self, PyObject *obj, PyObject *type)
|
|||
return PyMethod_New(self, obj);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
partial_annotate(PyObject *self, PyObject *format_obj)
|
||||
{
|
||||
/* Delegate to Python functools._partial_annotate helper */
|
||||
PyObject *functools = NULL, *helper = NULL, *result = NULL;
|
||||
|
||||
/* Import functools module */
|
||||
functools = PyImport_ImportModule("functools");
|
||||
if (functools == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Get the _partial_annotate function */
|
||||
helper = PyObject_GetAttrString(functools, "_partial_annotate");
|
||||
Py_DECREF(functools);
|
||||
if (helper == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Call _partial_annotate(self, format) */
|
||||
result = PyObject_CallFunctionObjArgs(helper, self, format_obj, NULL);
|
||||
Py_DECREF(helper);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
partial_vectorcall(PyObject *self, PyObject *const *args,
|
||||
size_t nargsf, PyObject *kwnames)
|
||||
|
|
@ -833,6 +859,7 @@ partial_setstate(PyObject *self, PyObject *state)
|
|||
static PyMethodDef partial_methods[] = {
|
||||
{"__reduce__", partial_reduce, METH_NOARGS},
|
||||
{"__setstate__", partial_setstate, METH_O},
|
||||
{"__annotate__", partial_annotate, METH_O},
|
||||
{"__class_getitem__", Py_GenericAlias,
|
||||
METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
|
||||
{NULL, NULL} /* sentinel */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue