python / cpython

The Python programming language
https://www.python.org
Other
63.49k stars 30.41k forks source link

[3.14] annotationlib - get_annotations returns an empty annotations dict if an `AttributeError` is raised when `__annotations__` is accessed #125618

Open DavidCEllis opened 1 month ago

DavidCEllis commented 1 month ago

Bug report

Bug description:

If there's an annotation with an incorrect attribute access, the AttributeError causes get_annotations to return an empty dictionary for the annotations instead of failing or returning a ForwardRef.

import typing
from annotationlib import get_annotations, Format

class Example2:
    real_attribute: typing.Any
    fake_attribute: typing.DoesNotExist

new_ann = get_annotations(Example2, format=Format.FORWARDREF)

print(f"{new_ann=}")

# This should fail, but instead returns an empty dict
value_ann = get_annotations(Example2, format=Format.VALUE)

print(f"{value_ann=}")

string_ann = get_annotations(Example2, format=Format.STRING)

print(f"{string_ann=}")

Output

new_ann={}
value_ann={}
string_ann={'real_attribute': 'typing.Any', 'fake_attribute': 'typing.DoesNotExist'}

I think this is due to _get_dunder_annotations catching AttributeError 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

JelleZijlstra commented 1 month ago

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 __annotations__ descriptor instead of throwing AttributeError.

DavidCEllis commented 1 month ago

I was actually testing it to see if I'd get a _Stringifier again or a ForwardRef based on the other issue and was a bit surprised when I got nothing instead.

JelleZijlstra commented 1 month ago

In the FORWARDREF format we try to return .__annotations__ first. If that succeeds, we're done. And in this case, it arguably does succeed.

DavidCEllis commented 1 month ago

If you directly access Example2.__annotations__ you get the AttributeError though. The 'success' is entirely because this error is being silenced.

I'd expect VALUE to raise this exception. I'd expect FORWARDREF to either also error in this way or return a ForwardRef for the (currently) invalid attribute.

Traceback (most recent call last):
  File "/home/david/src/scratch/annotation_issue.py", line 34, in <module>
    Example2.__annotations__
  File "/home/david/src/scratch/annotation_issue.py", line 19, in __annotate__
    fake_attribute: typing.DoesNotExist
                    ^^^^^^^^^^^^^^^^^^^
  File "/home/david/src/cpython/Lib/typing.py", line 3829, in __getattr__
    raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
AttributeError: module 'typing' has no attribute 'DoesNotExist'