Skip to content

Conversation

@Daraan
Copy link
Contributor

Fixes#310

This PR reverts injection of Union[..., NoneType] by typing.get_type_hints in Python <3.11 if a function uses a None default value.

# Values was not modified or original is already Optional
iforiginal_value==valueor_could_be_inserted_optional(original_value):
continue
# NoneType was added to value
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Alternatively hints[name] = original_value which should be equivalent. I wonder which would be the safer alternative.

Copy link
Member

Choose a reason for hiding this comment

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

Using original_value is incorrect as we may have modified the internals of the hint. For example, get_type_hints() turns List["int"] into List[int].

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback. Yes, it should have been piped trough _eval_type as well. Can you take a look again?

Copy link
Member

@JelleZijlstraJelleZijlstra left a comment

Choose a reason for hiding this comment

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

This PR is incorrect for this example:

>>> def f(x: Union[str, None, "str"] = None): pass ... >>> typing_extensions.get_type_hints(f){'x': <class 'str'>} 

I am not sure this approach is viable.

# Values was not modified or original is already Optional
iforiginal_value==valueor_could_be_inserted_optional(original_value):
continue
# NoneType was added to value
Copy link
Member

Choose a reason for hiding this comment

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

Using original_value is incorrect as we may have modified the internals of the hint. For example, get_type_hints() turns List["int"] into List[int].

@DaraanDaraan marked this pull request as draft October 9, 2024 13:07
@DaraanDaraan marked this pull request as ready for review October 9, 2024 16:46
Copy link
ContributorAuthor

@DaraanDaraan left a comment

Choose a reason for hiding this comment

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

What are your thoughts on the "viability"? For that one example or in general?

I tried to improve the recreation of the typing.get_type_hints path that is taken before the injection.

# Values was not modified or original is already Optional
iforiginal_value==valueor_could_be_inserted_optional(original_value):
continue
# NoneType was added to value
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback. Yes, it should have been piped trough _eval_type as well. Can you take a look again?

@JelleZijlstra
Copy link
Member

I haven't thought too hard about examples that might break things, but I'm concerned about using == to check whether the two annotations are the same; I don't know if we can rely on equality of annotation objects working reliably for this.

@Daraan
Copy link
ContributorAuthor

Daraan commented Oct 10, 2024

I haven't thought too hard about examples that might break things, but I'm concerned about using == to check whether the two annotations are the same; I don't know if we can rely on equality of annotation objects working reliably for this.

Do you refer to assertEqual here, or also for the ones in typing_extensions.py?

  • I'll also added hash checks, which are fine. str and repr are fine, too.
  • assertIs fails in some(to-many) cases depending on the python version.
    In essence the problem can be seen here:
importtypingfromtypingimportLista=List["str"] print(typing._eval_type(a, None, None) istyping._eval_type(a, None, None)) # False

typing._eval_type uses _GenericAlias.copy_with or in some cases creates a new GenericAlias which do not make use of the caches.

@DaraanDaraan marked this pull request as draft October 10, 2024 17:45
@DaraanDaraan marked this pull request as ready for review October 11, 2024 15:10
withself.subTest("Check str and repr"):
ifskip_reason=="UnionType not preserved in 3.10":
self.skipTest(skip_reason)
self.assertEqual(str(type_hints) +repr(type_hints), str(expected) +repr(expected))
Copy link
Member

Choose a reason for hiding this comment

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

Concatenating these seems a bit odd. In any case, type_hints is a dictionary, so the str() and repr() are the same.

Copy link
ContributorAuthor

@DaraanDaraanOct 21, 2024

Choose a reason for hiding this comment

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

Thanks for pointing this out I was not 100% sure here and kept a lazy version. Changed to only repr.

):
continue
original_value=original_hints[name]
iforiginal_valueisNone: # should not happen
Copy link
Member

Choose a reason for hiding this comment

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

Why shouldn't this happen? You can put None in annotations.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

At this point a None value should already be converted to NoneType in original_hints. I think these two lines are redundant but to be safe I added the double check.

Added a comment to clarify this.

Comment on lines 1310 to 1315
ifsys.version_info< (3, 9) andget_origin(original_evaluated) isUnion:
# Union[str, None, "str"] is not reduced to Union[str, None]
original_evaluated=Union[original_evaluated.__args__]
# Compare if values differ
iforiginal_evaluated!=value:
hints[name] =original_evaluated
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
ifsys.version_info< (3, 9) andget_origin(original_evaluated) isUnion:
# Union[str, None, "str"] is not reduced to Union[str, None]
original_evaluated=Union[original_evaluated.__args__]
# Compare if values differ
iforiginal_evaluated!=value:
hints[name] =original_evaluated
hints[name] =original_evaluated

Would this also work?

Copy link
ContributorAuthor

@DaraanDaraanOct 21, 2024

Choose a reason for hiding this comment

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

Yes it would. However there are minor differences involving identities and caching.

typing._eval_type uses _GenericAlias.copy_with or in some cases creates a new GenericAlias which do not make use of the caches.

If a ForwardRef is involved original_evaluated can be a complete new instance as _tp_cache is not queried for an existing equivalent one. Not changing hints["x"] will return an object that matches expected["x"] by identity in more cases.

That's a minor feature and I am fine with dropping it if you want; for now added a comment to clarify this.

Daraanand others added 2 commits October 21, 2024 15:10
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
continue
original_value=original_hints[name]
iforiginal_valueisNone: # should not happen
iforiginal_valueisNone: # should be NoneType already; check just in case
Copy link
Member

Choose a reason for hiding this comment

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

It shouldn't be NoneType already, since we get this directly from __annotations__.

Copy link
ContributorAuthor

@DaraanDaraanOct 21, 2024

Choose a reason for hiding this comment

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

(Edited for more clarity)
Excuse me I have argued wrongly. You are right that None would be from __annotations__. In such a case value should be NoneType. However, in that case _could_be_inserted_optional(NoneType) will skip this iteration (as get_origin(NoneType) is not Union).

That's why code with a original_value that is None should never propagate until this line, but keeping these two lines to keep it safe; updated the comment.

@Daraan
Copy link
ContributorAuthor

I am not sure why its still flagged as changes requested from me. I think this should be again waiting for review :)

@JelleZijlstraJelleZijlstra merged commit 86cf372 into python:mainFeb 11, 2025
23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Annotated/get_type_hints interaction in python <3.11

2 participants

@Daraan@JelleZijlstra