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

Fix frame() error on inferred node #1263

Merged

Conversation

tushar-deepsource
Copy link
Contributor

@tushar-deepsource tushar-deepsource commented Nov 20, 2021

Description

Ran into this issue today. The parent of an inferred node was None, so node.frame() was crashing. This should fix that.

Type of Changes

Type
βœ“ πŸ› Bug fix

@DanielNoord
Copy link
Collaborator

DanielNoord commented Nov 20, 2021

On which commit of astroid did you experience this issue? We have recently worked on NodeNG.frame() in #1225 and #1221, but it seems we have not fixed all issues.

In any case, whenever NodeNG.parent is missing we should raise the new ParentMissingError, or perhaps a new FrameMissing inheriting from it as we did for NodeNG.statement() in #1217.

This was quite difficult to do correctly and took me some tries to get right, but you're more than welcome to start working towards it! I'm happy to provide support a long the way and answer any questions you might have!

@tushar-deepsource
Copy link
Contributor Author

I encountered this on 2.7.3, with pylint 2.10.2 I believe.

@tushar-deepsource
Copy link
Contributor Author

Actually, in my case the latest master fixes this issue. Thanks though!

@tushar-deepsource tushar-deepsource deleted the tushar-deepsource-patch-1 branch November 20, 2021 18:03
@DanielNoord DanielNoord removed this from the 2.9.0 milestone Nov 21, 2021
@tushar-deepsource tushar-deepsource restored the tushar-deepsource-patch-1 branch November 21, 2021 17:50
@tushar-deepsource
Copy link
Contributor Author

It came back. I needed this fix to make inference work locally again today.

@tushar-deepsource
Copy link
Contributor Author

Does this seem good?
Should I do the same for .statement() as well?

@Pierre-Sassoulas Pierre-Sassoulas added this to the 2.9.1 milestone Nov 21, 2021
@cdce8p cdce8p self-requested a review November 21, 2021 18:52
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 for reopening the issue!

Would you mind adding a test for this as well? You can probably base it on the code that is currently throwing an error for you or look at how we tested StatementMissing in #1217!

@@ -311,6 +310,13 @@ def frame(

:returns: The first parent frame node.
"""
if self.parent is None:
if isinstance(
Copy link
Collaborator

@DanielNoord DanielNoord Nov 22, 2021

Choose a reason for hiding this comment

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

I don't think this isinstance check should be here. If self is either of these nodes we shouldn't be in this function anyway as they have their frame() defined in their class definitions.

I think just raising the exception is fine.

I looked at NamedExpr.frame() which seems to raise ParentMissingError. I think if we go ahead with raising FrameMissing we might use this exception there as well. It shouldn't be a breaking change since FrameMissing inherits from ParentMissingError.

Copy link
Member

Choose a reason for hiding this comment

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

The isinstance check is indeed unnecessary. Just this would be enough:

        if not self.parent:
            raise ParentMissingError(target=self)

        return self.parent.frame()

--
Technically this is a small breaking change as users could be fine with catching AttributeError.
We added future=True to statement() so just for completeness we should probably do that here, too. You can take a look at #1217 as example.

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.

Should I do the same for .statement() as well?

@tushar-deepsource What do you mean with that? @DanielNoord mentioned it already but we reworked statement() in #1217. With future=True this should already work.

This change will also need a changelog entry and tests.

Would you feel confident doing these changes? It should be relatively straightforward if you follow the lead of #1217. Please let us know if you need help or if we should take this one over. For a first PR this is quite complex.

Comment on lines 293 to 306
class FrameMissing(ParentMissingError):
"""Raised when a call to node.frame() does not return a node. This is because
a node in the chain does not have a parent attribute and therefore does not
return a node for frame().

Standard attributes:
target: The node for which the parent lookup failed.
"""

def __init__(self, target: "nodes.NodeNG") -> None:
# pylint: disable-next=bad-super-call
super(ParentMissingError, self).__init__(
message=f"Frame not found on {target!r}"
)
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if it's actually necessary to create a new exception subclass. All existing frame() and scope() methods raise just a ParentMissingError.

@DanielNoord I don't actually remember the exact details why we decided to add StatementMissingError maybe you do? Was there an actual need to have a more specific error or was it just an idea that we adopted?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this followed from the logic that we were catching the AttributeError ourselves so it sort of naturally followed that we had to create a new error to catch.
It might be a bit overkill, but I think it makes sense to make a new subclass: the more fine grained the error is the better users will know how to handle it.

@@ -311,6 +310,13 @@ def frame(

:returns: The first parent frame node.
"""
if self.parent is None:
if isinstance(
Copy link
Member

Choose a reason for hiding this comment

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

The isinstance check is indeed unnecessary. Just this would be enough:

        if not self.parent:
            raise ParentMissingError(target=self)

        return self.parent.frame()

--
Technically this is a small breaking change as users could be fine with catching AttributeError.
We added future=True to statement() so just for completeness we should probably do that here, too. You can take a look at #1217 as example.

@cdce8p cdce8p modified the milestones: 2.9.1, 2.10.0 Dec 7, 2021
@cdce8p cdce8p removed the crash label Dec 7, 2021
@tushar-deepsource
Copy link
Contributor Author

To double check: I'd have to essentially replicate the type stubs and tests for statement, for frame in this PR?

If the changes so far make sense, I can go ahead and do that.

@DanielNoord
Copy link
Collaborator

To double check: I'd have to essentially replicate the type stubs and tests for statement, for frame in this PR?

If the changes so far make sense, I can go ahead and do that.

I'll wait for @cdce8p to comment on whether he thinks that subclassing ParentMissingError makes sense here. Either way we should raise something for if not node.parent.

Other than that I think the following todo list should bring this PR a long way towards getting merged:

  1. Add keyword only argument future to frame()
  2. Add check for future argument and raise Frame/ParentMissingError for if not node.parent
  3. Add DeprecationWarning and TODO for calls to frame() which raise AttributeError (also raise AttributeError)
  4. Add overloads to frame()
  5. Add tests for all variants of frame() introduced by this PR. See Add typing to NodeNG.statementΒ #1217 for examples.

Follow-up PR:
6) Add future keyword to all internal calls to frame() (see #1235)

For all of these changes #1217 and #1235 and the discussions within those PR could help.

@cdce8p
Copy link
Member

cdce8p commented Dec 14, 2021

Thanks for the summery @DanielNoord! That looks about right to me.

I'll wait for @cdce8p to comment on whether he thinks that subclassing ParentMissingError makes sense here.

Usually just subclassing an exception doesn't really hurt much. However in this case it also doesn't add any more information. I personally don't really see a need to be even more specific than ParentMissingError already is. Adding FrameMissing just introduces unnecessary complexity both for users and us. So I would recommend to stick with the ParentMissingError.

@DanielNoord
Copy link
Collaborator

Usually just subclassing an exception doesn't really hurt much. However in this case it also doesn't add any more information. I personally don't really see a need to be even more specific than ParentMissingError already is. Adding FrameMissing just introduces unnecessary complexity both for users and us. So I would recommend to stick with the ParentMissingError.

Fine with me!

@tushar-deepsource Let me know if you need any help with implementing this. Happy to help as always πŸ˜„

astroid/nodes/node_ng.py Outdated Show resolved Hide resolved
astroid/nodes/node_ng.py Outdated Show resolved Hide resolved
@tushar-deepsource
Copy link
Contributor Author

Should be good now

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.

Looks good overall!

@Pierre-Sassoulas @DanielNoord What do you guys think, is a changelog entry necessary? We didn't add one for #1217. In theory everyone that depends on the AttributeError should get a DeprecationWarning. We can (or maybe should ?) include a note when we eventually release 3.0.0.

@@ -314,7 +313,7 @@ def statement(
return self.parent.statement(future=future)

def frame(
self,
self, *, future: Literal[None, True] = None
Copy link
Member

Choose a reason for hiding this comment

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

Turns out the overload idea doesn't actually work as I though. Sorry for the confusion. The default with None is needed for the parent calls.

...
return self.parent.frame(future=future)

I've already opened another PR to fix the node.statement calls #1317.

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

is a changelog entry necessary?

I think so, could you add a notice about the deprecation in the changelog @tushar-deepsource , please ? :)

@tusharsadhwani
Copy link
Contributor

sure

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas 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 ! This is blocked by the release of 2.9.1, we'll merge as soon as 2.9.1 is out :)

@Pierre-Sassoulas Pierre-Sassoulas added the Blocked 🚧 A PR or issue blocked by another PR or issue label Dec 28, 2021
@DanielNoord
Copy link
Collaborator

DanielNoord commented Dec 28, 2021

@Pierre-Sassoulas This should go in 2.9.1 as it is technically a bug fix πŸ˜‰

@Pierre-Sassoulas Pierre-Sassoulas removed the Blocked 🚧 A PR or issue blocked by another PR or issue label Dec 28, 2021
@Pierre-Sassoulas Pierre-Sassoulas modified the milestones: 2.10.0, 2.9.1 Dec 28, 2021
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas 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 the comment @DanielNoord :) Could you move the changelog to 2.9.1 @tushar-deepsource ? I can't suggest/apply it in interface.

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

πŸ‘Œ

@cdce8p
Copy link
Member

cdce8p commented Dec 29, 2021

I think we could merge this one as is. The test errors are unrelated to the changes made here.

@Pierre-Sassoulas Pierre-Sassoulas merged commit 12e2e8c into pylint-dev:main Dec 29, 2021
@tushar-deepsource tushar-deepsource deleted the tushar-deepsource-patch-1 branch January 4, 2022 15:21
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.

5 participants