-
-
Notifications
You must be signed in to change notification settings - Fork 33.1k
gh-91002: Support functools.partial and functools.partialmethod inspect in annotationlib.get_annotations #139753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1062,11 +1062,141 @@ 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will fail if the original function's signature has annotations that do not evaluate in the VALUE 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 | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? If we can't get correct annotations, we ideally shouldn't return wrong ones. |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This import is in the crucial code path for annotationlib, it doesn't make sense to make it lazy. |
||
|
||
# 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 +1214,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. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could refer to the new section below rather that repeating the example, what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM