From 0ca6df4fcc9c3cd4aacb0c47a259c1def1371f1b Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 8 Oct 2025 15:35:24 +0800 Subject: [PATCH 01/24] gh-91002: Support functools.partial and functools.partialmethod inspect in annotationlib.get_annotations Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 76 +++++++++++++- Lib/annotationlib.py | 143 ++++++++++++++++++++++++++ Lib/test/test_annotationlib.py | 180 +++++++++++++++++++++++++++++++++ 3 files changed, 398 insertions(+), 1 deletion(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index d6f5055955e8cf..0676c5f134d48c 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -368,6 +368,9 @@ 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. + * For :class:`functools.partial` and :class:`functools.partialmethod` objects, + only returns annotations for parameters that have not been bound by the + partial application, along with the return annotation if present. *eval_str* controls whether or not values of type :class:`!str` are replaced with the result of calling :func:`eval` on those values: @@ -391,7 +394,8 @@ Functions * If *obj* is a callable, *globals* defaults to :attr:`obj.__globals__ `, although if *obj* is a wrapped function (using - :func:`functools.update_wrapper`) or a :class:`functools.partial` object, + :func:`functools.update_wrapper`), a :class:`functools.partial` object, + or a :class:`functools.partialmethod` object, it is unwrapped until a non-wrapped function is found. Calling :func:`!get_annotations` is best practice for accessing the @@ -405,6 +409,19 @@ Functions >>> get_annotations(f) {'a': , 'b': , 'return': } + :func:`!get_annotations` also works with :class:`functools.partial` and + :class:`functools.partialmethod` objects, returning only the annotations + for parameters that have not been bound: + + .. doctest:: + + >>> from functools import partial + >>> def add(a: int, b: int, c: int) -> int: + ... return a + b + c + >>> add_10 = partial(add, 10) + >>> get_annotations(add_10) + {'b': , 'c': , 'return': } + .. versionadded:: 3.14 .. function:: type_repr(value) @@ -422,6 +439,63 @@ Functions .. versionadded:: 3.14 +Using :func:`!get_annotations` with :mod:`functools` objects +-------------------------------------------------------------- + +:func:`get_annotations` has special support for :class:`functools.partial` +and :class:`functools.partialmethod` objects. When called on these objects, +it returns only the annotations for parameters that have not been bound by +the partial application, along with the return annotation if present. + +For :class:`functools.partial` objects, positional arguments bind to parameters +in order, and the annotations for those parameters are excluded from the result: + +.. doctest:: + + >>> from functools import partial + >>> def func(a: int, b: str, c: float) -> bool: + ... return True + >>> partial_func = partial(func, 1) # Binds 'a' + >>> get_annotations(partial_func) + {'b': , 'c': , 'return': } + +Keyword arguments in :class:`functools.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': , 'b': , 'c': , 'return': } + +For :class:`functools.partialmethod` objects accessed through a class (unbound), +the first parameter (usually ``self`` or ``cls``) is preserved, and subsequent +parameters are handled similarly to :class:`functools.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': , 'return': } + +When a :class:`functools.partialmethod` is accessed through an instance (bound), +it becomes a :class:`functools.partial` object and is handled accordingly: + +.. doctest:: + + >>> obj = MyClass() + >>> get_annotations(obj.partial_method) # Same as above, 'self' is also bound + {'b': , 'return': } + +This behavior ensures that :func:`get_annotations` returns annotations that +accurately reflect the signature of the partial or partialmethod object, as +determined by :func:`inspect.signature`. + + Recipes ------- diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 43e1d51bc4b807..77d12f5965989f 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -1062,11 +1062,139 @@ def annotations_to_string(annotations): } +def _get_annotations_for_partialmethod(partialmethod_obj, format): + """Get annotations for a functools.partialmethod object. + + 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 + + # Get the wrapped function + func = partialmethod_obj.func + + # Get annotations from the wrapped function + func_annotations = get_annotations(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(func) + 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 + new_annotations = {} + + # Keep return annotation if present + if 'return' in func_annotations: + new_annotations['return'] = func_annotations['return'] + + # 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 + + remaining_params = func_params[1:] + num_positional_bound = len(partial_args) + + for i, param_name in enumerate(remaining_params): + # Skip if this param is bound positionally + if i < num_positional_bound: + continue + + # For keyword binding: keep the annotation (keyword sets default, doesn't remove param) + if param_name in partial_keywords: + if param_name in func_annotations: + new_annotations[param_name] = func_annotations[param_name] + continue + + # This parameter is not bound, keep its annotation + if param_name in func_annotations: + new_annotations[param_name] = func_annotations[param_name] + + return new_annotations + + except (ValueError, TypeError): + # If we can't process, return the original annotations + return func_annotations + + +def _get_annotations_for_partial(partial_obj, format): + """Get annotations for a functools.partial object. + + Returns annotations for the wrapped function, but only for parameters + that haven't been bound by the partial application. + """ + import inspect + + # Get the wrapped function + func = partial_obj.func + + # Get annotations from the wrapped function + func_annotations = get_annotations(func, format=format) + + if not func_annotations: + return {} + + # Get the signature to determine which parameters are bound + try: + sig = inspect.signature(partial_obj) + except (ValueError, TypeError): + # If we can't get signature, return empty dict + return {} + + # Build new annotations dict with only unbound parameters + new_annotations = {} + + # Keep return annotation if present + if 'return' in func_annotations: + new_annotations['return'] = func_annotations['return'] + + # 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] + + return new_annotations + + def _get_and_call_annotate(obj, format): """Get the __annotate__ function and call it. May not return a fresh dictionary. """ + import functools + + # Handle functools.partialmethod objects (unbound) + # Check for __partialmethod__ attribute first + try: + partialmethod = obj.__partialmethod__ + except AttributeError: + pass + else: + if isinstance(partialmethod, functools.partialmethod): + return _get_annotations_for_partialmethod(partialmethod, format) + + # Handle functools.partial objects + if isinstance(obj, functools.partial): + return _get_annotations_for_partial(obj, format) + annotate = getattr(obj, "__annotate__", None) if annotate is not None: ann = call_annotate_function(annotate, format, owner=obj) @@ -1084,6 +1212,21 @@ def _get_dunder_annotations(obj): Does not return a fresh dictionary. """ + # Check for functools.partialmethod - skip __annotations__ and use __annotate__ path + import functools + try: + partialmethod = obj.__partialmethod__ + if isinstance(partialmethod, functools.partialmethod): + # Return None to trigger _get_and_call_annotate + return None + except AttributeError: + pass + + # Check for functools.partial - skip __annotations__ and use __annotate__ path + if isinstance(obj, functools.partial): + # Return None to trigger _get_and_call_annotate + return None + # This special case is needed to support types defined under # from __future__ import annotations, where accessing the __annotations__ # attribute directly might return annotations for the wrong class. diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index a8a8bcec76a429..fde6e173f98248 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1704,6 +1704,186 @@ def test_fwdref_invalid_syntax(self): fr.evaluate() +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, {}) + + +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: int, b: str) -> bool: + return True + + partial_foo = functools.partial(foo, 1) + result = get_annotations(partial_foo, format=Format.FORWARDREF) + + # Should resolve to actual types + expected = {'b': str, 'return': bool} + self.assertEqual(result, expected) + + class TestAnnotationLib(unittest.TestCase): def test__all__(self): support.check__all__(self, annotationlib) From 8ea48385370944b4bf44215842e8411d1642e345 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 8 Oct 2025 15:38:04 +0800 Subject: [PATCH 02/24] Add news Signed-off-by: Manjusaka --- .../next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst diff --git a/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst b/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst new file mode 100644 index 00000000000000..99966edd5899b7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst @@ -0,0 +1,2 @@ +Support :class:`functools.partial` and :class:`functools.partialmethod` +inspect in :func:`annotationlib.get_annotations` From 05bc32af14f98900121f69493216e8833ba49836 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 8 Oct 2025 15:41:40 +0800 Subject: [PATCH 03/24] fix docs Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 0676c5f134d48c..399a082460b348 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -326,8 +326,8 @@ Functions .. versionadded:: 3.14 -.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE) - +`.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE) +` Compute the annotations dict for an object. *obj* may be a callable, class, module, or other object with @@ -422,7 +422,7 @@ Functions >>> get_annotations(add_10) {'b': , 'c': , 'return': } - .. versionadded:: 3.14 + .. versionadded:: 3.15q .. function:: type_repr(value) From 6cc04afa15680c8bdf6f0317fb8b3343c5c3a55b Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 8 Oct 2025 15:41:51 +0800 Subject: [PATCH 04/24] fix docs Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 399a082460b348..08daf6a666dffe 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -422,7 +422,7 @@ Functions >>> get_annotations(add_10) {'b': , 'c': , 'return': } - .. versionadded:: 3.15q + .. versionadded:: 3.15 .. function:: type_repr(value) From dfd72cf70fb537457d18828c54130663bc68bfe8 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 8 Oct 2025 15:49:24 +0800 Subject: [PATCH 05/24] fix docs Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 08daf6a666dffe..23ad085f6e0d58 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -326,8 +326,8 @@ Functions .. versionadded:: 3.14 -`.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE) -` +.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE) + Compute the annotations dict for an object. *obj* may be a callable, class, module, or other object with From 95a05221fc65a0bda6fabebcdd38bc7d8cd562c9 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 8 Oct 2025 16:06:53 +0800 Subject: [PATCH 06/24] fix doctest Signed-off-by: Manjusaka --- Lib/annotationlib.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 77d12f5965989f..e012edeea0a40e 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -1094,13 +1094,10 @@ def _get_annotations_for_partialmethod(partialmethod_obj, format): partial_args = partialmethod_obj.args or () partial_keywords = partialmethod_obj.keywords or {} - # Build new annotations dict + # Build new annotations dict in proper order + # (parameters first, then return) new_annotations = {} - # Keep return annotation if present - if 'return' in func_annotations: - new_annotations['return'] = func_annotations['return'] - # The first parameter (self/cls) is always kept for unbound partialmethod first_param = func_params[0] if first_param in func_annotations: @@ -1128,6 +1125,10 @@ def _get_annotations_for_partialmethod(partialmethod_obj, format): 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 except (ValueError, TypeError): @@ -1160,17 +1161,18 @@ def _get_annotations_for_partial(partial_obj, format): return {} # Build new annotations dict with only unbound parameters + # (parameters first, then return) new_annotations = {} - # Keep return annotation if present - if 'return' in func_annotations: - new_annotations['return'] = func_annotations['return'] - # 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 From ed9b31bd38e4fa477014e3b84ada7c73a2ea9ea7 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 15 Oct 2025 22:02:13 +0800 Subject: [PATCH 07/24] fix review idea Signed-off-by: Manjusaka --- Lib/annotationlib.py | 145 ------------------------------------- Lib/functools.py | 123 +++++++++++++++++++++++++++++++ Modules/_functoolsmodule.c | 27 +++++++ 3 files changed, 150 insertions(+), 145 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index e012edeea0a40e..43e1d51bc4b807 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -1062,141 +1062,11 @@ def annotations_to_string(annotations): } -def _get_annotations_for_partialmethod(partialmethod_obj, format): - """Get annotations for a functools.partialmethod object. - - 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 - - # Get the wrapped function - func = partialmethod_obj.func - - # Get annotations from the wrapped function - func_annotations = get_annotations(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(func) - 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 - - remaining_params = func_params[1:] - num_positional_bound = len(partial_args) - - for i, param_name in enumerate(remaining_params): - # Skip if this param is bound positionally - if i < num_positional_bound: - continue - - # For keyword binding: keep the annotation (keyword sets default, doesn't remove param) - if param_name in partial_keywords: - if param_name in func_annotations: - new_annotations[param_name] = func_annotations[param_name] - continue - - # This parameter is not bound, keep its annotation - 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 - - except (ValueError, TypeError): - # If we can't process, return the original annotations - return func_annotations - - -def _get_annotations_for_partial(partial_obj, format): - """Get annotations for a functools.partial object. - - Returns annotations for the wrapped function, but only for parameters - that haven't been bound by the partial application. - """ - import inspect - - # Get the wrapped function - func = partial_obj.func - - # Get annotations from the wrapped function - func_annotations = get_annotations(func, format=format) - - if not func_annotations: - return {} - - # Get the signature to determine which parameters are bound - try: - sig = inspect.signature(partial_obj) - except (ValueError, TypeError): - # If we can't get signature, return empty dict - return {} - - # 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 - - def _get_and_call_annotate(obj, format): """Get the __annotate__ function and call it. May not return a fresh dictionary. """ - import functools - - # Handle functools.partialmethod objects (unbound) - # Check for __partialmethod__ attribute first - try: - partialmethod = obj.__partialmethod__ - except AttributeError: - pass - else: - if isinstance(partialmethod, functools.partialmethod): - return _get_annotations_for_partialmethod(partialmethod, format) - - # Handle functools.partial objects - if isinstance(obj, functools.partial): - return _get_annotations_for_partial(obj, format) - annotate = getattr(obj, "__annotate__", None) if annotate is not None: ann = call_annotate_function(annotate, format, owner=obj) @@ -1214,21 +1084,6 @@ def _get_dunder_annotations(obj): Does not return a fresh dictionary. """ - # Check for functools.partialmethod - skip __annotations__ and use __annotate__ path - import functools - try: - partialmethod = obj.__partialmethod__ - if isinstance(partialmethod, functools.partialmethod): - # Return None to trigger _get_and_call_annotate - return None - except AttributeError: - pass - - # Check for functools.partial - skip __annotations__ and use __annotate__ path - if isinstance(obj, functools.partial): - # Return None to trigger _get_and_call_annotate - return None - # This special case is needed to support types defined under # from __future__ import annotations, where accessing the __annotations__ # attribute directly might return annotations for the wrong class. diff --git a/Lib/functools.py b/Lib/functools.py index a92844ba7227b0..aaaf95a46645dd 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -467,6 +467,8 @@ def _method(cls_or_self, /, *args, **keywords): 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 +494,10 @@ def __get__(self, obj, cls=None): def __isabstractmethod__(self): return getattr(self.func, "__isabstractmethod__", False) + def __annotate__(self, format): + """Return annotations for the partial method.""" + return _partialmethod_annotate(self, format) + __class_getitem__ = classmethod(GenericAlias) @@ -513,6 +519,123 @@ def _unwrap_partialmethod(func): func = _unwrap_partial(func) return func +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 the wrapped function + func = partial_obj.func + + # Get annotations from the wrapped function + func_annotations = get_annotations(func, format=format) + + if not func_annotations: + return {} + + # Get the signature to determine which parameters are bound + try: + sig = inspect.signature(partial_obj) + except (ValueError, TypeError): + # If we can't get signature, return empty dict + return {} + + # 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 + +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 the wrapped function + func = partialmethod_obj.func + + # Get annotations from the wrapped function + func_annotations = get_annotations(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(func) + 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 + + remaining_params = func_params[1:] + num_positional_bound = len(partial_args) + + for i, param_name in enumerate(remaining_params): + # Skip if this param is bound positionally + if i < num_positional_bound: + continue + + # For keyword binding: keep the annotation (keyword sets default, doesn't remove param) + if param_name in partial_keywords: + if param_name in func_annotations: + new_annotations[param_name] = func_annotations[param_name] + continue + + # This parameter is not bound, keep its annotation + 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 + + except (ValueError, TypeError): + # If we can't process, return the original annotations + return func_annotations + ################################################################################ ### LRU Cache function decorator ################################################################################ diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c index 257d5c6d53611c..42b57547423afe 100644 --- a/Modules/_functoolsmodule.c +++ b/Modules/_functoolsmodule.c @@ -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) @@ -832,6 +858,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 */ From 2d606cfa0d4547d6dd26829e9f75dc9f5d4b21cb Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 15 Oct 2025 22:22:48 +0800 Subject: [PATCH 08/24] fix review idea Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 47 ++++++++++++----- Doc/library/functools.rst | 13 +++++ Lib/functools.py | 52 ++++++++++++++----- Lib/test/test_annotationlib.py | 93 ++++++++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 28 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 23ad085f6e0d58..17e4375fb01a6f 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -371,6 +371,7 @@ Functions * For :class:`functools.partial` and :class:`functools.partialmethod` objects, only returns annotations for parameters that have not been bound by the partial application, along with the return annotation if present. + See :ref:`below ` for details. *eval_str* controls whether or not values of type :class:`!str` are replaced with the result of calling :func:`eval` on those values: @@ -409,20 +410,7 @@ Functions >>> get_annotations(f) {'a': , 'b': , 'return': } - :func:`!get_annotations` also works with :class:`functools.partial` and - :class:`functools.partialmethod` objects, returning only the annotations - for parameters that have not been bound: - - .. doctest:: - - >>> from functools import partial - >>> def add(a: int, b: int, c: int) -> int: - ... return a + b + c - >>> add_10 = partial(add, 10) - >>> get_annotations(add_10) - {'b': , 'c': , 'return': } - - .. versionadded:: 3.15 + .. versionadded:: next .. function:: type_repr(value) @@ -438,6 +426,7 @@ Functions .. versionadded:: 3.14 +.. _functools-objects-annotations: Using :func:`!get_annotations` with :mod:`functools` objects -------------------------------------------------------------- @@ -495,6 +484,36 @@ This behavior ensures that :func:`get_annotations` returns annotations that accurately reflect the signature of the partial or partialmethod object, as determined by :func:`inspect.signature`. +If :func:`!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. + Recipes ------- diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst index f8ffb3f41d1210..5f7b144964645e 100644 --- a/Doc/library/functools.rst +++ b/Doc/library/functools.rst @@ -825,3 +825,16 @@ 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. + +However, :class:`partial` objects do support the :attr:`~object.__annotate__` protocol for +annotation introspection. When accessed, :attr:`!__annotate__` returns only the annotations +for parameters that have not been bound by the partial application, along with the return +annotation. This behavior is consistent with :func:`inspect.signature` and allows tools like +:func:`annotationlib.get_annotations` to work correctly with partial objects. See the +:mod:`annotationlib` module documentation for more information on working with annotations +on partial objects. + +:class:`partialmethod` objects similarly support :attr:`~object.__annotate__` for unbound methods. + +.. versionadded:: next + Added :attr:`~object.__annotate__` support to :class:`partial` and :class:`partialmethod`. diff --git a/Lib/functools.py b/Lib/functools.py index aaaf95a46645dd..ec834e0ae75501 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -541,9 +541,14 @@ def _partial_annotate(partial_obj, format): # Get the signature to determine which parameters are bound try: sig = inspect.signature(partial_obj) - except (ValueError, TypeError): - # If we can't get signature, return empty dict - return {} + 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) @@ -608,23 +613,37 @@ def _partialmethod_annotate(partialmethod_obj, format): # 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:] - num_positional_bound = len(partial_args) + + # 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): - # Skip if this param is bound positionally - if i < num_positional_bound: + # 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 - # For keyword binding: keep the annotation (keyword sets default, doesn't remove param) - if param_name in partial_keywords: + # 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 - # This parameter is not bound, keep its annotation - if param_name in func_annotations: - new_annotations[param_name] = func_annotations[param_name] + # Otherwise, this position is bound (not a Placeholder and within bounds) + # Skip it # Add return annotation at the end if 'return' in func_annotations: @@ -632,9 +651,14 @@ def _partialmethod_annotate(partialmethod_obj, format): return new_annotations - except (ValueError, TypeError): - # If we can't process, return the original annotations - return func_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 ################################################################################ ### LRU Cache function decorator diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index fde6e173f98248..d25115072b4ca4 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1783,6 +1783,43 @@ def method(self, a, b, c): 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.""" @@ -1883,6 +1920,62 @@ def foo(a: int, b: str) -> bool: expected = {'b': str, 'return': bool} 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): From 9387fdf82f8ac094bf9ce90821e94b608200f29d Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 15 Oct 2025 22:30:19 +0800 Subject: [PATCH 09/24] update news and docs Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 17e4375fb01a6f..67efd395b81b0f 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -393,11 +393,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__ `, - although if *obj* is a wrapped function (using - :func:`functools.update_wrapper`), a :class:`functools.partial` object, - or a :class:`functools.partialmethod` object, - it is unwrapped until a non-wrapped function is found. + :attr:`obj.__globals__ `. + If *obj* has a :attr:`~function.__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 + :attr:`!__wrapped__` attribute or :attr:`~functools.partial.func` attribute + repeatedly until a function with :attr:`~function.__globals__` is found. Calling :func:`!get_annotations` is best practice for accessing the annotations dict of any object. See :ref:`annotations-howto` for From 0e8a69897430fe26edb6fd4e3f8ba0ef9faa5294 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 15 Oct 2025 22:35:11 +0800 Subject: [PATCH 10/24] update news and docs Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 12 ++++++------ .../2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 67efd395b81b0f..42601d0153f9b8 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -368,10 +368,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. - * For :class:`functools.partial` and :class:`functools.partialmethod` objects, - only returns annotations for parameters that have not been bound by the - partial application, along with the return annotation if present. - See :ref:`below ` for details. + * Supports objects that provide their own :attr:`~object.__annotate__` method, + such as :class:`functools.partial` and :class:`functools.partialmethod`. + See :ref:`below ` for details on using + :func:`!get_annotations` with :mod:`functools` objects. *eval_str* controls whether or not values of type :class:`!str` are replaced with the result of calling :func:`eval` on those values: @@ -394,10 +394,10 @@ Functions to the *obj* class namespace. * If *obj* is a callable, *globals* defaults to :attr:`obj.__globals__ `. - If *obj* has a :attr:`~function.__wrapped__` attribute (such as functions + 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 - :attr:`!__wrapped__` attribute or :attr:`~functools.partial.func` attribute + ``__wrapped__`` attribute or :attr:`~functools.partial.func` attribute repeatedly until a function with :attr:`~function.__globals__` is found. Calling :func:`!get_annotations` is best practice for accessing the diff --git a/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst b/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst index 99966edd5899b7..8e05b428724e89 100644 --- a/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst +++ b/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst @@ -1,2 +1,2 @@ -Support :class:`functools.partial` and :class:`functools.partialmethod` -inspect in :func:`annotationlib.get_annotations` +Support `__annotate__` for :class:`functools.partial` +and :class:`functools.partialmethod` From 9954da291f267b7584d4494ee133612b225d5d2e Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Wed, 15 Oct 2025 22:37:57 +0800 Subject: [PATCH 11/24] update news and docs Signed-off-by: Manjusaka --- .../next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst b/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst index 8e05b428724e89..b807f2a709aa81 100644 --- a/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst +++ b/Misc/NEWS.d/next/Library/2025-10-08-15-37-59.gh-issue-91002.vNJdih.rst @@ -1,2 +1,2 @@ -Support `__annotate__` for :class:`functools.partial` +Support ``__annotate__`` for :class:`functools.partial` and :class:`functools.partialmethod` From 295b351c3afb4eca256f93746d15ef60ff486954 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Tue, 21 Oct 2025 05:10:55 +0800 Subject: [PATCH 12/24] fix docs Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 42601d0153f9b8..8d9e0a76366f5e 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -411,7 +411,7 @@ Functions >>> get_annotations(f) {'a': , 'b': , 'return': } - .. versionadded:: next + .. versionadded:: 3.14 .. function:: type_repr(value) From 70a0abc56c54491ee6a8b692e87f7bf099214889 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Tue, 21 Oct 2025 05:27:06 +0800 Subject: [PATCH 13/24] fix test Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 2 +- Lib/functools.py | 4 ++-- Lib/test/test_annotationlib.py | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 8d9e0a76366f5e..6ed0fcdbf0d3f0 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -398,7 +398,7 @@ 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 until a function with :attr:`~function.__globals__` is found. + 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 diff --git a/Lib/functools.py b/Lib/functools.py index ec834e0ae75501..ebb4ee542f661f 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -540,7 +540,7 @@ def _partial_annotate(partial_obj, format): # Get the signature to determine which parameters are bound try: - sig = inspect.signature(partial_obj) + 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 @@ -590,7 +590,7 @@ def _partialmethod_annotate(partialmethod_obj, format): # The first parameter (self/cls) should remain, but bound args should be removed try: # Get the function signature - func_sig = inspect.signature(func) + func_sig = inspect.signature(func, annotation_format=format) func_params = list(func_sig.parameters.keys()) if not func_params: diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index d25115072b4ca4..3f9b0315d32f0c 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1910,14 +1910,17 @@ def foo(a: int, b: str) -> bool: def test_partial_format_forwardref(self): """Test partial with FORWARDREF format.""" - def foo(a: int, b: str) -> bool: + def foo(a: UndefinedType1, b: UndefinedType2) -> UndefinedReturnType: return True partial_foo = functools.partial(foo, 1) result = get_annotations(partial_foo, format=Format.FORWARDREF) - # Should resolve to actual types - expected = {'b': str, 'return': bool} + # 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): From 561f067ab5605e48e9990883966b7cc3a20523bf Mon Sep 17 00:00:00 2001 From: Nadeshiko Manju Date: Sat, 22 Nov 2025 19:27:56 +0800 Subject: [PATCH 14/24] Apply suggestion from @merwok MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Éric --- Lib/functools.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index f622332dbe7b62..30a3fb2afab728 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -494,9 +494,7 @@ def __get__(self, obj, cls=None): def __isabstractmethod__(self): return getattr(self.func, "__isabstractmethod__", False) - def __annotate__(self, format): - """Return annotations for the partial method.""" - return _partialmethod_annotate(self, format) + __annotate__ = _partialmethod_annotate __class_getitem__ = classmethod(GenericAlias) From e8381c1accfbfcbd01463969725b36ba61fcce08 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Sat, 22 Nov 2025 19:31:46 +0800 Subject: [PATCH 15/24] fix review idea Signed-off-by: Manjusaka --- Lib/functools.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index f622332dbe7b62..ae5a482990d66e 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -529,11 +529,8 @@ def _partial_annotate(partial_obj, format): import inspect from annotationlib import get_annotations - # Get the wrapped function - func = partial_obj.func - # Get annotations from the wrapped function - func_annotations = get_annotations(func, format=format) + func_annotations = get_annotations(partial_obj.func, format=format) if not func_annotations: return {} @@ -577,11 +574,8 @@ def _partialmethod_annotate(partialmethod_obj, format): import inspect from annotationlib import get_annotations - # Get the wrapped function - func = partialmethod_obj.func - # Get annotations from the wrapped function - func_annotations = get_annotations(func, format=format) + func_annotations = get_annotations(partialmethod_obj.func, format=format) if not func_annotations: return {} @@ -590,7 +584,7 @@ def _partialmethod_annotate(partialmethod_obj, format): # The first parameter (self/cls) should remain, but bound args should be removed try: # Get the function signature - func_sig = inspect.signature(func, annotation_format=format) + func_sig = inspect.signature(partialmethod_obj.func, annotation_format=format) func_params = list(func_sig.parameters.keys()) if not func_params: From b69e8dd4b5e7756ed685b78fa20b37a885553943 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Sat, 22 Nov 2025 19:38:34 +0800 Subject: [PATCH 16/24] fix review idea Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 118 +++++++++------------------------- Doc/library/functools.rst | 96 ++++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 98 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 8e3a22bb01320d..83da6a8c05b883 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -385,8 +385,8 @@ Functions using ``getattr()`` and ``dict.get()`` for safety. * Supports objects that provide their own :attr:`~object.__annotate__` method, such as :class:`functools.partial` and :class:`functools.partialmethod`. - See :ref:`below ` for details on using - :func:`!get_annotations` with :mod:`functools` objects. + 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: @@ -442,93 +442,33 @@ Functions .. versionadded:: 3.14 -.. _functools-objects-annotations: - -Using :func:`!get_annotations` with :mod:`functools` objects --------------------------------------------------------------- - -:func:`get_annotations` has special support for :class:`functools.partial` -and :class:`functools.partialmethod` objects. When called on these objects, -it returns only the annotations for parameters that have not been bound by -the partial application, along with the return annotation if present. - -For :class:`functools.partial` objects, positional arguments bind to parameters -in order, and the annotations for those parameters are excluded from the result: - -.. doctest:: - - >>> from functools import partial - >>> def func(a: int, b: str, c: float) -> bool: - ... return True - >>> partial_func = partial(func, 1) # Binds 'a' - >>> get_annotations(partial_func) - {'b': , 'c': , 'return': } - -Keyword arguments in :class:`functools.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': , 'b': , 'c': , 'return': } - -For :class:`functools.partialmethod` objects accessed through a class (unbound), -the first parameter (usually ``self`` or ``cls``) is preserved, and subsequent -parameters are handled similarly to :class:`functools.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': , 'return': } - -When a :class:`functools.partialmethod` is accessed through an instance (bound), -it becomes a :class:`functools.partial` object and is handled accordingly: - -.. doctest:: - - >>> obj = MyClass() - >>> get_annotations(obj.partial_method) # Same as above, 'self' is also bound - {'b': , 'return': } - -This behavior ensures that :func:`get_annotations` returns annotations that -accurately reflect the signature of the partial or partialmethod object, as -determined by :func:`inspect.signature`. - -If :func:`!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. +Supporting annotations in custom objects +------------------------------------------- + +Objects can support annotation introspection by implementing the :attr:`~object.__annotate__` +protocol. When an object provides an :attr:`!__annotate__` attribute, :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 diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst index a5610a9884c117..7f44e801d67dd5 100644 --- a/Doc/library/functools.rst +++ b/Doc/library/functools.rst @@ -829,15 +829,93 @@ 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. -However, :class:`partial` objects do support the :attr:`~object.__annotate__` protocol for -annotation introspection. When accessed, :attr:`!__annotate__` returns only the annotations -for parameters that have not been bound by the partial application, along with the return -annotation. This behavior is consistent with :func:`inspect.signature` and allows tools like -:func:`annotationlib.get_annotations` to work correctly with partial objects. See the -:mod:`annotationlib` module documentation for more information on working with annotations -on partial objects. - -:class:`partialmethod` objects similarly support :attr:`~object.__annotate__` for unbound methods. +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': , 'c': , 'return': } + +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': , 'b': , 'c': , 'return': } + +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': , 'return': } + +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': , 'return': } + +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`. From 64931d4672d47c68ca3b7024adcafcd69bf94f94 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Sat, 22 Nov 2025 19:46:54 +0800 Subject: [PATCH 17/24] fix review idea Signed-off-by: Manjusaka --- Lib/functools.py | 282 ++++++++++++++++++++++++----------------------- 1 file changed, 146 insertions(+), 136 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index 867cc624d49a09..b0d63f305b1ea1 100644 --- a/Lib/functools.py +++ b/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 @@ -499,8 +645,6 @@ def __isabstractmethod__(self): __class_getitem__ = classmethod(GenericAlias) -# Helper functions - def _unwrap_partial(func): while isinstance(func, partial): func = func.func @@ -517,140 +661,6 @@ def _unwrap_partialmethod(func): func = _unwrap_partial(func) return func -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 - -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 ################################################################################ ### LRU Cache function decorator From 1e05eafd0b30a831adb18d1f47d9e784eb83ff85 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Sat, 22 Nov 2025 19:48:12 +0800 Subject: [PATCH 18/24] fix review idea Signed-off-by: Manjusaka --- Lib/functools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/functools.py b/Lib/functools.py index b0d63f305b1ea1..abba217a2e9376 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -644,6 +644,7 @@ def __isabstractmethod__(self): __class_getitem__ = classmethod(GenericAlias) +# Helper functions def _unwrap_partial(func): while isinstance(func, partial): @@ -661,7 +662,6 @@ def _unwrap_partialmethod(func): func = _unwrap_partial(func) return func - ################################################################################ ### LRU Cache function decorator ################################################################################ From c63a6b25e4aad67ffc57e5364e0fbd72bc191859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric?= Date: Sat, 22 Nov 2025 13:30:29 -0500 Subject: [PATCH 19/24] restore empty line --- Lib/functools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/functools.py b/Lib/functools.py index abba217a2e9376..ff8e2cfb532770 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -644,6 +644,7 @@ def __isabstractmethod__(self): __class_getitem__ = classmethod(GenericAlias) + # Helper functions def _unwrap_partial(func): From 5ef742d5e6cc53d50841c32263a2b9a3f20ee5dc Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Mon, 24 Nov 2025 14:49:47 +0800 Subject: [PATCH 20/24] fix review idea Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 83da6a8c05b883..a063964efa998d 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -442,11 +442,13 @@ 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 provides an :attr:`!__annotate__` attribute, :func:`get_annotations` +protocol. When an object’s class provides an :attr:`!__annotate__` attribute, :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. From c82fb7efadbe045e0fb30819f07fd960130a89ec Mon Sep 17 00:00:00 2001 From: Nadeshiko Manju Date: Sun, 14 Dec 2025 03:27:31 +0800 Subject: [PATCH 21/24] Update Doc/library/annotationlib.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Éric --- Doc/library/annotationlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index a063964efa998d..dd8e98f002ec57 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -448,7 +448,7 @@ 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__` attribute, :func:`get_annotations` +protocol. When an object provides an :attr:`!__annotate__` attribute, :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. From 7cddaca18893c6e0317455d8aafca818fcd8326d Mon Sep 17 00:00:00 2001 From: Nadeshiko Manju Date: Sun, 14 Dec 2025 03:27:46 +0800 Subject: [PATCH 22/24] Update Doc/library/annotationlib.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Éric --- Doc/library/annotationlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index dd8e98f002ec57..53324b534b6618 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -383,7 +383,7 @@ 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__` method, + * Supports objects that provide their own :attr:`~object.__annotate__` attribute, such as :class:`functools.partial` and :class:`functools.partialmethod`. See the :mod:`functools` module documentation for details on how these objects support annotations. From f6fe897c1dc2e64618b45ebffd437abd6f42efd8 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Sun, 14 Dec 2025 03:35:03 +0800 Subject: [PATCH 23/24] fix review idea Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 53324b534b6618..005f75d6acfe0e 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -448,7 +448,7 @@ Supporting annotations in custom objects ------------------------------------------- Objects can support annotation introspection by implementing the :attr:`~object.__annotate__` -protocol. When an object provides an :attr:`!__annotate__` attribute, :func:`get_annotations` +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. From 09b0bbb5a65aa18b6213a9624ed6efca720ebcf9 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Sun, 14 Dec 2025 03:36:41 +0800 Subject: [PATCH 24/24] fix review idea Signed-off-by: Manjusaka --- Doc/library/annotationlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 005f75d6acfe0e..5705a01164b74c 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -383,7 +383,7 @@ 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__` attribute, + * 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.