-
-
Notifications
You must be signed in to change notification settings - Fork 33.5k
gh-141388: Fully support non-function callables as annotate functions #141449
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 8 commits
0328ac1
d98ef64
b600f8c
a9a7f88
4703e8d
6e31007
f752c48
9e4faa4
c4d8d9d
a83ca8f
e6bc7a0
2e4b927
4b955a8
f5ea8a0
a43a873
a3b68ee
ea60223
c16083b
ba1927c
b96532f
e70b489
8df3d24
61b76a5
ac888cc
6a48bbe
1d35db0
c2cccff
2ca8f95
33c9d13
d2dc8a3
4acb56b
b749c49
a76d794
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 |
|---|---|---|
|
|
@@ -728,13 +728,13 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): | |
| annotate, owner, is_class, globals, allow_evaluation=False | ||
| ) | ||
| func = types.FunctionType( | ||
| annotate.__code__, | ||
| _get_annotate_attr(annotate, "__code__", None), | ||
| globals, | ||
| closure=closure, | ||
| argdefs=annotate.__defaults__, | ||
| kwdefaults=annotate.__kwdefaults__, | ||
| argdefs=_get_annotate_attr(annotate, "__defaults__", None), | ||
| kwdefaults=_get_annotate_attr(annotate, "__kwdefaults__", None), | ||
| ) | ||
| annos = func(Format.VALUE_WITH_FAKE_GLOBALS) | ||
| annos = _direct_call_annotate(func, annotate, Format.VALUE_WITH_FAKE_GLOBALS) | ||
| if _is_evaluate: | ||
| return _stringify_single(annos) | ||
| return { | ||
|
|
@@ -759,11 +759,18 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): | |
| # reconstruct the source. But in the dictionary that we eventually return, we | ||
| # want to return objects with more user-friendly behavior, such as an __eq__ | ||
| # that returns a bool and an defined set of attributes. | ||
| namespace = {**annotate.__builtins__, **annotate.__globals__} | ||
| annotate_globals = _get_annotate_attr(annotate, "__globals__", {}) | ||
| annotate_code = _get_annotate_attr(annotate, "__code__", None) | ||
| annotate_defaults = _get_annotate_attr(annotate, "__defaults__", None) | ||
| annotate_kwdefaults = _get_annotate_attr(annotate, "__kwdefaults__", None) | ||
| namespace = { | ||
| **_get_annotate_attr(annotate, "__builtins__", {}), | ||
| **annotate_globals | ||
| } | ||
| is_class = isinstance(owner, type) | ||
| globals = _StringifierDict( | ||
| namespace, | ||
| globals=annotate.__globals__, | ||
| globals=annotate_globals, | ||
| owner=owner, | ||
| is_class=is_class, | ||
| format=format, | ||
|
|
@@ -772,14 +779,14 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): | |
| annotate, owner, is_class, globals, allow_evaluation=True | ||
| ) | ||
| func = types.FunctionType( | ||
| annotate.__code__, | ||
| annotate_code, | ||
| globals, | ||
| closure=closure, | ||
| argdefs=annotate.__defaults__, | ||
| kwdefaults=annotate.__kwdefaults__, | ||
| argdefs=annotate_defaults, | ||
| kwdefaults=annotate_kwdefaults, | ||
| ) | ||
| try: | ||
| result = func(Format.VALUE_WITH_FAKE_GLOBALS) | ||
| result = _direct_call_annotate(func, annotate, Format.VALUE_WITH_FAKE_GLOBALS) | ||
| except NotImplementedError: | ||
| # FORWARDREF and VALUE_WITH_FAKE_GLOBALS not supported, fall back to VALUE | ||
| return annotate(Format.VALUE) | ||
|
|
@@ -793,7 +800,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): | |
| # a value in certain cases where an exception gets raised during evaluation. | ||
| globals = _StringifierDict( | ||
| {}, | ||
| globals=annotate.__globals__, | ||
| globals=annotate_globals, | ||
| owner=owner, | ||
| is_class=is_class, | ||
| format=format, | ||
|
|
@@ -802,13 +809,13 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): | |
| annotate, owner, is_class, globals, allow_evaluation=False | ||
| ) | ||
| func = types.FunctionType( | ||
| annotate.__code__, | ||
| annotate_code, | ||
| globals, | ||
| closure=closure, | ||
| argdefs=annotate.__defaults__, | ||
| kwdefaults=annotate.__kwdefaults__, | ||
| argdefs=annotate_defaults, | ||
| kwdefaults=annotate_kwdefaults, | ||
| ) | ||
| result = func(Format.VALUE_WITH_FAKE_GLOBALS) | ||
| result = _direct_call_annotate(func, annotate, Format.VALUE_WITH_FAKE_GLOBALS) | ||
| globals.transmogrify(cell_dict) | ||
| if _is_evaluate: | ||
| if isinstance(result, ForwardRef): | ||
|
|
@@ -833,12 +840,13 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): | |
|
|
||
|
|
||
| def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluation): | ||
| if not annotate.__closure__: | ||
| closure = _get_annotate_attr(annotate, "__closure__", None) | ||
| if not closure: | ||
| return None, None | ||
| freevars = annotate.__code__.co_freevars | ||
| freevars = _get_annotate_attr(annotate, "__code__", None).co_freevars | ||
| new_closure = [] | ||
| cell_dict = {} | ||
| for i, cell in enumerate(annotate.__closure__): | ||
| for i, cell in enumerate(closure): | ||
| if i < len(freevars): | ||
| name = freevars[i] | ||
| else: | ||
|
|
@@ -857,7 +865,7 @@ def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluat | |
| name, | ||
| cell=cell, | ||
| owner=owner, | ||
| globals=annotate.__globals__, | ||
| globals=_get_annotate_attr(annotate, "__globals__", {}), | ||
| is_class=is_class, | ||
| stringifier_dict=stringifier_dict, | ||
| ) | ||
|
|
@@ -879,6 +887,48 @@ def _stringify_single(anno): | |
| return repr(anno) | ||
|
|
||
|
|
||
| def _get_annotate_attr(annotate, attr, default): | ||
| if (value := getattr(annotate, attr, None)) is not None: | ||
| return value | ||
|
|
||
| if isinstance(annotate.__call__, types.MethodType): | ||
| if call_func := getattr(annotate.__call__, "__func__", None): | ||
| return getattr(call_func, attr, default) | ||
| elif isinstance(annotate, type): | ||
| return getattr(annotate.__init__, attr, default) | ||
|
||
| elif ( | ||
| (functools := sys.modules.get("functools", None)) | ||
| and isinstance(annotate, functools.partial) | ||
| ): | ||
| return getattr(annotate.func, attr, default) | ||
|
|
||
| return default | ||
|
|
||
| def _direct_call_annotate(func, annotate, format): | ||
|
||
| # If annotate is a method, we need to pass its self as the first param | ||
| if ( | ||
| hasattr(annotate.__call__, "__func__") and | ||
| (self := getattr(annotate.__call__, "__self__", None)) | ||
| ): | ||
| return func(self, format) | ||
|
|
||
| # If annotate is a class, `func` is the __init__ method, so we still need to call | ||
| # __new__() to create the instance | ||
| if isinstance(annotate, type): | ||
| inst = annotate.__new__(annotate) | ||
| func(inst, format) | ||
| return inst | ||
|
|
||
| # If annotate is a partial function, re-create it with the new function object. | ||
| # We could call the function directly, but then we'd have to handle placeholders, | ||
| # and this way should be more robust for future changes. | ||
| if functools := sys.modules.get("functools", None): | ||
| if isinstance(annotate, functools.partial): | ||
| return functools.partial(func, *annotate.args, **annotate.keywords)(format) | ||
|
|
||
| return func(format) | ||
|
|
||
|
|
||
| def get_annotate_from_class_namespace(obj): | ||
| """Retrieve the annotate function from a class namespace dictionary. | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.