-
-
Notifications
You must be signed in to change notification settings - Fork 276
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
Treat NewTypes like normal subclasses #1301
base: main
Are you sure you want to change the base?
Treat NewTypes like normal subclasses #1301
Conversation
4cf70d3
to
9caef00
Compare
9caef00
to
b634003
Compare
Switched from checking for |
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 @colatkinson!
This seems like a good change and closes two old pylint
issues. Let me know if you have any questions about my comments.
b634003
to
a49de04
Compare
Made the requested changes -- let me know if I missed any! I also changed the transformation a bit, so that it takes in the correct number of arguments, and beefed up the tests a bit, both here and in the pylint PR (pylint-dev/pylint#5542). I also had to change the transformation to accurately resolve references to user-defined and imported types, so adding those tests was definitely important. |
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 @colatkinson
I'm a bit out of my depth with your last question so I hope another contributor with more astroid
experience finds the time to review this soon. Just have some final questions myself.
a49de04
to
0233147
Compare
Hey sorry about the delay. I haven't had a whole lot of bandwidth to work on this, but I'm hoping to be able to get back to it this week. Just a heads up it hasn't totally fallen off my radar! |
957d223
to
a1370de
Compare
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.
I like the approach you took! I did a first review and left some comments. Let me know if you have any questions!
astroid/brain/brain_typing.py
Outdated
) -> typing.Iterator[nodes.ClassDef]: | ||
"""Infer a typing.NewType(...) call""" | ||
try: | ||
func = next(node.func.infer(context=context_itton)) |
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 there is any ambiguity in the inference then we fail to notice that here. Perhaps we should use helpers.safe_infer
?
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.
Ran into a small issue with this. Since decimal.Decimal
has two implementations (_pydecimal
vs _decimal
) it always seems to end up ambiguous.
Is this the desired behavior for cases like this? If so I'll make the change and rework the tests, since I chose Decimal
as a test type arbitrarily anyway, but I figured I'd confirm.
a1370de
to
e7fe8b3
Compare
Pull Request Test Coverage Report for Build 3508001866
💛 - Coveralls |
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.
Some final comments.
I'd really like somebody with more experience on inference to take a look at this as I'm not sure the way we resolve the base names here is something we normally do in the brains
or if we have utils
for this.
@cdce8p Do you see chance to take a look at this PR perhaps?
astroid/brain/brain_typing.py
Outdated
if func.qname() != "typing.TypeVar": | ||
raise UseInferenceDefault |
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.
I believe this can be removed as it is non-sensical. This is tested by looks_like_typing_typevar
.
astroid/brain/brain_typing.py
Outdated
except (InferenceError, StopIteration) as exc: | ||
raise UseInferenceDefault from exc | ||
|
||
if func.qname() != "typing.NewType": |
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.
I believe this can be removed as it is non-sensical. This is tested by looks_like_typing_newtype
.
except (InferenceError, StopIteration) as exc: | ||
raise UseInferenceDefault from exc | ||
|
||
if func.qname() not in TYPING_TYPEVARS_QUALIFIED: | ||
if func.qname() != "typing.TypeVar": | ||
raise UseInferenceDefault | ||
if not node.args: | ||
raise UseInferenceDefault |
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.
L127-128 has a drop in coverage. Could you re-create a test for it?
|
||
if func.qname() != "typing.NewType": | ||
raise UseInferenceDefault | ||
if len(node.args) != 2: |
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.
Could you create a test for this? It is currently uncovered.
astroid/brain/brain_typing.py
Outdated
raise UseInferenceDefault | ||
|
||
# Cannot infer from a dynamic class name (f-string) | ||
if isinstance(node.args[0], JoinedStr): |
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.
You can probably re-use the test for this for the comment above.
NewTypes are assumed not to inherit any members from their base classes. This results in incorrect inference results. Avoid this by changing the transformation for NewTypes to treat them like any other subclass. pylint-dev/pylint#3162 pylint-dev/pylint#2296
e7fe8b3
to
6bbb840
Compare
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 your patience. I finally got around to look it your PR. First of all, I'm quite impressed with the work you did here @colatkinson! You obviously have a pretty good understanding of the astroid internals already.
However, as impressive as the PR is, I'm not quite sure the current approach is the correct one. You've modeled the inference result after the typing implementation. Astroid (and pylint) aren't type checkers though. They use inference to determine the runtime
results. For a NewType
call, that's simply the argument itself which is returned and not an instance of a subclass.
from typing import NewType
B = NewType("B", "A")
class A:
def __init__(self, value: int):
self.value = value
print(B(A(5)))
<__main__.A object at 0x100b1b160>
The inference result with the current implementation:
[<Instance of .B at 0...427248880>]
The CPython implementation: https://github.com/python/cpython/blob/v3.10.5/Lib/typing.py#L2482-L2483
--
Am I missing something / what do you think? In case we change the approach, I would like to suggest that you create a new PR for it. That way we can at least preserve the idea and implementation of resolving base classes should there ever be a need for it.
b = B(5) | ||
b #@ |
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.
b = B(5) | |
b #@ | |
b = B(A(5)) | |
b #@ |
What is the inference result of b
? At runtime it should be an instance of A
.
new_node.postinit( | ||
bases=new_bases, body=new_node.body, decorators=new_node.decorators | ||
) |
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.
new_node
is fully constructed already. It's enough to set bases
manually.
new_node.postinit( | |
bases=new_bases, body=new_node.body, decorators=new_node.decorators | |
) | |
new_node.bases = new_bases |
@cdce8p Thanks for taking a look! The feedback from you and @DanielNoord has been super helpful throughout this process. Your point about the divergence of the inferred and runtime types make sense to me. I modeled the implementation on the one currently in the main branch, which leaves things in roughly the same state (inferring a class that doesn't exist at runtime), but that's kind of the source of all this trouble in the first place 😉. Do you have any thoughts or pointers on a more correct implementation? My initial thought would be a simple identity function, e.g. def ChildType(x):
return x Do you think that would be sufficient? It could be a bit strange that the code would infer a function, which would then be referenced by type annotations elsewhere, but I'm not sure if this presents any problems in practice. It might also make sense to have the inferred implementation be identical to the upstream CPython one you linked -- though in that case, is any transformation even necessary? Based on a quick test, the results come back as Finally, with regards to creating a new PR: I was planning on keeping the current implementation around in the git history. Though if a new PR is easier for you and other maintainers, or if the project generally makes use of squash commits, I can definitely do that instead. |
An identity function might work, but I would patch Tbh though, I haven't had time to investigate if it's at all possible.
We do use squash merge usually. However, my reasoning was more from a practical approach. No unrelated comments and if everything else fails, we can still come back to this one. I'll convert the PR to a draft. We can close it once the alternative is merged. |
Steps
Description
NewTypes are assumed not to inherit any members from their base classes.
This results in incorrect inference results. Avoid this by changing the
transformation for NewTypes to treat them like any other subclass.
Type of Changes
Related Issue
pylint-dev/pylint#3162
pylint-dev/pylint#2296