-
-
Notifications
You must be signed in to change notification settings - Fork 110
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
Fix Union[..., NoneType]
injection by get_type_hints
if a None
default value is used.
#482
base: main
Are you sure you want to change the base?
Conversation
src/typing_extensions.py
Outdated
# Values was not modified or original is already Optional | ||
if original_value == value or _could_be_inserted_optional(original_value): | ||
continue | ||
# NoneType was added to value |
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.
Alternatively hints[name] = original_value
which should be equivalent. I wonder which would be the safer alternative.
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.
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]
.
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.
Thank you for the feedback. Yes, it should have been piped trough _eval_type
as well. Can you take a look again?
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.
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.
src/typing_extensions.py
Outdated
# Values was not modified or original is already Optional | ||
if original_value == value or _could_be_inserted_optional(original_value): | ||
continue | ||
# NoneType was added to value |
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.
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]
.
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.
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.
src/typing_extensions.py
Outdated
# Values was not modified or original is already Optional | ||
if original_value == value or _could_be_inserted_optional(original_value): | ||
continue | ||
# NoneType was added to value |
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.
Thank you for the feedback. Yes, it should have been piped trough _eval_type
as well. Can you take a look again?
I haven't thought too hard about examples that might break things, but I'm concerned about using |
Do you refer to
import typing
from typing import List
a = List["str"]
print(typing._eval_type(a, None, None) is typing._eval_type(a, None, None)) # False
|
fixed wrong variable and format
src/test_typing_extensions.py
Outdated
with self.subTest("Check str and repr"): | ||
if skip_reason == "UnionType not preserved in 3.10": | ||
self.skipTest(skip_reason) | ||
self.assertEqual(str(type_hints) + repr(type_hints), str(expected) + repr(expected)) |
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.
Concatenating these seems a bit odd. In any case, type_hints
is a dictionary, so the str() and repr() are the same.
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.
Thanks for pointing this out I was not 100% sure here and kept a lazy version. Changed to only repr
.
src/typing_extensions.py
Outdated
): | ||
continue | ||
original_value = original_hints[name] | ||
if original_value is None: # should not happen |
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.
Why shouldn't this happen? You can put None
in annotations.
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.
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.
src/typing_extensions.py
Outdated
if sys.version_info < (3, 9) and get_origin(original_evaluated) is Union: | ||
# Union[str, None, "str"] is not reduced to Union[str, None] | ||
original_evaluated = Union[original_evaluated.__args__] | ||
# Compare if values differ | ||
if original_evaluated != value: | ||
hints[name] = original_evaluated |
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.
if sys.version_info < (3, 9) and get_origin(original_evaluated) is Union: | |
# Union[str, None, "str"] is not reduced to Union[str, None] | |
original_evaluated = Union[original_evaluated.__args__] | |
# Compare if values differ | |
if original_evaluated != value: | |
hints[name] = original_evaluated | |
hints[name] = original_evaluated |
Would this also work?
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.
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 new object as _tp_cache
is not queried. 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.
Co-authored-by: Jelle Zijlstra <[email protected]>
+ updated comments
src/typing_extensions.py
Outdated
@@ -1282,7 +1282,7 @@ def _clean_optional(obj, hints, globalns=None, localns=None): | |||
): | |||
continue | |||
original_value = original_hints[name] | |||
if original_value is None: # should not happen | |||
if original_value is None: # should be NoneType already; check just in case |
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.
It shouldn't be NoneType already, since we get this directly from __annotations__
.
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.
Excuse me I have argued wrongly. You are right that None
would be from __annotations__
. In such a case value
should be NoneType
and _could_be_inserted_optional(NoneType)
would have already skipped this iteration (get_origin(NoneType) is not Union
).
That's why original_value
would still never be None
here; updated the comment.
Fixes #310
This PR reverts injection of
Union[..., NoneType]
bytyping.get_type_hints
in Python <3.11 if a function uses aNone
default value.