-
-
Notifications
You must be signed in to change notification settings - Fork 33.6k
gh-141388: Improve support for non-function callables as annotate functions #142327
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
91036ac
414251b
fd6125d
c008676
1ab0139
e177621
fe84920
d42d8ad
45cb956
eef70c4
44f2a45
095cfb5
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 | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -510,6 +510,72 @@ annotations from the class and puts them in a separate attribute: | |||||||||||||||||||||||||||||
| return typ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Creating a custom callable annotate function | ||||||||||||||||||||||||||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Custom :term:`annotate functions <annotate function>` may be literal functions like those | ||||||||||||||||||||||||||||||
| automatically generated for functions, classes, and modules. Or, they may wish to utilise | ||||||||||||||||||||||||||||||
| the encapsulation provided by classes, in which case any :term:`callable` can be used as | ||||||||||||||||||||||||||||||
| an :term:`annotate function`. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| However, :term:`methods <method>`, class instances that implement | ||||||||||||||||||||||||||||||
| :meth:`object.__call__`, and most other callables, do not provide the same attributes as | ||||||||||||||||||||||||||||||
| true functions, which are needed for the :attr:`~Format.VALUE_WITH_FAKE_GLOBALS` | ||||||||||||||||||||||||||||||
| machinery to work. :func:`call_annotate_function` and other :mod:`annotationlib` | ||||||||||||||||||||||||||||||
| functions will attempt to infer those attributes where possible, but some of them must | ||||||||||||||||||||||||||||||
| always be present for :attr:`~Format.VALUE_WITH_FAKE_GLOBALS` to work. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Below is an example of a callable class that provides the necessary attributes to be | ||||||||||||||||||||||||||||||
| used with all formats, and takes advantage of class encapsulation: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .. code-block:: python | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| class Annotate: | ||||||||||||||||||||||||||||||
|
Member
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. Indent the entire code block by 3 spaces (and then 4 spaces)
Member
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. I would rather have a protocol being explained, e.g.: The annotate protocol is an object with the following proeprties:
- A callable ``__call__`` attribute with the following signature: ...
- Properties `__defaults__`, `__kwdefaults__` and `__code__` such that ...It's not clear what is required and what's not and what we can put inside. And then you can have an example. |
||||||||||||||||||||||||||||||
| called_formats = [] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def __call__(self, format=None, *, _self=None): | ||||||||||||||||||||||||||||||
|
Member
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. Is this the signature we want to force? |
||||||||||||||||||||||||||||||
| # When called with fake globals, `_self` will be the | ||||||||||||||||||||||||||||||
| # actual self value, and `self` will be the format. | ||||||||||||||||||||||||||||||
| if _self is not None: | ||||||||||||||||||||||||||||||
| self, format = _self, self | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| self.called_formats.append(format) | ||||||||||||||||||||||||||||||
| if format <= 2: # VALUE or VALUE_WITH_FAKE_GLOBALS | ||||||||||||||||||||||||||||||
| return {"x": MyType} | ||||||||||||||||||||||||||||||
| raise NotImplementedError | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||
| def __defaults__(self): | ||||||||||||||||||||||||||||||
| return (None,) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||
| def __kwdefaults__(self): | ||||||||||||||||||||||||||||||
| return {"_self": self} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||
| def __code__(self): | ||||||||||||||||||||||||||||||
| return self.__call__.__code__ | ||||||||||||||||||||||||||||||
|
Comment on lines
+548
to
+558
Member
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.
Suggested change
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| This can then be called with: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .. code-block:: python | ||||||||||||||||||||||||||||||
|
Member
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.
Suggested change
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| >>> from annotationlib import call_annotate_function, Format | ||||||||||||||||||||||||||||||
| >>> call_annotate_function(Annotate(), format=Format.STRING) | ||||||||||||||||||||||||||||||
| {'x': 'MyType'} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Or used as the annotate function for an object: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .. code-block:: python | ||||||||||||||||||||||||||||||
|
Member
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.
Suggested change
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| >>> from annotationlib import get_annotations, Format | ||||||||||||||||||||||||||||||
| >>> class C: | ||||||||||||||||||||||||||||||
| ... pass | ||||||||||||||||||||||||||||||
| >>> C.__annotate__ = Annotate() | ||||||||||||||||||||||||||||||
| >>> get_annotations(Annotate(), format=Format.STRING) | ||||||||||||||||||||||||||||||
| {'x': 'MyType'} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
Comment on lines
+577
to
+578
Member
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.
Suggested change
|
||||||||||||||||||||||||||||||
| Limitations of the ``STRING`` format | ||||||||||||||||||||||||||||||
| ------------------------------------ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -711,6 +711,9 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): | |
| return annotate(format) | ||
| except NotImplementedError: | ||
| pass | ||
|
|
||
| annotate_defaults = getattr(annotate, "__defaults__", None) | ||
| annotate_kwdefaults = getattr(annotate, "__kwdefaults__", None) | ||
| if format == Format.STRING: | ||
| # STRING is implemented by calling the annotate function in a special | ||
| # environment where every name lookup results in an instance of _Stringifier. | ||
|
|
@@ -734,14 +737,23 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): | |
| globals = _StringifierDict({}, format=format) | ||
| is_class = isinstance(owner, type) | ||
| closure, _ = _build_closure( | ||
| annotate, owner, is_class, globals, allow_evaluation=False | ||
| annotate, owner, is_class, globals, | ||
| getattr(annotate, "__globals__", {}), allow_evaluation=False | ||
| ) | ||
| try: | ||
| annotate_code = annotate.__code__ | ||
| except AttributeError: | ||
| raise AttributeError( | ||
| "annotate function requires __code__ attribute", | ||
| name="__code__", | ||
| obj=annotate | ||
| ) | ||
|
Comment on lines
+743
to
+750
Member
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. I think we can just propagate the AttributeError without this wrapping. |
||
| func = types.FunctionType( | ||
| annotate.__code__, | ||
| annotate_code, | ||
| globals, | ||
| closure=closure, | ||
| argdefs=annotate.__defaults__, | ||
| kwdefaults=annotate.__kwdefaults__, | ||
| argdefs=annotate_defaults, | ||
| kwdefaults=annotate_kwdefaults, | ||
| ) | ||
| annos = func(Format.VALUE_WITH_FAKE_GLOBALS) | ||
| if _is_evaluate: | ||
|
|
@@ -768,24 +780,38 @@ 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 = getattr(annotate, "__globals__", {}) | ||
| if annotate_builtins := getattr(annotate, "__builtins__", None): | ||
| namespace = {**annotate_builtins, **annotate_globals} | ||
| elif annotate_builtins := annotate_globals.get("__builtins__"): | ||
| namespace = {**annotate_builtins, **annotate_globals} | ||
| else: | ||
| namespace = {**builtins.__dict__, **annotate_globals} | ||
| is_class = isinstance(owner, type) | ||
| globals = _StringifierDict( | ||
| namespace, | ||
| globals=annotate.__globals__, | ||
| globals=annotate_globals, | ||
| owner=owner, | ||
| is_class=is_class, | ||
| format=format, | ||
| ) | ||
| closure, cell_dict = _build_closure( | ||
| annotate, owner, is_class, globals, allow_evaluation=True | ||
| annotate, owner, is_class, globals, annotate_globals, allow_evaluation=True | ||
| ) | ||
| try: | ||
|
Member
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. Ditto |
||
| annotate_code = annotate.__code__ | ||
| except AttributeError: | ||
| raise AttributeError( | ||
| "annotate function requires __code__ attribute", | ||
| name="__code__", | ||
| obj=annotate | ||
| ) | ||
| 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) | ||
|
|
@@ -802,20 +828,20 @@ 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, | ||
| ) | ||
| closure, cell_dict = _build_closure( | ||
| annotate, owner, is_class, globals, allow_evaluation=False | ||
| annotate, owner, is_class, globals, annotate_globals, allow_evaluation=False | ||
|
Member
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. Wrap this under 80 chars (I think we can split after globals) |
||
| ) | ||
| 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) | ||
| globals.transmogrify(cell_dict) | ||
|
|
@@ -841,12 +867,13 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): | |
| raise ValueError(f"Invalid format: {format!r}") | ||
|
|
||
|
|
||
| def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluation): | ||
| if not annotate.__closure__: | ||
| def _build_closure(annotate, owner, is_class, stringifier_dict, | ||
| annotate_globals, *, allow_evaluation): | ||
| if not (annotate_closure := getattr(annotate, "__closure__", None)): | ||
| return None, None | ||
| new_closure = [] | ||
| cell_dict = {} | ||
| for name, cell in zip(annotate.__code__.co_freevars, annotate.__closure__, strict=True): | ||
| for name, cell in zip(annotate.__code__.co_freevars, annotate_closure, strict=True): | ||
| cell_dict[name] = cell | ||
| new_cell = None | ||
| if allow_evaluation: | ||
|
|
@@ -861,7 +888,7 @@ def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluat | |
| name, | ||
| cell=cell, | ||
| owner=owner, | ||
| globals=annotate.__globals__, | ||
| globals=annotate_globals, | ||
| is_class=is_class, | ||
| stringifier_dict=stringifier_dict, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Improve support, error messages, and documentation for non-function callables as | ||
| :term:`annotate functions <annotate function>`. |
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.
Only use 2 blank lines to separate sections.