Skip to content
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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

colatkinson
Copy link

Steps

  • For new features or bug fixes, add a ChangeLog entry describing what your PR does.
  • Write a good description on what the PR does.

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

Type
🐛 Bug fix
✨ New feature
🔨 Refactoring
📜 Docs

Related Issue

pylint-dev/pylint#3162
pylint-dev/pylint#2296

@Pierre-Sassoulas Pierre-Sassoulas added the Enhancement ✨ Improvement to a component label Dec 17, 2021
@Pierre-Sassoulas Pierre-Sassoulas modified the milestones: 2.9.1, 2.10.0 Dec 17, 2021
@Pierre-Sassoulas Pierre-Sassoulas added the Needs review 🔍 Needs to be reviewed by one or multiple more persons label Dec 17, 2021
@colatkinson
Copy link
Author

Switched from checking for bit_count() to bit_length(), as I didn't realize the former was introduced in 3.10.

@cdce8p cdce8p self-requested a review December 18, 2021 13:16
Copy link
Collaborator

@DanielNoord DanielNoord left a 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.

astroid/brain/brain_typing.py Show resolved Hide resolved
tests/unittest_brain.py Outdated Show resolved Hide resolved
tests/unittest_brain.py Outdated Show resolved Hide resolved
tests/unittest_brain.py Outdated Show resolved Hide resolved
tests/unittest_brain.py Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
@colatkinson
Copy link
Author

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.

Copy link
Collaborator

@DanielNoord DanielNoord left a 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.

astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
tests/unittest_brain.py Outdated Show resolved Hide resolved
tests/unittest_brain.py Outdated Show resolved Hide resolved
tests/unittest_brain.py Outdated Show resolved Hide resolved
astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
@colatkinson
Copy link
Author

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!

@cdce8p cdce8p modified the milestones: 2.10.0, 2.11.0 Feb 27, 2022
@colatkinson colatkinson force-pushed the fix-typing-newtype-derivation branch 2 times, most recently from 957d223 to a1370de Compare March 7, 2022 11:34
Copy link
Collaborator

@DanielNoord DanielNoord left a 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 Show resolved Hide resolved
) -> typing.Iterator[nodes.ClassDef]:
"""Infer a typing.NewType(...) call"""
try:
func = next(node.func.infer(context=context_itton))
Copy link
Collaborator

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?

Copy link
Author

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.

astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
tests/unittest_brain.py Outdated Show resolved Hide resolved
@coveralls
Copy link

coveralls commented May 28, 2022

Pull Request Test Coverage Report for Build 3508001866

  • 81 of 83 (97.59%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.08%) to 92.367%

Changes Missing Coverage Covered Lines Changed/Added Lines %
astroid/brain/brain_typing.py 81 83 97.59%
Totals Coverage Status
Change from base Build 3503977238: 0.08%
Covered Lines: 9947
Relevant Lines: 10769

💛 - Coveralls

Copy link
Collaborator

@DanielNoord DanielNoord left a 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?

Comment on lines 139 to 140
if func.qname() != "typing.TypeVar":
raise UseInferenceDefault
Copy link
Collaborator

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.

except (InferenceError, StopIteration) as exc:
raise UseInferenceDefault from exc

if func.qname() != "typing.NewType":
Copy link
Collaborator

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
Copy link
Collaborator

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:
Copy link
Collaborator

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.

raise UseInferenceDefault

# Cannot infer from a dynamic class name (f-string)
if isinstance(node.args[0], JoinedStr):
Copy link
Collaborator

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
@colatkinson colatkinson force-pushed the fix-typing-newtype-derivation branch from e7fe8b3 to 6bbb840 Compare June 4, 2022 09:24
Copy link
Member

@cdce8p cdce8p left a 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.

Comment on lines +1793 to +1794
b = B(5)
b #@
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
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.

Comment on lines +200 to +202
new_node.postinit(
bases=new_bases, body=new_node.body, decorators=new_node.decorators
)
Copy link
Member

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.

Suggested change
new_node.postinit(
bases=new_bases, body=new_node.body, decorators=new_node.decorators
)
new_node.bases = new_bases

@cdce8p cdce8p removed this from the 2.12.0 milestone Jun 8, 2022
@colatkinson
Copy link
Author

@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 Uninferable without any transforms, but I'm not sure if there's a lighter-touch way around that.

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.

@cdce8p
Copy link
Member

cdce8p commented Jun 22, 2022

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.

An identity function might work, but I would patch ChildType. Instead it might be better to replace the NewType inferred function itself. That could also solve the type annotation issue. Basically the inferred type should become x instead of ChildType. You just don't get the subclassing.

Tbh though, I haven't had time to investigate if it's at all possible.

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.

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.

@cdce8p cdce8p marked this pull request as draft June 22, 2022 00:19
@cdce8p cdce8p added Blocked 🚧 A PR or issue blocked by another PR or issue inference labels Jun 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Blocked 🚧 A PR or issue blocked by another PR or issue Enhancement ✨ Improvement to a component inference Needs review 🔍 Needs to be reviewed by one or multiple more persons
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants