Skip to content

gh-148680: Replace internal names with type_reprs of objects in string representations of ForwardRef#148682

Open
DavidCEllis wants to merge 4 commits intopython:mainfrom
DavidCEllis:fix-forwardref-string-names
Open

gh-148680: Replace internal names with type_reprs of objects in string representations of ForwardRef#148682
DavidCEllis wants to merge 4 commits intopython:mainfrom
DavidCEllis:fix-forwardref-string-names

Conversation

@DavidCEllis
Copy link
Copy Markdown
Contributor

@DavidCEllis DavidCEllis commented Apr 17, 2026

This adds a new ForwardRef.__resolved_forward_str__ attribute which takes the __forward_arg__ and replaces any internal __annotationlib_name_x__ names with the type_repr of the object they represent.

Before:

ForwardRef('ref | __annotationlib_name_1__', is_class=True, owner=<class '__main__.Example'>)
ref | __annotationlib_name_1__

After:

ForwardRef('ref | str', is_class=True, owner=<class '__main__.Example'>)
ref | str

This approach replaces the extra_names with their type_repr when the forwardref is evaluated to a string. I'm fairly sure that trying to get the original names as they are in the source would be both slower and significantly more complicated.

This logic is largely based on something I already have for my reannotate library.

"typing.List[ForwardRef('int', owner='class')]",
)

def test_forward_repr_extra_names(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make the tests rely less on direct assignment to __extra_names__? I don't care too much about the behavior if you manually construct ForwardRefs and set attributes on them, what matters is the buggy behavior with things you get from get_annotations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can do that here. This was partly done to avoid the owner=... part of the repr.

The other example of this in test_evaluate_string_format_extra_names exists largely to exercise the logic for the single identifier case. I'm actually not sure you can get the single identifier case with standard get_annotations?

I need this in reannotate for splitting up unions within forward references but unless you're doing similar manipulations I'm not sure this is a case you can get?

Perhaps the isidentifier special case should be removed? The other logic should work for it anyway.

Comment thread Lib/annotationlib.py
resolved_str = type_repr(name_obj)
else:
visitor = _ExtraNameFixer(names)
ast_expr = ast.parse(resolved_str, mode="eval").body
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to cache this, it's probably pretty slow. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in fact, not very fast. Definitely worth caching if it's going to be accessed frequently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

>>> a_anno
ForwardRef('unknown | str | int | list[str] | tuple[int, ...]', is_class=True, owner=<class '__main__.Example'>)
>>> b_anno
ForwardRef('unknown', is_class=True, owner=<class '__main__.Example'>)

>>> a = timeit(lambda: a_anno.__resolved_forward_str__, number=10_000)
>>> b = timeit(lambda: b_anno.__resolved_forward_str__, number=10_000)
>>> a
0.18359258100008446
>>> b
0.0018204959997092374
>>> a / b
100.8475607907994

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants