-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
[3.14] annotationlib - get_annotations returns an empty annotations dict if an AttributeError
is raised when __annotations__
is accessed
#125618
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
Comments
Thanks, that's an interesting case. Not immediately clear to me how we can fix this; it's not clear how to tell where the AttributeError came from. Maybe we should make static types return an empty dict in their |
I was actually testing it to see if I'd get a |
In the FORWARDREF format we try to return |
If you directly access I'd expect
|
So I came back to this and realised that this is actually worse if some of the types are from a submodule that's only imported in a For instance:
So the definition of the dataclass can change depending on whether |
Was writing a test for this and found another variation of the bug along with a confusing error message which I'd consider a separate bug. in 3.13 if you try >>> inspect.get_annotations(X())
TypeError: <__main__.X object at 0x70591bb8c980> is not a module, class, or callable. However in the current 3.14 main there are no such guards which leads to some unexpected behaviour. The first case is the same bug as we have here in a different presentation. from annotationlib import get_annotations, Format
class Example:
a: int
ex = Example()
print(f"{get_annotations(ex) = }")
print(f"{get_annotations(Example) = }")
print(f"{get_annotations(ex) = }") Output:
The bug is this function: It assumes >>> class Example:
... a: int
...
>>> ex = Example()
>>> ex.__annotations__
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
ex.__annotations__
AttributeError: 'Example' object has no attribute '__annotations__'. Did you mean: '__annotate__'?
>>> Example.__annotations__
{'a': <class 'int'>}
>>> ex.__annotations__
{'a': <class 'int'>} Edit: Accidentally linked to the wrong function - it should be Footnotes
|
I wrote some test cases for this error. My initial thought is that The test cases - class TestGetAnnotationsAttributeError(unittest.TestCase):
def test_value(self):
class HasError:
missing_attribute: base.missing_attribute
# Before creating 'base'
with self.assertRaises(NameError):
annotationlib.get_annotations(HasError)
base = object()
# Should now be an attributeerror for base.missing_attribute
with self.assertRaises(AttributeError):
annotationlib.get_annotations(HasError)
def test_forwardref(self):
class HasError:
missing_attribute: base.missing_attribute
missing_ref = support.EqualToForwardRef(
"base.missing_attribute",
owner=HasError,
is_class=True,
)
# before creating 'base'
forward_annos = annotationlib.get_annotations(HasError, format=Format.FORWARDREF)
self.assertEqual(
forward_annos,
{"missing_attribute": missing_ref}
)
# Create base but the annotations should be the same
base = object()
forward_annos = annotationlib.get_annotations(HasError, format=Format.FORWARDREF)
self.assertEqual(
forward_annos,
{"missing_attribute": missing_ref}
)
def test_string(self):
class HasError:
missing_attribute: base.missing_attribute
string_annos = annotationlib.get_annotations(HasError, format=Format.STRING)
self.assertEqual(
string_annos,
{"missing_attribute": "base.missing_attribute"}
)
base = object()
string_annos = annotationlib.get_annotations(HasError, format=Format.STRING)
self.assertEqual(
string_annos,
{"missing_attribute": "base.missing_attribute"}
) |
#132195 makes some related changes. |
From these tests, #132195 makes The [Edit: rephrasing for clarity] |
The behavior with #132195 seems much more reasonable to me. I don't immediately see a way to get the behavior you want, where if we see |
Yes I'm not sure how to get there as after you've managed to pull the attribute out of the 'fake globals' dict correctly. At least without needing to make the fake globals dict do something very ugly. It does bug me that this is an area where Footnotes
|
There might be an internal reason not to do this1 but one possible solution would be to provide an empty dict as you do for This is almost certainly slower but could potentially be used as a fallback if the current method failed. Side note: A format that always returned Footnotes
|
I believe that would break cases like We could still use this approach as a fallback in cases where evaluation otherwise raises AttributeError; that seems safe.
I don't think this has been considered. I'm not sure I fully understand the use case. If you don't need anything evaluated, why are you evaluating it at all? If you need something dataclass-like that doesn't need to look at annotations at all, you can just forward from the wrapper's |
Ah, you're correct it does produce this currently and also that there doesn't seem to be a test for it. Would have been nice if there was an
For this dataclass-like I mostly1 don't care about the annotation value for class construction but I do need the keys and - as far as I know - you can't get one without the other. Ideally though, I'd also like to be able to keep a class FieldLike:
...
_type: ForwardRef
@property
def type(self):
try:
return self._type.evaluate()
except NameError, AttributeError, KeyError:
return self._type Then assume you have a class like this: @my_dataclass
class Example:
attrib: defer_imports.bigmodule.SomeType With an Footnotes
|
The behavior of your original example is now that FORWARDREF and VALUE fail with a sensible error Are you interested in contributing a PR for your suggestion to make the FORWARDREF format fall back to the approach in #125618 (comment) on exceptions? I think that's a good idea. |
Sure, I can probably find some time next week to have a look at that. Possibly another look to see if there's a way to do that and still get |
Thanks!
I think a way to do that could be to add a |
* origin/main: pythongh-125618: Make FORWARDREF format succeed more often (python#132818)
Bug report
Bug description:
If there's an annotation with an incorrect attribute access, the
AttributeError
causesget_annotations
to return an empty dictionary for the annotations instead of failing or returning aForwardRef
.Output
I think this is due to
_get_dunder_annotations
catchingAttributeError
and returning an empty dict, intended for static types but catching this by mistake.CPython versions tested on:
3.14
Operating systems tested on:
Linux
Linked PRs
The text was updated successfully, but these errors were encountered: