-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[pylint] add fix safety section (PLW3301)
#17878
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
[pylint] add fix safety section (PLW3301)
#17878
Conversation
|
| /// ## Fix safety | ||
| /// This fix is always unsafe. Flattening nested `min()` or `max()` may change | ||
| /// code behavior when the inner call is the outer call’s only argument and the | ||
| /// inner call has multiple arguments. | ||
| /// ```python | ||
| /// print(min(min([2, 3], [4, 1]))) # before: 2 | ||
| /// print(min([2, 3], [4, 1])) # after: [2, 3] | ||
| /// ``` | ||
| /// |
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.
This isn't really a fix safety issue, this is a bug with an open issue that needs to be fixed.
I went through the git blame on this one, and I'm not really sure it actually needs to be unsafe. It was first converted from Fix::unspecified to Fix::suggested in #5251, which became Fix::sometimes_applies, which became Fix::unsafe_edit. I didn't see any specific discussion of the fix being unsafe, so I think it may have just been the more conservative option.
All that to say, it's not clear to me why this rule would need to be unsafe, at least from its description and development history. We even check for comment ranges and avoid a fix entirely if comments are present, so let's hold off on this for now.
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.
The min fix is unsafe when __lt__ is not commutative. The max fix is unsafe when __gt__ is not commutative.
$ cat >plw3301.py <<'# EOF'
print(min(2.0, min(float("nan"), 1.0)))
print(max(1.0, max(float("nan"), 2.0)))
# EOF
$ python plw3301.py
2.0
1.0
$ ruff check --isolated --select PLW3301 plw3301.py --unsafe-fixes --fix
Found 2 errors (2 fixed, 0 remaining).
$ python plw3301.py
1.0
2.0There 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.
@ntBre @dscorbett thanks so much for the input! I've updated the docs, though I'm not exactly sure, referencing the above case, is this rule always or sometimes unsafe? In general, what's makes a rule always/sometimes unsafe?
thanks a lot.
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.
Ah great catch as always, @dscorbett! I was hoping you might see this one.
I may have mentioned this on another PR, but the way I check for safety is to search the file for safe_edit. If you find cases of Fix::safe_edit and Fix::unsafe_edit, then the rule is sometimes safe. If you only find Fix::unsafe_edit, as I believe is the case here, the rule is always unsafe. You'll have to be slightly more careful with your search range in files with multiple rules, but the general strategy still applies.
ntBre
left a comment
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.
Thanks, this is looking good.
| /// This fix is always unsafe. The `min` fix is unsafe when `__lt__` is not commutative, | ||
| /// while the `max` fix is unsafe when `__gt__` is not commutative. |
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'd probably phrase this more like
This fix is always unsafe because it can change the program's behavior in cases where the underlying dunder methods (
__lt__forminand__gt__formax) are not commutative, as is the case for floats, for example:
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.
“Commutative” is the wrong word: I meant “associative”. Sorry for the confusion. Actually, I’m not sure associativity is precisely what is necessary and sufficient here. It’s probably best to avoid the technical jargon unless someone proves it’s correct.
|
@nefrob @dscorbett thanks for the reviews! I’ve adjusted the wording slightly based on my understanding—hope it’s clearer and aligns with the intent. Let me know if there’s any part that could be further improved! |
| /// This fix is always unsafe and may change the program's behavior when expanding | ||
| /// `min()` or `max()` calls. The underlying dunder methods (`__lt__` for `min` and | ||
| /// `__gt__` for `max`) are not associative, particularly when dealing with special | ||
| /// floating-point values like `NaN` (Not a Number). |
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.
| /// This fix is always unsafe and may change the program's behavior when expanding | |
| /// `min()` or `max()` calls. The underlying dunder methods (`__lt__` for `min` and | |
| /// `__gt__` for `max`) are not associative, particularly when dealing with special | |
| /// floating-point values like `NaN` (Not a Number). | |
| /// This fix is always unsafe and may change the program's behavior for | |
| /// types without full equivalence relations, such as float comparisons involving `NaN`. |
I think associativity is the correct property, but this is another possibility that I borrowed from the Rust PartialEq docs. I think we could also phrase it as "types without a total ordering" or something. Maybe associative is close enough to stick with that 😅
ntBre
left a comment
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.
Looks good, thanks! I just pushed one small change putting the section back at the end.
…eepish * origin/main: [ty] Induct into instances and subclasses when finding and applying generics (#18052) [ty] Allow classes to inherit from `type[Any]` or `type[Unknown]` (#18060) [ty] Allow a class to inherit from an intersection if the intersection contains a dynamic type and the intersection is not disjoint from `type` (#18055) [ty] Narrowing for `hasattr()` (#18053) Update reference documentation for `--python-version` (#18056) [`flake8-bugbear`] Ignore `B028` if `skip_file_prefixes` is present (#18047) [`airflow`] Apply try-catch guard to all AIR3 rules (`AIR3`) (#17887) [`pylint`] add fix safety section (`PLW3301`) (#17878) Update `--python` to accept paths to executables in virtual environments (#17954) [`pylint`] add fix safety section (`PLE4703`) (#17824) [`ruff`] Implement a recursive check for `RUF060` (#17976) [`flake8-use-pathlib`] `PTH*` suppress diagnostic for all `os.*` functions that have the `dir_fd` parameter (#17968) [`refurb`] Mark autofix as safe only for number literals in `FURB116` (#17692) [`flake8-simplify`] Fix `SIM905` autofix for `rsplit` creating a reversed list literal (#18045) Avoid initializing progress bars early (#18049)
…eep-dish * origin/main: [ty] Infer parameter specializations of generic aliases (#18021) [ty] Understand homogeneous tuple annotations (#17998) [ty] Induct into instances and subclasses when finding and applying generics (#18052) [ty] Allow classes to inherit from `type[Any]` or `type[Unknown]` (#18060) [ty] Allow a class to inherit from an intersection if the intersection contains a dynamic type and the intersection is not disjoint from `type` (#18055) [ty] Narrowing for `hasattr()` (#18053) Update reference documentation for `--python-version` (#18056) [`flake8-bugbear`] Ignore `B028` if `skip_file_prefixes` is present (#18047) [`airflow`] Apply try-catch guard to all AIR3 rules (`AIR3`) (#17887) [`pylint`] add fix safety section (`PLW3301`) (#17878) Update `--python` to accept paths to executables in virtual environments (#17954) [`pylint`] add fix safety section (`PLE4703`) (#17824) [`ruff`] Implement a recursive check for `RUF060` (#17976) [`flake8-use-pathlib`] `PTH*` suppress diagnostic for all `os.*` functions that have the `dir_fd` parameter (#17968) [`refurb`] Mark autofix as safe only for number literals in `FURB116` (#17692) [`flake8-simplify`] Fix `SIM905` autofix for `rsplit` creating a reversed list literal (#18045) Avoid initializing progress bars early (#18049)
parent: astral-sh#15584 issue: astral-sh#16163 --------- Co-authored-by: Brent Westbrook <[email protected]>
parent: #15584
issue: #16163