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

Handle inference of ComprehensionScope better #1475

Closed
wants to merge 11 commits into from

Conversation

DanielNoord
Copy link
Collaborator

@DanielNoord DanielNoord commented Mar 15, 2022

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

This makes it so that ComprehensionScopes are actually inferred. This requires some changes in pylint as we rely on the fact that these don't get inferred currently.
I have made a related PR to pylint which shows what these changes would be (pylint-dev/pylint#5923). I think it will be useful to review this simultaneously, as they show how this impacts astroid's output. That PR has now been merged.

One problem I couldn't overcome: this will break pre-commit for the project that merges this first. The change in behaviour changes various checks and we need to resolve an order in which these get merged. Pre-commit will be fixed once we have pylint 2.13 available here.

Let me know what you guys think!
We might want to add some additional tests.

Type of Changes

Type
🐛 Bug fix
✨ New feature

Related Issue

Closes #135, closes #1404

@DanielNoord DanielNoord added Enhancement ✨ Improvement to a component pylint-tested PRs that don't cause major regressions with pylint labels Mar 15, 2022
generators: List["nodes.Comprehension"]
"""The generators that are looped through."""

def qname(self):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is another instance of #278. We could also change qname() on LocalsDictNodeNG not to use name() if it doesn't exist.

I don't know what is better.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
def qname(self):
def qname(self) -> str:

@coveralls
Copy link

coveralls commented Mar 25, 2022

Pull Request Test Coverage Report for Build 2268818330

  • 13 of 21 (61.9%) changed or added relevant lines in 3 files are covered.
  • 3 unchanged lines in 1 file lost coverage.
  • Overall coverage decreased (-0.09%) to 91.541%

Changes Missing Coverage Covered Lines Changed/Added Lines %
astroid/helpers.py 2 4 50.0%
astroid/nodes/scoped_nodes/mixin.py 7 9 77.78%
astroid/nodes/scoped_nodes/scoped_nodes.py 4 8 50.0%
Files with Coverage Reduction New Missed Lines %
astroid/brain/brain_builtin_inference.py 3 89.88%
Totals Coverage Status
Change from base Build 2268781407: -0.09%
Covered Lines: 9144
Relevant Lines: 9989

💛 - Coveralls

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.

I have some questions, but this looks pretty good !

astroid/nodes/scoped_nodes/mixin.py Show resolved Hide resolved
astroid/nodes/scoped_nodes/scoped_nodes.py Outdated Show resolved Hide resolved
astroid/nodes/scoped_nodes/scoped_nodes.py Show resolved Hide resolved
@Pierre-Sassoulas
Copy link
Member

Forgot to point out that the coverage decreased by more than 0.05% and coverall did not fail. I must have set it up wrong.

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.

The coverage decreased a lot, is this coverall acting up again ?

@DanielNoord
Copy link
Collaborator Author

Hmm I'll have a good look.

I think this might actually fix some StopIteration except handlers as we now "can" infer these Scope nodes. See https://coveralls.io/builds/47720462/source?filename=astroid%2Fbrain%2Fbrain_builtin_inference.py#L207.

Part of it is also the merge of main which @jacobtylerwalls mentioned in another PR today. That seems to give coveralls issues.

@DanielNoord
Copy link
Collaborator Author

@Pierre-Sassoulas Can we merge this and backport this to the upcoming patch release? Would be good to fix the coverage in pylint before 2.14.

@Pierre-Sassoulas
Copy link
Member

Can we merge this and backport this to the upcoming patch release?

Sure, but do you mean astroid patch release or pylint's patch release, or both ?

@DanielNoord
Copy link
Collaborator Author

Can we merge this and backport this to the upcoming patch release?

Sure, but do you mean astroid patch release or pylint's patch release, or both ?

astroid patch!

Comment on lines +180 to +182
def qname(self) -> str:
"""Get the 'qualified' name of the node."""
return self.pytype()
Copy link
Member

Choose a reason for hiding this comment

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

@cdce8p do you have thoughts about this? In the past I believe you were a vote against, see #1284 (review).

@DanielNoord
Copy link
Collaborator Author

Moving this to 2.12 as we can realise the patch version without this.

@DanielNoord DanielNoord requested a review from cdce8p May 4, 2022 09:11
Comment on lines +183 to +186
def _infer(
self: _T, context: Optional["InferenceContext"] = None, **kwargs: Any
) -> Iterator[_T]:
yield self
Copy link
Member

Choose a reason for hiding this comment

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

What do we get by returning self, i.e. the AST node, here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do you mean return self instead of yield self?

Comment on lines 2644 to +2649
dict_comp = next(module["dict_comp"].infer())
self.assertEqual(dict_comp, util.Uninferable)
assert isinstance(dict_comp, nodes.DictComp)
set_comp = next(module["set_comp"].infer())
self.assertEqual(set_comp, util.Uninferable)
assert isinstance(set_comp, nodes.SetComp)
list_comp = next(module["list_comp"].infer())
self.assertEqual(list_comp, util.Uninferable)
assert isinstance(list_comp, nodes.ListComp)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe I'm misunderstanding the concept of inference here. If so, please correct me.

However, the way I see it inference means we look at the AST node and tell the caller what the Python result will be without actually executing the code. To give an example

lst = [x for x in range(3)]

The inference result for lst shouldn't be the list comprehension itself. Instead it should be the AST node for a list with

[0, 1, 2]

Same for generator expressions, dict and set comprehensions.

That is much much harder to do which is likely we it wasn't done already.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, yeah I didn't think of it like that. That would decouple the value of lst from the actual ast tree though.

For example, we would then have a ListComp node in nodes.Module.body but a List node for lst? If we create that List when calling _infer on ListComp what would then be the lineno etc.?

Wouldn't it be better to say that lst is a ListComp and let downstream users decide if they want to handle ListComp and List similarly? Like we often do with FunctionDef and AsyncFunctionDef.

Copy link
Member

Choose a reason for hiding this comment

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

That would decouple the value of lst from the actual ast tree though.

Yeah, it's an inference result after all.

If we create that List when calling _infer on ListComp what would then be the lineno etc.?

None? Both lineno and col_offset attributes are typed as optional.
Or the same one as the original ListComp.

Wouldn't it be better to say that lst is a ListComp and let downstream users decide if they want to handle ListComp and List similarly? Like we often do with FunctionDef and AsyncFunctionDef.

The downstream user still has full control over it. If he once the "raw" data, he can just use the node itself. No need to call infer for that. Regarding FunctionDef, the infer method can actually return a Property if the function is decorated. Similarly, inferring a DictUnpack will return a single dict or a comparison just a bool constant

{1: 'A', **{2: 'B'}}
True == True

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I guess we should probably handle this comprehension per comprehension then. Instead of one larger PR.

Copy link
Member

Choose a reason for hiding this comment

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

I guess we should probably handle this comprehension per comprehension then. Instead of one larger PR.

Yeah. There are multiple parts to be aware of: evaluating the iterator, matching the loop var(s) with optional sequence unpacking, checking any if statements and doing late assignments via :=, evaluating the loop expression. Likely I'm forgetting something.

[a + c for a, b in func() if (c := b * 2)]

If you, or someone else wants to go down that route, it's probably best to start with a fairly limited set constraints and only one comprehension type. Once that is down extending it should be fairly easy in comparison.

@cdce8p
Copy link
Member

cdce8p commented May 10, 2022

Going to close this PR since we would need a different approach for inference.
See #1475 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement ✨ Improvement to a component pylint-tested PRs that don't cause major regressions with pylint
Projects
None yet
Development

Successfully merging this pull request may close these issues.

dict comprehension is not inferred as dict Understand list/set/dict comprehension
5 participants