-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[flake8-use-pathlib] Mark fixes unsafe for return type changes (PTH104, PTH105, PTH109, PTH115)
#21440
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
|
| code | total | + violation | - violation | + fix | - fix |
|---|---|---|---|---|---|
| PTH109 | 32 | 0 | 0 | 0 | 32 |
| PTH115 | 6 | 0 | 0 | 0 | 6 |
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.
I made a couple of suggestions for simplification, but we also need to verify that this change is only applied to the correct rules. I flagged a few cases inline where rules not mentioned in #21431 were changed unnecessarily, but I didn't check them exhaustively.
| let applicability = if checker.comment_ranges().intersects(range) { | ||
| Applicability::Unsafe | ||
| } else { | ||
| } else if is_top_level_expression_call(checker, call) { | ||
| // Safe when the call is a top-level expression (return value not used) | ||
| Applicability::Safe | ||
| } else { | ||
| // Unsafe because the return type changes (str/bytes -> Path) | ||
| Applicability::Unsafe | ||
| }; |
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 think I would slightly prefer something like:
| let applicability = if checker.comment_ranges().intersects(range) { | |
| Applicability::Unsafe | |
| } else { | |
| } else if is_top_level_expression_call(checker, call) { | |
| // Safe when the call is a top-level expression (return value not used) | |
| Applicability::Safe | |
| } else { | |
| // Unsafe because the return type changes (str/bytes -> Path) | |
| Applicability::Unsafe | |
| }; | |
| // Unsafe when the fix would delete comments or change a used return value | |
| let applicability = if checker.comment_ranges().intersects(range) | |
| || !is_top_level_expression_call(checker, call) { | |
| Applicability::Unsafe | |
| } else { | |
| Applicability::Safe | |
| }; |
| /// Additionally, the fix is marked as unsafe because `os.getcwd()` and `os.getcwdb()` return `str` or `bytes`, | ||
| /// while `Path.cwd()` returns a `Path` object. This change in return type can break code that uses the return value. | ||
| /// The fix is safe when the function call is a top-level expression in its statement (i.e., the return value is not used). |
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 think we could simplify this slightly to something like:
| /// Additionally, the fix is marked as unsafe because `os.getcwd()` and `os.getcwdb()` return `str` or `bytes`, | |
| /// while `Path.cwd()` returns a `Path` object. This change in return type can break code that uses the return value. | |
| /// The fix is safe when the function call is a top-level expression in its statement (i.e., the return value is not used). | |
| /// Additionally, the fix is marked as unsafe when the return value is used because the type changes | |
| /// from `str` or `bytes` to a `Path` object. |
I think something similar would work for the other documentation updates too.
| 21 + bbbb = pathlib.Path(p).is_file() | ||
| 22 | bbbbb = os.path.islink(p) | ||
| 23 | os.readlink(p) | ||
| 24 | os.stat(p) | ||
| note: This is an unsafe fix and may change runtime behavior |
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 doesn't look right. Should isfile be affected by these changes? I think both versions return a bool.
| - bbbbb = os.path.islink(p) | ||
| 22 + bbbbb = pathlib.Path(p).is_symlink() | ||
| 23 | os.readlink(p) | ||
| 24 | os.stat(p) | ||
| 25 | os.path.isabs(p) | ||
| note: This is an unsafe fix and may change runtime behavior |
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 think this one also shouldn't change.
| - b = foo_p.exists(p) | ||
| 18 + b = pathlib.Path(p).exists() | ||
| 19 | bb = foo_p.expanduser(p) | ||
| 20 | bbb = foo_p.isdir(p) | ||
| 21 | bbbb = foo_p.isfile(p) | ||
| note: This is an unsafe fix and may change runtime behavior |
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.
Another one that shouldn't change, from what I can tell.
| if let Stmt::Expr(ast::StmtExpr { | ||
| value: child, | ||
| range: _, | ||
| node_index: _, | ||
| }) = checker.semantic().current_statement() | ||
| { | ||
| // Check if the call is the same expression as the statement's value | ||
| // We compare by checking if the call's range is contained within the child's range | ||
| // and if they're the same expression node | ||
| if let Expr::Call(call_expr) = child.as_ref() { | ||
| return call_expr.range() == call.range(); | ||
| } | ||
| } | ||
| false |
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.
Assuming that call is the SemanticModel::current_expression, I think this function could just be:
| if let Stmt::Expr(ast::StmtExpr { | |
| value: child, | |
| range: _, | |
| node_index: _, | |
| }) = checker.semantic().current_statement() | |
| { | |
| // Check if the call is the same expression as the statement's value | |
| // We compare by checking if the call's range is contained within the child's range | |
| // and if they're the same expression node | |
| if let Expr::Call(call_expr) = child.as_ref() { | |
| return call_expr.range() == call.range(); | |
| } | |
| } | |
| false | |
| checker.semantic().current_expression_parent().is_none() |
|
Thanks for the feedback! I had thought that the same concepts applied to a few other rules, but it appears not after taking a closer look. |
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 better, but I had a few more suggestions.
I tried to flag all of the places the docs should say "str or bytes," but I started using typing.AnyStr as a shorthand since that's what ty shows. It's technically a type variable that depends on the input, but I think it's fine just to say "str or bytes" in each of those cases.
| let determined_applicability = if checker.comment_ranges().intersects(range) { | ||
| Applicability::Unsafe | ||
| } else if applicability.is_none() { | ||
| // When applicability is None, we need to determine it based on return type changes | ||
| if is_top_level_expression_call(checker, call) { | ||
| // Safe when the call is a top-level expression (return value not used) | ||
| Applicability::Safe | ||
| } else { | ||
| // Unsafe because the return type changes (str/bytes -> Path or None -> Path) | ||
| Applicability::Unsafe | ||
| } |
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 don't think we should do this check here. I checked all of the references for check_os_pathlib_single_arg_calls, and it's only used by PTH115, of the rules we're trying to fix in this PR.
I think we should just do the is_top_level_expression_call check in PTH115 itself.
| } else if is_top_level_expression_call(checker, call) { | ||
| // Safe when the call is a top-level expression (return value not used) | ||
| Applicability::Safe | ||
| } else { | ||
| // Unsafe because the return type changes (None -> Path) | ||
| Applicability::Unsafe |
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.
As above, this helper also affects rules outside of PTH{104,105,109,115}. We should narrow the check to the affected rules and pass that applicability into the helper.
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.
Just a note on this - to have applicability passed through the helper, I had to add #[allow(clippy::too_many_arguments)].
Let me know if that's fine or if you'd rather have it refactored.
crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_readlink.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_dirname.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_abspath.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_expanduser.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isfile.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_islink.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_exists.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_readlink.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_replace.rs
Outdated
Show resolved
Hide resolved
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! I just fixed one more round of tiny nits. Once the ecosystem check runs, I'll merge.
* origin/main: (67 commits) Move `Token`, `TokenKind` and `Tokens` to `ruff-python-ast` (#21760) [ty] Don't confuse multiple occurrences of `typing.Self` when binding bound methods (#21754) Use our org-wide Renovate preset (#21759) Delete `my-script.py` (#21751) [ty] Move `all_members`, and related types/routines, out of `ide_support.rs` (#21695) [ty] Fix find-references for import aliases (#21736) [ty] add tests for workspaces (#21741) [ty] Stop testing the (brittle) constraint set display implementation (#21743) [ty] Use generator over list comprehension to avoid cast (#21748) [ty] Add a diagnostic for prohibited `NamedTuple` attribute overrides (#21717) [ty] Fix subtyping with `type[T]` and unions (#21740) Use `npm ci --ignore-scripts` everywhere (#21742) [`flake8-simplify`] Fix truthiness assumption for non-iterable arguments in tuple/list/set calls (`SIM222`, `SIM223`) (#21479) [`flake8-use-pathlib`] Mark fixes unsafe for return type changes (`PTH104`, `PTH105`, `PTH109`, `PTH115`) (#21440) [ty] Fix auto-import code action to handle pre-existing import Enable PEP 740 attestations when publishing to PyPI (#21735) [ty] Fix find references for type defined in stub (#21732) Use OIDC instead of codspeed token (#21719) [ty] Exclude `typing_extensions` from completions unless it's really available [ty] Fix false positives for `class F(Generic[*Ts]): ...` (#21723) ...
Summary
Marks fixes as unsafe when they change return types (
None→Path,str/bytes→Path,str→Path), except when the call is a top-level expression.Fixes #21431.
Problem
Fixes for
os.rename,os.replace,os.getcwd/os.getcwdb, andos.readlinkwere marked safe despite changing return types, which can break code that uses the return value.Approach
Added
is_top_level_expression_callhelper to detect when a call is a top-level expression (return value unused). Updatedcheck_os_pathlib_two_arg_callsandcheck_os_pathlib_single_arg_callsto mark fixes as unsafe unless the call is a top-level expression. Updated PTH109 to use the helper for applicability determination.Test Plan
Updated snapshots for
preview_full_name.py,preview_import_as.py,preview_import_from.py, andpreview_import_from_as.pyto reflect unsafe markers.