-
-
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
base: main
Are you sure you want to change the base?
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10514 +/- ##
==========================================
- Coverage 95.85% 95.85% -0.01%
==========================================
Files 177 177
Lines 19198 19289 +91
==========================================
+ Hits 18402 18489 +87
- Misses 796 800 +4
🚀 New features to boost your workflow:
|
🤖 According to the primer, this change has no effect on the checked open source code. 🤖🎉 This comment was generated for commit 1996d57 |
@@ -37,13 +37,13 @@ def visit_match(self, node: nodes.Match) -> None: | |||
""" | |||
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
.
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.
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):
...
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?
# TODO: remove once match false-positives are fixed, probably in v1.18 | ||
# https://github.com/python/mypy/pull/19708 | ||
"has-type", |
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.
Mypy errors if we don't disable has-type
:
pylint/extensions/private_import.py:199: error: Cannot determine type of "n" [has-type]
pylint/extensions/private_import.py:200: error: Cannot determine type of "n" [has-type]
pylint/extensions/private_import.py:201: error: Cannot determine type of "n" [has-type]
pylint/extensions/private_import.py:201: note: Error code "has-type" not covered by "type: ignore" comment
pylint/extensions/private_import.py:204: error: Cannot determine type of "s" [has-type]
pylint/extensions/private_import.py:207: error: Cannot determine type of "v" [has-type]
pylint/extensions/private_import.py:213: error: Cannot determine type of "e" [has-type]
pylint/checkers/match_statements_checker.py:46: error: Cannot determine type of "n" [has-type]
Basically every time we do a variable binding inside a match case and use set variable.
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. |
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.