-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Use match statement in checkers (1) #10514
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
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10514 +/- ##
=======================================
Coverage 95.84% 95.84%
=======================================
Files 177 177
Lines 19284 19288 +4
=======================================
+ Hits 18483 18487 +4
Misses 801 801
🚀 New features to boost your workflow:
|
This comment has been minimized.
This comment has been minimized.
for idx, case in enumerate(node.cases): | ||
match case.pattern: | ||
case nodes.MatchAs(pattern=None, name=nodes.AssignName()) if ( | ||
case nodes.MatchAs(pattern=None, name=nodes.AssignName(name=n)) if ( |
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 we bind variables, what should the variable name be? A short-form like n
here or better name
? Maybe both is also an option?
Not sure where I picked this up from but using short-forms has somewhat grown on me. It's especially useful if we bind multiple variables in one case
. Say nodes.Subscript(slice=s, value=v)
. Furthermore it might help reduce the ambiguity if the keyword name is used for the variable name as well, e.g. name=name
. It also reminds me a bit of indices in loops with also just use i
, j
, so it might feel natural to look for the name in the match case.
Then again just name
might still be better in the case body.
--
Another factor could be if it's just a generic name or a descriptive one. As example, one of the other example in this PR uses next_if_node
.
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 personally prefer more descriptive names, especially since match ... case
can become quite long and I quickly lose context in the Github review view.
But if others prefer shorthand, that is also fine. I don't feel strongly.
"bare-name-capture-pattern", | ||
node=case.pattern, | ||
args=case.pattern.name.name, | ||
args=n, |
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.
Even though case.pattern.name.name
is correctly narrowed by the match case, both pyright and mypy have problems inferring the type correctly. This can be resolved / worked around if the name is bound in the match case.
Note: Mypy will always infer it as Any
since astroid is untyped, but even it it wasn't wouldn't be able to infer case .pattern.name.name
probably at the moment.
pylint/extensions/code_style.py
Outdated
if len(node.orelse) == 1 and isinstance(node.orelse[0], nodes.If): | ||
# elif block | ||
next_if_node = node.orelse[0] | ||
elif isinstance(next_sibling, nodes.If): | ||
# separate if block | ||
next_if_node = next_sibling | ||
match (node.orelse, next_sibling): | ||
case [[nodes.If() as next_if_node], _]: | ||
# elif block | ||
pass | ||
case [_, nodes.If() as next_if_node]: | ||
# separate if block | ||
pass |
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.
Not sure about this one. It works 🤷🏻♂️ There are a few things to note though.
Only variable binding - no body
So far I've seen quite a few cases where it might make sense to "abuse" match to just do the variable binding. I considered using ...
as body to make it more obvious that's what's happing but that doesn't look good / feels wrong. Maybe just pass
is still the best option.
Performance
For the match to work here, I matched each case against a constructed sequence. The plain if-elif is probably a bit faster though I haven't profiled it yet. Ideally something like a match expression would be added to Python at some point. Something like
if node.orelse match [nodes.If() as next_if_node]:
pass
elif isinstance(next_sibling, nodes.If):
...
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.
Yeah I'm a bit on the fence on this one as well. The original code isn't much better, but the explicit matching on a tuple only to then disregard 50% of the tuple in both cases feels a bit "meh"... But I can't come up with a better suggestion, so fine to merge for me.
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.
Reverted this one. No need to force it with the match statement. If still works fine.
pylint/extensions/private_import.py
Outdated
case nodes.Subscript(slice=s, value=v): | ||
# slice is the next nested type | ||
self._populate_type_annotations_annotation(s, all_used_type_annotations) | ||
# value is the current type name: could be a Name or Attribute | ||
return self._populate_type_annotations_annotation( | ||
v, all_used_type_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.
Note the use of s
and v
here as I mentioned earlier. Would replacing s
with slice
/ v
with value
be better?
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.
For me it would!
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.
Replaced most variable names with their long form. Incidentally not s
since slice
is also a builtin name.
Update
It might even make more sense to just continue using node.slice
and node.value
here. Though I haven't tested it yet, it could be a small performance improvement as we can skip hasattr
checks which are implicit in the match statement.
Love it, I like the elegance of match and it's proper in pylint with all the isinstance check we do all the time. I'm not sure if we should upgrade everything though. This kind of changes could tell us if our tests are actually covering or if we're just protected by years of live testing on real code bases and by not touching some part of the code without a good reason. Could be a lot of hassle if the latter is true and we go all in. Regarding the standard I don't think we're at the step of making rules yet. Or at least I'm not : I'm not using match a lot. Seems like for a generic node |
Me too! I've used match once or twice before in the custom pylint plugin for HomeAssistant, so I knew it might work but I was honestly surprised at how good it actually works.
True. So far I'd say the functional tests have caught most errors during conversion and helped a lot. There is always the risk that we introduce a regression though. The biggest issue I see though is that it will probably make backports much more difficult.
Works for me. Just wanted to check what you guys think before doing more changes. |
for idx, case in enumerate(node.cases): | ||
match case.pattern: | ||
case nodes.MatchAs(pattern=None, name=nodes.AssignName()) if ( | ||
case nodes.MatchAs(pattern=None, name=nodes.AssignName(name=n)) if ( |
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 personally prefer more descriptive names, especially since match ... case
can become quite long and I quickly lose context in the Github review view.
But if others prefer shorthand, that is also fine. I don't feel strongly.
pylint/extensions/code_style.py
Outdated
if len(node.orelse) == 1 and isinstance(node.orelse[0], nodes.If): | ||
# elif block | ||
next_if_node = node.orelse[0] | ||
elif isinstance(next_sibling, nodes.If): | ||
# separate if block | ||
next_if_node = next_sibling | ||
match (node.orelse, next_sibling): | ||
case [[nodes.If() as next_if_node], _]: | ||
# elif block | ||
pass | ||
case [_, nodes.If() as next_if_node]: | ||
# separate if block | ||
pass |
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.
Yeah I'm a bit on the fence on this one as well. The original code isn't much better, but the explicit matching on a tuple only to then disregard 50% of the tuple in both cases feels a bit "meh"... But I can't come up with a better suggestion, so fine to merge for me.
pylint/extensions/private_import.py
Outdated
case nodes.Subscript(slice=s, value=v): | ||
# slice is the next nested type | ||
self._populate_type_annotations_annotation(s, all_used_type_annotations) | ||
# value is the current type name: could be a Name or Attribute | ||
return self._populate_type_annotations_annotation( | ||
v, all_used_type_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.
For me it would!
1996d57
to
ffed4b5
Compare
This comment has been minimized.
This comment has been minimized.
I think this is ready now. @DanielNoord Mostly adopted your suggestions. Left some comments above. -- |
🤖 According to the primer, this change has no effect on the checked open source code. 🤖🎉 This comment was generated for commit b3d1654 |
@Pierre-Sassoulas Anything to add here? Otherwise I'd like to start merging the PRs with this one to avoid conflicts. |
After we added the first match statement in #10424 (comment), I checked a few other files to see which checks can be converted. There are actually quite a lot. Before we go ahead with it though, I think we should decide on some design choices and good practices first. I'll create comments for the specific sections. Feel free to add new ones as well in case I missed anything.
There are also mypy issues as astroid isn't fully typed yet. Opened PRs for it upstream already but those will be included in
v1.18
at the earliest. Until then it might be best to just disable the offending error code(s) or add type ignores.