Compare commits

..

49 Commits

Author SHA1 Message Date
Brent Westbrook
5bc81f26c8 Bump 0.12.3 (#19279) 2025-07-11 09:07:50 -04:00
Brent Westbrook
6908e2682f Filter ruff_linter::VERSION out of SARIF output tests (#19280)
Summary
--

Fixes the test failures in #19279. This is the same variable used to
construct the SARIF output:


350d563c88/crates/ruff_linter/src/message/sarif.rs (L39-L44)

Test Plan
--

Existing tests with the modified filter
2025-07-11 08:55:51 -04:00
Dhruv Manilawala
25c4295564 [ty] Avoid stale diagnostics for open files diagnostic mode (#19273)
## Summary

This PR fixes a bug where in `openFilesOnly` diagnostic mode, VS Code
wouldn't clean up the diagnostics even though the server asked it to by
sending an empty publish diagnostics.

This is not the long-term solution but a quick fix. Ideally, the server
would dynamically register for workspace diagnostics but that requires
listening for `didChangeConfiguration` notification which I'm going to
be working on with https://github.com/astral-sh/ty/issues/82.

## Test Plan

### Before

This uses the latest stable version of ty.


https://github.com/user-attachments/assets/0cc6c513-ccad-4955-a1b6-a0ee242119d6

### After

This uses the debug build of ty from this PR.


https://github.com/user-attachments/assets/e539d569-d852-46a9-bbfc-d54375127c62
2025-07-11 16:29:16 +05:30
Micha Reiser
426fa4bb12 [ty] Add signature help provider to playground (#19276) 2025-07-11 09:58:14 +02:00
UnboundVariable
b0b65c24ff [ty] Initial implementation of signature help provider (#19194)
This PR includes:
* Implemented core signature help logic
* Added new docstring method on Definition that returns a docstring for
function and class definitions
* Modified the display code for Signature that allows a signature string
to be broken into text ranges that correspond to each parameter in the
signature
* Augmented Signature struct so it can track the Definition for a
signature when available; this allows us to find the docstring
associated with the signature
* Added utility functions for parsing parameter documentation from three
popular docstring formats (Google, NumPy and reST)
* Implemented tests for all of the above

"Signature help" is displayed by an editor when you are typing a
function call expression. It is typically triggered when you type an
open parenthesis. The language server provides information about the
target function's signature (or multiple signatures), documentation, and
parameters.

Here is how this appears:


![image](https://github.com/user-attachments/assets/40dce616-ed74-4810-be62-42a5b5e4b334)

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-07-10 19:32:00 -07:00
Brent Westbrook
08bc6d2589 Add simple integration tests for all output formats (#19265)
Summary
--

I spun this off from #19133 to be sure to get an accurate baseline
before modifying any of the formats. I picked the code snippet to
include a lint diagnostic with a fix, one without a fix, and one syntax
error. I'm happy to expand it if there are any other kinds we want to
test.

I initially passed `CONTENT` on stdin, but I was a bit surprised to
notice that some of our output formats include an absolute path to the
file. I switched to a `TempDir` to use the `tempdir_filter`.

Test Plan
--

New CLI tests
2025-07-10 17:57:48 -04:00
Victor Hugo Gomes
f2ae12bab3 [flake8-return] Fix false-positive for variables used inside nested functions in RET504 (#18433)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
This PR is the same as #17656.

I accidentally deleted the branch of that PR, so I'm creating a new one.

Fixes #14052

## Test Plan

Add regression tests
<!-- How was it tested? -->
2025-07-10 16:10:22 -04:00
Zanie Blue
965f415212 [ty] Add a --quiet mode (#19233)
Adds a `--quiet` flag which silences diagnostic, warning logs, and
messages like "all checks passed" while retaining summary messages that
indicate problems, e.g., the number of diagnostics.

I'm a bit on the fence regarding filtering out warning logs, because it
can omit important details, e.g., the message that a fatal diagnostic
was encountered. Let's discuss that in
https://github.com/astral-sh/ruff/pull/19233#discussion_r2195408693

The implementation recycles the `Printer` abstraction used in uv, which
is intended to replace all direct usage of `std::io::stdout`. See
https://github.com/astral-sh/ruff/pull/19233#discussion_r2195140197

I ended up futzing with the progress bar more than I probably should
have to ensure it was also using the printer, but it doesn't seem like a
big deal. See
https://github.com/astral-sh/ruff/pull/19233#discussion_r2195330467

Closes https://github.com/astral-sh/ty/issues/772
2025-07-10 09:40:47 -05:00
frank
83b5bbf004 Treat form feed as valid whitespace before a line continuation (#19220)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-07-10 14:09:34 +00:00
Micha Reiser
87f6f08ef5 [ty] Make check_file a salsa query (#19255)
## Summary
We noticed that all files get reparsed when workspace diagnostics are
enabled.

I realised that this is because `check_file_impl` access the parsed
module but itself isn't a salsa query.
This pr makes `check_file_impl` a salsa query, so that we only access
the `parsed_module` when the file actually changed. I decided to remove
the salsa query from `check_types` because most functions it calls are
salsa queries itself and having both `check_types` and `check_file` as
salsa querise has the downside that we double cache the diagnostics.

## Test Plan

**Before**

```
2025-07-10 12:54:16.620766000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0c))}: File `/Users/micha/astral/test/yaml/yaml-stubs/__init__.pyi` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.621942000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c13))}: File `/Users/micha/astral/test/ignore2 2/nested-repository/main.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.622107000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c09))}: File `/Users/micha/astral/test/notebook.ipynb` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.622357000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c04))}: File `/Users/micha/astral/test/no-trailing.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.622634000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c02))}: File `/Users/micha/astral/test/simple.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.623056000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c07))}: File `/Users/micha/astral/test/open/more.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.623254000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c11))}: File `/Users/micha/astral/test/ignore-bug/backend/src/subdir/log/some_logging_lib.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.623450000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0f))}: File `/Users/micha/astral/test/yaml/tomllib/__init__.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.624599000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c05))}: File `/Users/micha/astral/test/create.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.624784000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c00))}: File `/Users/micha/astral/test/lib.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.624911000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0a))}: File `/Users/micha/astral/test/sub/test.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625032000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c12))}: File `/Users/micha/astral/test/ignore2/nested-repository/main.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625101000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c08))}: File `/Users/micha/astral/test/open/test.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625227000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c03))}: File `/Users/micha/astral/test/pseudocode_with_bom.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625353000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0b))}: File `/Users/micha/astral/test/yaml/yaml-stubs/loader.pyi` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625543000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c01))}: File `/Users/micha/astral/test/test_trailing.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625616000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0d))}: File `/Users/micha/astral/test/yaml/tomllib/_re.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625667000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c06))}: File `/Users/micha/astral/test/yaml/main.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.625779000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c10))}: File `/Users/micha/astral/test/yaml/tomllib/_types.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.627526000  WARN request{id=19 method="workspace/diagnostic"}:Project::check:check_file{file=file(Id(c0e))}: File `/Users/micha/astral/test/yaml/tomllib/_parser.py` was reparsed after being collected in the current Salsa revision
2025-07-10 12:54:16.627959000 DEBUG request{id=19 method="workspace/diagnostic"}:Project::check: Checking all files took 0.007s
```

Now, no more logs regarding reparsing
2025-07-10 18:46:56 +05:30
Alex Waygood
59114d0301 [ty] Consolidate submodule resolving code between types.rs and ide_support.rs (#19256) 2025-07-10 13:10:09 +00:00
Micha Reiser
492f5bf2aa [ty] Remove countme from salsa-structs (#19257) 2025-07-10 11:45:09 +00:00
Alex Waygood
934aaa23f3 [ty] Improve and document equivalence for module-literal types (#19243) 2025-07-10 09:11:10 +00:00
Alex Waygood
59aa869724 [ty] Optimize protocol subtyping by removing expensive and unnecessary equivalence check from the top of Type::has_relation_to() (#19230) 2025-07-10 09:42:27 +01:00
David Peter
edaffa6c4f [ty] Ecosystem analyzer: parallelize, fix race condition (#19252)
## Summary

Pulls in two fixes and a performance optimization:

- Fix a bug with the Markdown table formatting.
- Combine the two `analyze` commands into a single `diff` command. This
means we only need to set up the projects once, which is faster and also
avoids a race condition where projects could change between the two
`analyze` runs.
2025-07-10 10:25:24 +02:00
Micha Reiser
5fb2fb916b [ty] Add completion kind to playground (#19251) 2025-07-10 07:41:59 +00:00
David Peter
801f69a7b4 [ty] Deploy ecosystem diff to Cloudflare pages (#19234)
## Summary

Changes the ecosystem-analyzer workflow to deploy the diff to Cloudflare
pages and post a link in the PR. Also adds a summary statistics to that
PR comment.

## Test Plan

The comment below:
https://github.com/astral-sh/ruff/pull/19234#issuecomment-3053205937. I
previously had some dummy changes on this PR to see a non-zero diff. And
I didn't reapply the label after I reverted that change, such that it's
still visible for reviewers.
2025-07-10 09:03:42 +02:00
Micha Reiser
3926dd8424 [ty] Add semantic token provider to playground (#19232) 2025-07-10 07:50:28 +02:00
Faisal
563268ce53 [docs] add capital one to who's using ruff (#19248)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

Add Capital One to Who's Using Ruff (README)
Also thanks for the fantastic project!
2025-07-09 23:50:27 +00:00
Dan Parizher
221edcba5c [pyupgrade] Keyword arguments in super should suppress the UP008 fix (#19131)
## Summary

Fixes #19096
2025-07-09 15:13:22 -04:00
chiri
beb98dae7c [flake8-use-pathlib] Add autofixes for PTH100, PTH106, PTH107, PTH108, PTH110, PTH111, PTH112, PTH113, PTH114, PTH115, PTH117, PTH119, PTH120 (#19213)
## Summary

Part of #2331

## Test Plan

update snapshots for preview mode
2025-07-09 14:54:33 -04:00
InSync
05b1b788a0 [ty] Do not run mypy_primer.yaml when all changed files are Markdown files (#19244) 2025-07-09 19:40:43 +01:00
GiGaGon
a18f76158d [flake8-bandit] Make example error out-of-the-box (S412) (#19241)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of #18972

This PR makes [suspicious-httpoxy-import
(S412)](https://docs.astral.sh/ruff/rules/suspicious-httpoxy-import/#suspicious-httpoxy-import-s412)'s
example error out-of-the-box. Since the checked imports are classes
instead of modules, the example isn't valid. See #19009 for more details
```
PS ~>py -c "import wsgiref.handlers.CGIHandler"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import wsgiref.handlers.CGIHandler
ModuleNotFoundError: No module named 'wsgiref.handlers.CGIHandler'; 'wsgiref.handlers' is not a package
PS ~>py -c "from wsgiref.handlers import CGIHandler"
PS ~>
```

[Old example](https://play.ruff.rs/bf48c901-6a46-4795-ba1d-c6af79d5c96e)
```py
import wsgiref.handlers.CGIHandler
```

[New example](https://play.ruff.rs/1f0e1e60-1f0f-484a-9a17-2d0290a68f2a)
```py
from wsgiref.handlers import CGIHandler
```

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-07-09 14:25:27 -04:00
GiGaGon
8f400bb37a [pydoclint] Make example error out-of-the-box (DOC501) (#19218)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of #18972

This PR makes [docstring-missing-exception
(DOC501)](https://docs.astral.sh/ruff/rules/docstring-missing-exception/#docstring-missing-exception-doc501)'s
example error out-of-the-box. Since the exceptions in the function body
need to undergo name resolution to figure out if one of them is
`NotImplementedError`, `DOC501` won't lint if the raised name is not
defined. This could be considered a limitation, but should be fine since
`F821` already covers undefined names. I did discover a different edge
case, but it's not relevant to the example.

[Old example](https://play.ruff.rs/d213e87d-e5c7-49d8-a908-931f61f06055)
```py
def calculate_speed(distance: float, time: float) -> float:
    """Calculate speed as distance divided by time.

    Args:
        distance: Distance traveled.
        time: Time spent traveling.

    Returns:
        Speed as distance divided by time.
    """
    try:
        return distance / time
    except ZeroDivisionError as exc:
        raise FasterThanLightError from exc
```

[New example](https://play.ruff.rs/cb41e0b7-b950-4fa0-842d-cecab9c8e842)
```py
class FasterThanLightError(ArithmeticError): ...


def calculate_speed(distance: float, time: float) -> float:
    """Calculate speed as distance divided by time.

    Args:
        distance: Distance traveled.
        time: Time spent traveling.

    Returns:
        Speed as distance divided by time.
    """
    try:
        return distance / time
    except ZeroDivisionError as exc:
        raise FasterThanLightError from exc
```

The "Use instead" section was also updated similarly.

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-07-09 12:59:31 -04:00
Andrew Gallant
1eff0300d3 [ty] Add "kind" to completion suggestions
This makes use of the new `Type` field on `Completion` to figure out the
"kind" of a `Completion`.

The mapping here is perhaps a little suspect for some cases.

Closes astral-sh/ty#775
2025-07-09 12:03:56 -04:00
Andrew Gallant
fea84e8777 [ty] Add type information to all_members API
Since we generally need (so far) to get the type information of each
suggestion to figure out its boundness anyway, we might as well expose
it here. Completions want to use this information to enhance the
metadata on each suggestion for a more pleasant user experience.

For the most part, this was pretty straight-forward. The most exciting
part was in computing the types for instance attributes. I'm not 100%
sure it's correct or is the best way to do it.
2025-07-09 12:03:56 -04:00
Andrew Gallant
79fe538458 [ty] Expand API of all_members to return a struct
This commit doesn't change any behavior, but makes it so `all_members`
returns a `Vec<Member>` instead of `Vec<Name>`, where a `Member`
contains a `Name`. This gives us an expansion point to include other
data (such as the type of the `Name`).
2025-07-09 12:03:56 -04:00
David Peter
f7234cb474 [ty] Ecosystem analyzer PR comment workflow (#19237)
## Summary

Add PR comment workflow as a prerequisite for
https://github.com/astral-sh/ruff/pull/19234

## Test Plan

Not yet tested. Need to merge this first.
2025-07-09 18:02:05 +02:00
Micha Reiser
35a33f045e [ty] Merge ty_macros into ruff_macros (#19229) 2025-07-09 11:28:21 +00:00
Matthew Mckee
f32f7a3b48 [ty] Fix ClassLiteral.into_callable for dataclasses (#19192)
## Summary

Change `ClassLiteral.into_callable` to also look for `__init__` functions
of type `Type::Callable` (such as synthesized `__init__` functions of
dataclasses).

Fixes https://github.com/astral-sh/ty/issues/760

## Test Plan

Add subtype test

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-09 10:04:55 +02:00
David Peter
68106dd631 [ty] dataclasses.field support (#19140)
## Summary

Add an initial set of tests for `dataclasses.field`.
2025-07-09 09:18:08 +02:00
David Peter
ab3af924ef [ty] Fix panic for attribute expressions with empty value (#19069)
## Summary

closes https://github.com/astral-sh/ty/issues/738

## Test Plan

Added corpus test
2025-07-09 08:46:33 +02:00
Matthew Mckee
05139a323b [ty] Return CallableType from BoundMethodType.into_callable_type (#19193) 2025-07-08 20:33:43 +01:00
Dan Parizher
5eb5ec987d [flake8-bugbear] Support non-context-manager calls in B017 (#19063)
## Summary

Fixes #19050

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-07-08 15:04:55 -04:00
David Peter
1a099886ab [ty] Improved diagnostic for reassignments of Final symbols (#19214)
## Summary

Implement [this
suggestion](https://github.com/astral-sh/ruff/pull/19178#discussion_r2192658146)
by @AlexWaygood.


![image](https://github.com/user-attachments/assets/f183d691-ef6e-43a2-b005-3a32205bc408)
2025-07-08 20:29:07 +02:00
David Peter
a8f2c26143 [ty] Use full range for assignment definitions (#19211)
## Summary

Fix the `full_range` function for (annotated) assignment definition
kinds.

## Test Plan

Update snapshot tests
2025-07-08 19:51:09 +02:00
Junhson Jean-Baptiste
fda188953f [pylint] Update missing-maxsplit-arg docs and error to suggest proper usage (PLC0207) (#18949)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Fix #18383 by updating the documentation and error message to explain
that users should use `rsplit` in order to access the last element of
the result with `maxsplit=1`

## Test Plan

<!-- How was it tested? -->

Only documentation and an error message was changed. As such, snapshots
were updated to reflect the new error message. With this change, all
existing tests pass.
2025-07-08 12:53:23 -04:00
Ibraheem Ahmed
546f1b7b39 [ty] Add set -eu to mypy-primer script (#19212)
## Summary

So that the CI job fails if ty panics.
2025-07-08 12:16:09 -04:00
Alex Waygood
7533a0bfdb [ty] Upgrade mypy_primer (#19207) 2025-07-08 15:56:54 +01:00
Charlie Marsh
3ee3434187 Auto-generate environment variable references for ty (#19205)
## Summary

This PR mirrors the environment variable implementation we have in uv:
efc361223c/crates/uv-static/src/env_vars.rs (L6-L7).

See: https://github.com/astral-sh/ty/issues/773.
2025-07-08 10:48:31 -04:00
David Peter
149350bf39 [ty] Enforce typing.Final (#19178)
## Summary

Emit a diagnostic when a `Final`-qualified symbol is modified. This
first iteration only works for name targets. Tests with TODO comments
were added for attribute assignments as well.

related ticket: https://github.com/astral-sh/ty/issues/158

## Ecosystem impact

Correctly identified [modification of a `Final`
symbol](7b4164a5f2/sphinx/__init__.py (L44))
(behind a `# type: ignore`):
```diff
- warning[unused-ignore-comment] sphinx/__init__.py:44:56: Unused blanket `type: ignore` directive
```
And the same
[here](5471a37e82/src/trio/_core/_run.py (L128)):
```diff
- warning[unused-ignore-comment] src/trio/_core/_run.py:128:45: Unused blanket `type: ignore` directive
```

## Test Plan

New Markdown tests
2025-07-08 16:26:09 +02:00
Aria Desires
6a42d28867 [ty] Do not report settings diagnostics in check_file (#19206)
This is the trivial first part of
https://github.com/astral-sh/ty/issues/613

Ideally we should surface these elsewhere, but this is definitely Not
the place to surface them.
2025-07-08 10:18:32 -04:00
David Peter
ce2bdb9357 [ty] Conditionally defined dataclass fields (#19197)
## Summary

Fixes a bug where conditionally defined dataclass fields were previously
ignored.

Thanks to @lipefree for reporting this.

## Test Plan

New Markdown tests
2025-07-08 16:16:50 +02:00
GiGaGon
d78d10dd94 [pycodestyle] Make example not raise unnecessary SyntaxError (E114) (#19190)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of #18972

This PR makes [indentation-with-invalid-multiple-comment
(E114)](https://docs.astral.sh/ruff/rules/indentation-with-invalid-multiple-comment/#indentation-with-invalid-multiple-comment-e114)'s
example not raise a syntax error by adding a 4 space indented `...`. The
example still gave `E114` without this, but adding the `...` both makes
the change in indentation of the comment clearer, and makes it not give
a `SyntaxError`.

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-07-08 10:00:14 -04:00
GiGaGon
36276143be [pycodestyle] Make example error out-of-the-box (E272) (#19191)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of #18972

This PR makes [multiple-spaces-before-keyword
(E272)](https://docs.astral.sh/ruff/rules/multiple-spaces-before-keyword/#multiple-spaces-before-keyword-e272)'s
example error out-of-the-box. Since `True` is also a keyword, the old
example raises `E271` instead.

[Old example](https://play.ruff.rs/23ec3774-5038-471c-be3f-1c1e36f85cbb)
```py
True  and False
```

[New example](https://play.ruff.rs/d77432e2-fd99-4db2-9cd0-bc08675c0aca)
```py
x  and y
```

The "Use instead" section was also updated similarly.

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-07-08 09:58:04 -04:00
Brent Westbrook
2643dc5b7a Rename Diagnostic::syntax_error methods, separate Ord implementation (#19179)
## Summary

This PR addresses some additional feedback on #19053:

- Renaming the `syntax_error` methods to `invalid_syntax` to match the
lint id
- Moving the standalone `diagnostic_from_violation` function to
`Violation::into_diagnostic`
- Removing the `Ord` and `PartialOrd` implementations from `Diagnostic`
in favor of `Diagnostic::start_ordering`

## Test Plan

Existing tests

## Additional Follow-ups

Besides these, I also put the following comments on my todo list, but
they seemed like they might be big enough to have their own PRs:

- [Use `LintId::IOError` for IO
errors](https://github.com/astral-sh/ruff/pull/19053#discussion_r2189425922)
- [Move `Fix` and
`Edit`](https://github.com/astral-sh/ruff/pull/19053#discussion_r2189448647)
- [Avoid so many
unwraps](https://github.com/astral-sh/ruff/pull/19053#discussion_r2189465980)
2025-07-08 09:54:19 -04:00
justin
738692baff [ty] Fix __setattr__ call check precedence during attribute assignment (#18347)
## Summary

Related:

- https://github.com/astral-sh/ty/issues/111
- https://github.com/astral-sh/ruff/pull/17974#discussion_r2108527106

Previously, when validating an attribute assignment, a `__setattr__`
call check was only done if the attribute wasn't found as either a class
member or instance member

This PR changes the `__setattr__` call check to be attempted first,
prior to the "[normal
mechanism](https://docs.python.org/3/reference/datamodel.html#object.__setattr__)",
as a defined `__setattr__` should take precedence over setting an
attribute on the instance dictionary directly.

if the return type of `__setattr__` is `Never`, an `invalid-assignment`
diagnostic is emitted

Once this is merged, a subsequent PR will synthesize a `__setattr__`
method with a `Never` return type for frozen dataclasses.

## Test Plan

Existing tests + mypy_primer

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-08 15:34:34 +02:00
David Peter
9a4b85d845 [ty] Add tests for dataclass fields annotated with Final (#19202)
## Summary

Adds some tests for dataclass fields that are annotated with `Final`
(see comment
[here](https://github.com/astral-sh/ruff/pull/15768#issuecomment-3044737645)).
Turns out that nothing is needed here, everything already works as
expected (apart from the fact that we can assign to `Final` fields,
which is tracked in https://github.com/astral-sh/ty/issues/158

## Test Plan

New Markdown tests
2025-07-08 12:33:46 +00:00
David Peter
6d8c84bde9 [ty] Clarify diagnostic message (#19203)
This diagnostic message was missing the word "type"
2025-07-08 14:21:20 +02:00
184 changed files with 10178 additions and 4656 deletions

View File

@@ -12,6 +12,7 @@ on:
- ".github/workflows/mypy_primer.yaml"
- ".github/workflows/mypy_primer_comment.yaml"
- "Cargo.lock"
- "!**.md"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}

View File

@@ -17,6 +17,7 @@ env:
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
REF_NAME: ${{ github.ref_name }}
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
jobs:
ty-ecosystem-analyzer:
@@ -63,32 +64,75 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@9c34dc514ee9aef6735db1dfebb80f63acbc3440"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
ecosystem-analyzer \
--repository ruff \
analyze \
--projects ruff/projects_old.txt \
--commit old_commit \
--output diagnostics_old.json
diff \
--projects-old ruff/projects_old.txt \
--projects-new ruff/projects_new.txt \
--old old_commit \
--new new_commit \
--output-old diagnostics-old.json \
--output-new diagnostics-new.json
ecosystem-analyzer \
--repository ruff \
analyze \
--projects ruff/projects_new.txt \
--commit new_commit \
--output diagnostics_new.json
mkdir dist
ecosystem-analyzer \
generate-diff \
diagnostics_old.json \
diagnostics_new.json \
diagnostics-old.json \
diagnostics-new.json \
--old-name "main (merge base)" \
--new-name "$REF_NAME" \
--output-html diff.html
--output-html dist/diff.html
- name: Upload HTML diff report
ecosystem-analyzer \
generate-diff-statistics \
diagnostics-old.json \
diagnostics-new.json \
--old-name "main (merge base)" \
--new-name "$REF_NAME" \
--output diff-statistics.md
echo '## `ecosystem-analyzer` results' > comment.md
echo >> comment.md
cat diff-statistics.md >> comment.md
cat diff-statistics.md >> "$GITHUB_STEP_SUMMARY"
echo ${{ github.event.number }} > pr-number
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
id: deploy
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages deploy dist --project-name=ty-ecosystem --branch ${{ github.head_ref }} --commit-hash ${GITHUB_SHA}
- name: "Append deployment URL"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
run: |
echo >> comment.md
echo "**[Full report with detailed diff]($DEPLOYMENT_URL/diff)**" >> comment.md
- name: Upload comment
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: comment.md
path: comment.md
- name: Upload pr-number
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pr-number
path: pr-number
- name: Upload diagnostics diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: diff.html
path: diff.html
path: dist/diff.html

View File

@@ -0,0 +1,85 @@
name: PR comment (ty ecosystem-analyzer)
on: # zizmor: ignore[dangerous-triggers]
workflow_run:
workflows: [ty ecosystem-analyzer]
types: [completed]
workflow_dispatch:
inputs:
workflow_run_id:
description: The ty ecosystem-analyzer workflow that triggers the workflow run
required: true
jobs:
comment:
runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download PR number
with:
name: pr-number
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
if_no_artifact_found: ignore
allow_forks: true
- name: Parse pull request number
id: pr-number
run: |
if [[ -f pr-number ]]
then
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: "Download comment.md"
id: download-comment
if: steps.pr-number.outputs.pr-number
with:
name: comment.md
workflow: ty-ecosystem-analyzer.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/comment
workflow_conclusion: completed
if_no_artifact_found: ignore
allow_forks: true
- name: Generate comment content
id: generate-comment
if: ${{ steps.download-comment.outputs.found_artifact == 'true' }}
run: |
# Guard against malicious ty ecosystem-analyzer results that symlink to a secret
# file on this runner
if [[ -L pr/comment/comment.md ]]
then
echo "Error: comment.md cannot be a symlink"
exit 1
fi
# Note: this identifier is used to find the comment to update on subsequent runs
echo '<!-- generated-comment ty ecosystem-analyzer -->' > comment.md
echo >> comment.md
cat pr/comment/comment.md >> comment.md
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.md >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:
issue-number: ${{ steps.pr-number.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: "<!-- generated-comment ty ecosystem-analyzer -->"
- name: Create or update comment
if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }}
body-path: comment.md
edit-mode: replace

1
.github/zizmor.yml vendored
View File

@@ -10,6 +10,7 @@ rules:
ignore:
- build-docker.yml
- publish-playground.yml
- ty-ecosystem-analyzer.yaml
excessive-permissions:
# it's hard to test what the impact of removing these ignores would be
# without actually running the release workflow...

View File

@@ -6,7 +6,7 @@ exclude: |
crates/ty_vendored/vendor/.*|
crates/ty_project/resources/.*|
crates/ty_python_semantic/resources/corpus/.*|
crates/ty/docs/(configuration|rules|cli).md|
crates/ty/docs/(configuration|rules|cli|environment).md|
crates/ruff_benchmark/resources/.*|
crates/ruff_linter/resources/.*|
crates/ruff_linter/src/rules/.*/snapshots/.*|

View File

@@ -1,5 +1,33 @@
# Changelog
## 0.12.3
### Preview features
- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922))
### Bug fixes
- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433))
- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220))
- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150))
- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131))
### Documentation
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111))
- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151))
- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189))
- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191))
- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190))
- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218))
- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127))
- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949))
- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241))
## 0.12.2
### Preview features

85
Cargo.lock generated
View File

@@ -680,11 +680,6 @@ name = "countme"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
dependencies = [
"dashmap 5.5.3",
"once_cell",
"rustc-hash 1.1.0",
]
[[package]]
name = "cpufeatures"
@@ -852,19 +847,6 @@ dependencies = [
"syn",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "dashmap"
version = "6.1.0"
@@ -2262,7 +2244,7 @@ dependencies = [
"once_cell",
"pep440_rs",
"regex",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"smallvec",
"thiserror 1.0.69",
@@ -2729,7 +2711,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.2"
version = "0.12.3"
dependencies = [
"anyhow",
"argfile",
@@ -2772,7 +2754,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"shellexpand",
@@ -2818,7 +2800,7 @@ dependencies = [
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"tikv-jemallocator",
@@ -2847,7 +2829,7 @@ dependencies = [
"arc-swap",
"camino",
"countme",
"dashmap 6.1.0",
"dashmap",
"dunce",
"etcetera",
"filetime",
@@ -2866,7 +2848,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -2874,6 +2856,7 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"tracing-subscriber",
"ty_static",
"web-time",
"zip",
]
@@ -2917,6 +2900,7 @@ dependencies = [
"tracing-subscriber",
"ty",
"ty_project",
"ty_static",
"url",
]
@@ -2938,7 +2922,7 @@ dependencies = [
"ruff_cache",
"ruff_macros",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"schemars",
"serde",
"static_assertions",
@@ -2977,7 +2961,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.2"
version = "0.12.3"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3020,7 +3004,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"schemars",
"serde",
"serde_json",
@@ -3092,7 +3076,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -3142,7 +3126,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -3191,7 +3175,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"static_assertions",
@@ -3215,7 +3199,7 @@ dependencies = [
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"schemars",
"serde",
"smallvec",
@@ -3276,7 +3260,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"shellexpand",
@@ -3310,7 +3294,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.2"
version = "0.12.3"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3365,7 +3349,7 @@ dependencies = [
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_source_file",
"rustc-hash 2.1.1",
"rustc-hash",
"schemars",
"serde",
"shellexpand",
@@ -3384,12 +3368,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -3444,7 +3422,7 @@ dependencies = [
"parking_lot",
"portable-atomic",
"rayon",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa-macro-rules",
"salsa-macros",
"smallvec",
@@ -4142,7 +4120,6 @@ dependencies = [
"clap",
"clap_complete_command",
"colored 3.0.0",
"countme",
"crossbeam",
"ctrlc",
"dunce",
@@ -4165,6 +4142,7 @@ dependencies = [
"ty_project",
"ty_python_semantic",
"ty_server",
"ty_static",
"wild",
]
@@ -4174,11 +4152,14 @@ version = "0.0.0"
dependencies = [
"bitflags 2.9.1",
"insta",
"regex",
"ruff_db",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"smallvec",
"tracing",
@@ -4210,7 +4191,7 @@ dependencies = [
"ruff_python_ast",
"ruff_python_formatter",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -4231,7 +4212,6 @@ dependencies = [
"camino",
"colored 3.0.0",
"compact_str",
"countme",
"dir-test",
"drop_bomb",
"get-size2",
@@ -4255,7 +4235,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -4268,6 +4248,7 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"ty_python_semantic",
"ty_static",
"ty_test",
"ty_vendored",
]
@@ -4286,7 +4267,7 @@ dependencies = [
"ruff_notebook",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"serde",
"serde_json",
@@ -4299,6 +4280,13 @@ dependencies = [
"ty_vendored",
]
[[package]]
name = "ty_static"
version = "0.0.1"
dependencies = [
"ruff_macros",
]
[[package]]
name = "ty_test"
version = "0.0.0"
@@ -4317,7 +4305,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"rustc-stable-hash",
"salsa",
"serde",
@@ -4327,6 +4315,7 @@ dependencies = [
"toml",
"tracing",
"ty_python_semantic",
"ty_static",
"ty_vendored",
]

View File

@@ -44,6 +44,7 @@ ty_ide = { path = "crates/ty_ide" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
ty_server = { path = "crates/ty_server" }
ty_static = { path = "crates/ty_static" }
ty_test = { path = "crates/ty_test" }
ty_vendored = { path = "crates/ty_vendored" }
@@ -83,7 +84,7 @@ get-size2 = { version = "0.5.0", features = [
"derive",
"smallvec",
"hashbrown",
"compact-str"
"compact-str",
] }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
@@ -173,7 +174,7 @@ tracing-subscriber = { version = "0.3.18", default-features = false, features =
"env-filter",
"fmt",
"ansi",
"smallvec"
"smallvec",
] }
tryfn = { version = "0.2.1" }
typed-arena = { version = "2.0.2" }
@@ -183,11 +184,7 @@ unicode-width = { version = "0.2.0" }
unicode_names2 = { version = "1.2.2" }
unicode-normalization = { version = "0.1.23" }
url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = [
"v4",
"fast-rng",
"macro-diagnostics",
] }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] }
walkdir = { version = "2.3.2" }
wasm-bindgen = { version = "0.2.92" }
wasm-bindgen-test = { version = "0.3.42" }
@@ -222,8 +219,8 @@ must_use_candidate = "allow"
similar_names = "allow"
single_match_else = "allow"
too_many_lines = "allow"
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often.
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often.
# Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250
needless_raw_string_hashes = "allow"
# Disallowed restriction lints

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.12.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.2/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.3/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
rev: v0.12.3
hooks:
# Run the linter.
- id: ruff-check
@@ -430,6 +430,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Babel](https://github.com/python-babel/babel)
- Benchling ([Refac](https://github.com/benchling/refac))
- [Bokeh](https://github.com/bokeh/bokeh)
- Capital One ([datacompy](https://github.com/capitalone/datacompy))
- CrowdCent ([NumerBlox](https://github.com/crowdcent/numerblox)) <!-- typos: ignore -->
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- CERN ([Indico](https://getindico.io/))

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.12.2"
version = "0.12.3"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -681,7 +681,7 @@ mod tests {
UnsafeFixes::Enabled,
)
.unwrap();
if diagnostics.inner.iter().any(Diagnostic::is_syntax_error) {
if diagnostics.inner.iter().any(Diagnostic::is_invalid_syntax) {
parse_errors.push(path.clone());
}
paths.push(path);

View File

@@ -9,15 +9,15 @@ use ignore::Error;
use log::{debug, error, warn};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use ruff_linter::message::diagnostic_from_violation;
use rustc_hash::FxHashMap;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::panic::catch_unwind;
use ruff_linter::package::PackageRoot;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{LinterSettings, flags};
use ruff_linter::{IOError, fs, warn_user_once};
use ruff_linter::{IOError, Violation, fs, warn_user_once};
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::TextRange;
use ruff_workspace::resolver::{
@@ -129,11 +129,7 @@ pub(crate) fn check(
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
Diagnostics::new(
vec![diagnostic_from_violation(
IOError { message },
TextRange::default(),
&dummy,
)],
vec![IOError { message }.into_diagnostic(TextRange::default(), &dummy)],
FxHashMap::default(),
)
} else {
@@ -166,7 +162,9 @@ pub(crate) fn check(
|a, b| (a.0 + b.0, a.1 + b.1),
);
all_diagnostics.inner.sort();
all_diagnostics
.inner
.sort_by(Diagnostic::ruff_start_ordering);
// Store the caches.
caches.persist()?;

View File

@@ -1,6 +1,7 @@
use std::path::Path;
use anyhow::Result;
use ruff_db::diagnostic::Diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::packaging;
use ruff_linter::settings::flags;
@@ -52,6 +53,8 @@ pub(crate) fn check_stdin(
noqa,
fix_mode,
)?;
diagnostics.inner.sort_unstable();
diagnostics
.inner
.sort_unstable_by(Diagnostic::ruff_start_ordering);
Ok(diagnostics)
}

View File

@@ -13,13 +13,13 @@ use log::{debug, warn};
use ruff_db::diagnostic::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only};
use ruff_linter::message::{create_syntax_error_diagnostic, diagnostic_from_violation};
use ruff_linter::message::create_syntax_error_diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{LinterSettings, flags};
use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::{IOError, fs};
use ruff_linter::{IOError, Violation, fs};
use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
use ruff_source_file::SourceFileBuilder;
@@ -62,13 +62,12 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let source_file = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![diagnostic_from_violation(
vec![
IOError {
message: err.to_string(),
},
TextRange::default(),
&source_file,
)],
}
.into_diagnostic(TextRange::default(), &source_file),
],
FxHashMap::default(),
)
} else {

View File

@@ -5692,3 +5692,57 @@ class Foo:
"
);
}
#[test_case::test_case("concise")]
#[test_case::test_case("full")]
#[test_case::test_case("json")]
#[test_case::test_case("json-lines")]
#[test_case::test_case("junit")]
#[test_case::test_case("grouped")]
#[test_case::test_case("github")]
#[test_case::test_case("gitlab")]
#[test_case::test_case("pylint")]
#[test_case::test_case("rdjson")]
#[test_case::test_case("azure")]
#[test_case::test_case("sarif")]
fn output_format(output_format: &str) -> Result<()> {
const CONTENT: &str = "\
import os # F401
x = y # F821
match 42: # invalid-syntax
case _: ...
";
let tempdir = TempDir::new()?;
let input = tempdir.path().join("input.py");
fs::write(&input, CONTENT)?;
let snapshot = format!("output_format_{output_format}");
insta::with_settings!({
filters => vec![
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
(r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#),
(ruff_linter::VERSION, "[VERSION]"),
]
}, {
assert_cmd_snapshot!(
snapshot,
Command::new(get_cargo_bin(BIN_NAME))
.args([
"check",
"--no-cache",
"--output-format",
output_format,
"--select",
"F401,F821",
"--target-version",
"py39",
"input.py",
])
.current_dir(&tempdir),
);
});
Ok(())
}

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- azure
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=2;columnnumber=5;code=F821;]Undefined name `y`
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=3;columnnumber=1;]SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -0,0 +1,25 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- concise
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:1:8: F401 [*] `os` imported but unused
input.py:2:5: F821 Undefined name `y`
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 3 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----

View File

@@ -0,0 +1,49 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- full
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:1:8: F401 [*] `os` imported but unused
|
1 | import os # F401
| ^^ F401
2 | x = y # F821
3 | match 42: # invalid-syntax
|
= help: Remove unused import: `os`
input.py:2:5: F821 Undefined name `y`
|
1 | import os # F401
2 | x = y # F821
| ^ F821
3 | match 42: # invalid-syntax
4 | case _: ...
|
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
1 | import os # F401
2 | x = y # F821
3 | match 42: # invalid-syntax
| ^^^^^
4 | case _: ...
|
Found 3 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- github
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
::error title=Ruff (F401),file=[TMP]/input.py,line=1,col=8,endLine=1,endColumn=10::input.py:1:8: F401 `os` imported but unused
::error title=Ruff (F821),file=[TMP]/input.py,line=2,col=5,endLine=2,endColumn=6::input.py:2:5: F821 Undefined name `y`
::error title=Ruff,file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -0,0 +1,60 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- gitlab
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
[
{
"check_name": "F401",
"description": "`os` imported but unused",
"fingerprint": "4dbad37161e65c72",
"location": {
"lines": {
"begin": 1,
"end": 1
},
"path": "input.py"
},
"severity": "major"
},
{
"check_name": "F821",
"description": "Undefined name `y`",
"fingerprint": "7af59862a085230",
"location": {
"lines": {
"begin": 2,
"end": 2
},
"path": "input.py"
},
"severity": "major"
},
{
"check_name": "syntax-error",
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"fingerprint": "e558cec859bb66e8",
"location": {
"lines": {
"begin": 3,
"end": 3
},
"path": "input.py"
},
"severity": "major"
}
]
----- stderr -----

View File

@@ -0,0 +1,27 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- grouped
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:
1:8 F401 [*] `os` imported but unused
2:5 F821 Undefined name `y`
3:1 SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 3 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- json-lines
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
{"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"[TMP]/input.py","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
{"cell":null,"code":"F821","end_location":{"column":6,"row":2},"filename":"[TMP]/input.py","fix":null,"location":{"column":5,"row":2},"message":"Undefined name `y`","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/undefined-name"}
{"cell":null,"code":null,"end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null}
----- stderr -----

View File

@@ -0,0 +1,88 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- json
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
[
{
"cell": null,
"code": "F401",
"end_location": {
"column": 10,
"row": 1
},
"filename": "[TMP]/input.py",
"fix": {
"applicability": "safe",
"edits": [
{
"content": "",
"end_location": {
"column": 1,
"row": 2
},
"location": {
"column": 1,
"row": 1
}
}
],
"message": "Remove unused import: `os`"
},
"location": {
"column": 8,
"row": 1
},
"message": "`os` imported but unused",
"noqa_row": 1,
"url": "https://docs.astral.sh/ruff/rules/unused-import"
},
{
"cell": null,
"code": "F821",
"end_location": {
"column": 6,
"row": 2
},
"filename": "[TMP]/input.py",
"fix": null,
"location": {
"column": 5,
"row": 2
},
"message": "Undefined name `y`",
"noqa_row": 2,
"url": "https://docs.astral.sh/ruff/rules/undefined-name"
},
{
"cell": null,
"code": null,
"end_location": {
"column": 6,
"row": 3
},
"filename": "[TMP]/input.py",
"fix": null,
"location": {
"column": 1,
"row": 3
},
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"noqa_row": null,
"url": null
}
]
----- stderr -----

View File

@@ -0,0 +1,34 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- junit
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="ruff" tests="3" failures="3" errors="0">
<testsuite name="[TMP]/input.py" tests="3" disabled="0" errors="0" failures="3" package="org.ruff">
<testcase name="org.ruff.F401" classname="[TMP]/input" line="1" column="8">
<failure message="`os` imported but unused">line 1, col 8, `os` imported but unused</failure>
</testcase>
<testcase name="org.ruff.F821" classname="[TMP]/input" line="2" column="5">
<failure message="Undefined name `y`">line 2, col 5, Undefined name `y`</failure>
</testcase>
<testcase name="org.ruff" classname="[TMP]/input" line="3" column="1">
<failure message="SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)">line 3, col 1, SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)</failure>
</testcase>
</testsuite>
</testsuites>
----- stderr -----

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- pylint
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:1: [F401] `os` imported but unused
input.py:2: [F821] Undefined name `y`
input.py:3: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -0,0 +1,103 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- rdjson
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
{
"diagnostics": [
{
"code": {
"url": "https://docs.astral.sh/ruff/rules/unused-import",
"value": "F401"
},
"location": {
"path": "[TMP]/input.py",
"range": {
"end": {
"column": 10,
"line": 1
},
"start": {
"column": 8,
"line": 1
}
}
},
"message": "`os` imported but unused",
"suggestions": [
{
"range": {
"end": {
"column": 1,
"line": 2
},
"start": {
"column": 1,
"line": 1
}
},
"text": ""
}
]
},
{
"code": {
"url": "https://docs.astral.sh/ruff/rules/undefined-name",
"value": "F821"
},
"location": {
"path": "[TMP]/input.py",
"range": {
"end": {
"column": 6,
"line": 2
},
"start": {
"column": 5,
"line": 2
}
}
},
"message": "Undefined name `y`"
},
{
"code": {
"url": null,
"value": null
},
"location": {
"path": "[TMP]/input.py",
"range": {
"end": {
"column": 6,
"line": 3
},
"start": {
"column": 1,
"line": 3
}
}
},
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
}
],
"severity": "warning",
"source": {
"name": "ruff",
"url": "https://docs.astral.sh/ruff"
}
}
----- stderr -----

View File

@@ -0,0 +1,142 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- sarif
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"runs": [
{
"results": [
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"region": {
"endColumn": 10,
"endLine": 1,
"startColumn": 8,
"startLine": 1
}
}
}
],
"message": {
"text": "`os` imported but unused"
},
"ruleId": "F401"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"region": {
"endColumn": 6,
"endLine": 2,
"startColumn": 5,
"startLine": 2
}
}
}
],
"message": {
"text": "Undefined name `y`"
},
"ruleId": "F821"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"region": {
"endColumn": 6,
"endLine": 3,
"startColumn": 1,
"startLine": 3
}
}
}
],
"message": {
"text": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
},
"ruleId": null
}
],
"tool": {
"driver": {
"informationUri": "https://github.com/astral-sh/ruff",
"name": "ruff",
"rules": [
{
"fullDescription": {
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
},
"help": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
},
"helpUri": "https://docs.astral.sh/ruff/rules/unused-import",
"id": "F401",
"properties": {
"id": "F401",
"kind": "Pyflakes",
"name": "unused-import",
"problem.severity": "error"
},
"shortDescription": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
}
},
{
"fullDescription": {
"text": "## What it does\nChecks for uses of undefined names.\n\n## Why is this bad?\nAn undefined name is likely to raise `NameError` at runtime.\n\n## Example\n```python\ndef double():\n return n * 2 # raises `NameError` if `n` is undefined when `double` is called\n```\n\nUse instead:\n```python\ndef double(n):\n return n * 2\n```\n\n## Options\n- [`target-version`]: Can be used to configure which symbols Ruff will understand\n as being available in the `builtins` namespace.\n\n## References\n- [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)\n"
},
"help": {
"text": "Undefined name `{name}`. {tip}"
},
"helpUri": "https://docs.astral.sh/ruff/rules/undefined-name",
"id": "F821",
"properties": {
"id": "F821",
"kind": "Pyflakes",
"name": "undefined-name",
"problem.severity": "error"
},
"shortDescription": {
"text": "Undefined name `{name}`. {tip}"
}
}
],
"version": "[VERSION]"
}
}
}
],
"version": "2.1.0"
}
----- stderr -----

View File

@@ -20,6 +20,7 @@ ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true, features = ["get-size"] }
ruff_text_size = { workspace = true }
ty_static = { workspace = true }
anstyle = { workspace = true }
arc-swap = { workspace = true }

View File

@@ -83,7 +83,7 @@ impl Diagnostic {
///
/// Note that `message` is stored in the primary annotation, _not_ in the primary diagnostic
/// message.
pub fn syntax_error(
pub fn invalid_syntax(
span: impl Into<Span>,
message: impl IntoDiagnosticMessage,
range: impl Ranged,
@@ -365,7 +365,7 @@ impl Diagnostic {
}
/// Returns `true` if `self` is a syntax error message.
pub fn is_syntax_error(&self) -> bool {
pub fn is_invalid_syntax(&self) -> bool {
self.id().is_invalid_syntax()
}
@@ -381,7 +381,7 @@ impl Diagnostic {
/// Returns the URL for the rule documentation, if it exists.
pub fn to_url(&self) -> Option<String> {
if self.is_syntax_error() {
if self.is_invalid_syntax() {
None
} else {
Some(format!(
@@ -447,20 +447,16 @@ impl Diagnostic {
pub fn expect_range(&self) -> TextRange {
self.range().expect("Expected a range for the primary span")
}
}
impl Ord for Diagnostic {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
}
}
impl PartialOrd for Diagnostic {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(
(self.ruff_source_file()?, self.range()?.start())
.cmp(&(other.ruff_source_file()?, other.range()?.start())),
)
/// Returns the ordering of diagnostics based on the start of their ranges, if they have any.
///
/// Panics if either diagnostic has no primary span, if the span has no range, or if its file is
/// not a `SourceFile`.
pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering {
(self.expect_ruff_source_file(), self.expect_range().start()).cmp(&(
other.expect_ruff_source_file(),
other.expect_range().start(),
))
}
}

View File

@@ -5,6 +5,7 @@ use ruff_python_ast::PythonVersion;
use rustc_hash::FxHasher;
use std::hash::BuildHasherDefault;
use std::num::NonZeroUsize;
use ty_static::EnvVars;
pub mod diagnostic;
pub mod display;
@@ -50,8 +51,8 @@ pub trait Db: salsa::Database {
/// ty can still spawn more threads for other tasks, e.g. to wait for a Ctrl+C signal or
/// watching the files for changes.
pub fn max_parallelism() -> NonZeroUsize {
std::env::var("TY_MAX_PARALLELISM")
.or_else(|_| std::env::var("RAYON_NUM_THREADS"))
std::env::var(EnvVars::TY_MAX_PARALLELISM)
.or_else(|_| std::env::var(EnvVars::RAYON_NUM_THREADS))
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| {

View File

@@ -13,6 +13,7 @@ license = { workspace = true }
[dependencies]
ty = { workspace = true }
ty_project = { workspace = true, features = ["schemars"] }
ty_static = { workspace = true }
ruff = { workspace = true }
ruff_formatter = { workspace = true }
ruff_linter = { workspace = true, features = ["schemars"] }

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use crate::{
generate_cli_help, generate_docs, generate_json_schema, generate_ty_cli_reference,
generate_ty_options, generate_ty_rules, generate_ty_schema,
generate_ty_env_vars_reference, generate_ty_options, generate_ty_rules, generate_ty_schema,
};
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
@@ -44,5 +44,8 @@ pub(crate) fn main(args: &Args) -> Result<()> {
generate_ty_options::main(&generate_ty_options::Args { mode: args.mode })?;
generate_ty_rules::main(&generate_ty_rules::Args { mode: args.mode })?;
generate_ty_cli_reference::main(&generate_ty_cli_reference::Args { mode: args.mode })?;
generate_ty_env_vars_reference::main(&generate_ty_env_vars_reference::Args {
mode: args.mode,
})?;
Ok(())
}

View File

@@ -0,0 +1,119 @@
//! Generate the environment variables reference from `ty_static::EnvVars`.
use std::collections::BTreeSet;
use std::fs;
use std::path::PathBuf;
use anyhow::bail;
use pretty_assertions::StrComparison;
use ty_static::EnvVars;
use crate::generate_all::Mode;
#[derive(clap::Args)]
pub(crate) struct Args {
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
let reference_string = generate();
let filename = "environment.md";
let reference_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("crates")
.join("ty")
.join("docs")
.join(filename);
match args.mode {
Mode::DryRun => {
println!("{reference_string}");
}
Mode::Check => match fs::read_to_string(&reference_path) {
Ok(current) => {
if current == reference_string {
println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &reference_string);
bail!(
"{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{comparison}"
);
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
bail!(
"{filename} not found, please run `cargo dev generate-ty-env-vars-reference`"
);
}
Err(err) => {
bail!(
"{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{err}"
);
}
},
Mode::Write => {
// Ensure the docs directory exists
if let Some(parent) = reference_path.parent() {
fs::create_dir_all(parent)?;
}
match fs::read_to_string(&reference_path) {
Ok(current) => {
if current == reference_string {
println!("Up-to-date: {filename}");
} else {
println!("Updating: {filename}");
fs::write(&reference_path, reference_string.as_bytes())?;
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
println!("Updating: {filename}");
fs::write(&reference_path, reference_string.as_bytes())?;
}
Err(err) => {
bail!(
"{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{err}"
);
}
}
}
}
Ok(())
}
fn generate() -> String {
let mut output = String::new();
output.push_str("# Environment variables\n\n");
// Partition and sort environment variables into TY_ and external variables.
let (ty_vars, external_vars): (BTreeSet<_>, BTreeSet<_>) = EnvVars::metadata()
.iter()
.partition(|(var, _)| var.starts_with("TY_"));
output.push_str("ty defines and respects the following environment variables:\n\n");
for (var, doc) in ty_vars {
output.push_str(&render(var, doc));
}
output.push_str("## Externally-defined variables\n\n");
output.push_str("ty also reads the following externally defined environment variables:\n\n");
for (var, doc) in external_vars {
output.push_str(&render(var, doc));
}
output
}
/// Render an environment variable and its documentation.
fn render(var: &str, doc: &str) -> String {
format!("### `{var}`\n\n{doc}\n\n")
}

View File

@@ -18,6 +18,7 @@ mod generate_json_schema;
mod generate_options;
mod generate_rules_table;
mod generate_ty_cli_reference;
mod generate_ty_env_vars_reference;
mod generate_ty_options;
mod generate_ty_rules;
mod generate_ty_schema;
@@ -53,6 +54,8 @@ enum Command {
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions,
GenerateTyOptions(generate_ty_options::Args),
/// Generate environment variables reference for ty.
GenerateTyEnvVarsReference(generate_ty_env_vars_reference::Args),
/// Generate CLI help.
GenerateCliHelp(generate_cli_help::Args),
/// Generate Markdown docs.
@@ -98,6 +101,7 @@ fn main() -> Result<ExitCode> {
Command::GenerateTyRules(args) => generate_ty_rules::main(&args)?,
Command::GenerateOptions => println!("{}", generate_options::generate()),
Command::GenerateTyOptions(args) => generate_ty_options::main(&args)?,
Command::GenerateTyEnvVarsReference(args) => generate_ty_env_vars_reference::main(&args)?,
Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?,
Command::GenerateDocs(args) => generate_docs::main(&args)?,
Command::PrintAST(args) => print_ast::main(&args)?,

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.12.2"
version = "0.12.3"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,10 +1,10 @@
"""
Should emit:
B017 - on lines 23 and 41
B017 - on lines 24, 28, 46, 49, 52, and 58
"""
import asyncio
import unittest
import pytest
import pytest, contextlib
CONSTANT = True

View File

@@ -0,0 +1,28 @@
"""
Should emit:
B017 - on lines 20, 21, 25, and 26
"""
import unittest
import pytest
def something_else() -> None:
for i in (1, 2, 3):
print(i)
class Foo:
pass
class Foobar(unittest.TestCase):
def call_form_raises(self) -> None:
self.assertRaises(Exception, something_else)
self.assertRaises(BaseException, something_else)
def test_pytest_call_form() -> None:
pytest.raises(Exception, something_else)
pytest.raises(BaseException, something_else)
pytest.raises(Exception, something_else, match="hello")

View File

@@ -422,6 +422,35 @@ def func(a: dict[str, int]) -> list[dict[str, int]]:
services = a["services"]
return services
# See: https://github.com/astral-sh/ruff/issues/14052
def outer() -> list[object]:
@register
async def inner() -> None:
print(layout)
layout = [...]
return layout
def outer() -> list[object]:
with open("") as f:
async def inner() -> None:
print(layout)
layout = [...]
return layout
def outer() -> list[object]:
def inner():
with open("") as f:
async def inner_inner() -> None:
print(layout)
layout = [...]
return layout
# See: https://github.com/astral-sh/ruff/issues/18411
def f():
(#=

View File

@@ -0,0 +1,6 @@
# Regression test for: https://github.com/astral-sh/ruff/issues/19175
# there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash
from typing import TYPE_CHECKING \
if TYPE_CHECKING: import builtins
builtins.print("!")

View File

@@ -125,3 +125,19 @@ class ClassForCommentEnthusiasts(BaseClass):
self
# also a comment
).f()
# Issue #19096: super calls with keyword arguments should emit diagnostic but not be fixed
class Ord(int):
def __len__(self):
return super(Ord, self, uhoh=True, **{"error": True}).bit_length()
class ExampleWithKeywords:
def method1(self):
super(ExampleWithKeywords, self, invalid=True).some_method() # Should emit diagnostic but NOT be fixed
def method2(self):
super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
def method3(self):
super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords

View File

@@ -4,8 +4,8 @@ use crate::Fix;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes,
pylint, pyupgrade, refurb, ruff,
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_return,
flake8_type_checking, pyflakes, pylint, pyupgrade, refurb, ruff,
};
/// Run lint rules over the [`Binding`]s.
@@ -25,11 +25,20 @@ pub(crate) fn bindings(checker: &Checker) {
Rule::ForLoopWrites,
Rule::CustomTypeVarForSelf,
Rule::PrivateTypeParameter,
Rule::UnnecessaryAssign,
]) {
return;
}
for (binding_id, binding) in checker.semantic.bindings.iter_enumerated() {
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
if binding.kind.is_function_definition() {
flake8_return::rules::unnecessary_assign(
checker,
binding.statement(checker.semantic()).unwrap(),
);
}
}
if checker.is_rule_enabled(Rule::UnusedVariable) {
if binding.kind.is_bound_exception()
&& binding.is_unused()

View File

@@ -7,7 +7,9 @@ use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_optional_as_none_in_union_enabled;
use crate::preview::{
is_assert_raises_exception_call_enabled, is_optional_as_none_in_union_enabled,
};
use crate::registry::Rule;
use crate::rules::{
airflow, flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear,
@@ -1037,27 +1039,14 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
}
if checker.any_rule_enabled(&[
Rule::OsPathAbspath,
Rule::OsChmod,
Rule::OsMkdir,
Rule::OsMakedirs,
Rule::OsRename,
Rule::OsReplace,
Rule::OsRmdir,
Rule::OsRemove,
Rule::OsUnlink,
Rule::OsGetcwd,
Rule::OsPathExists,
Rule::OsPathExpanduser,
Rule::OsPathIsdir,
Rule::OsPathIsfile,
Rule::OsPathIslink,
Rule::OsReadlink,
Rule::OsStat,
Rule::OsPathIsabs,
Rule::OsPathJoin,
Rule::OsPathBasename,
Rule::OsPathDirname,
Rule::OsPathSamefile,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
@@ -1068,21 +1057,66 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
]) {
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetsize) {
flake8_use_pathlib::rules::os_path_getsize(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetatime) {
flake8_use_pathlib::rules::os_path_getatime(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetctime) {
flake8_use_pathlib::rules::os_path_getctime(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetmtime) {
flake8_use_pathlib::rules::os_path_getmtime(checker, call);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, call);
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(&call.func) {
let segments = qualified_name.segments();
if checker.is_rule_enabled(Rule::OsPathGetsize) {
flake8_use_pathlib::rules::os_path_getsize(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathGetatime) {
flake8_use_pathlib::rules::os_path_getatime(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathGetctime) {
flake8_use_pathlib::rules::os_path_getctime(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathGetmtime) {
flake8_use_pathlib::rules::os_path_getmtime(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathAbspath) {
flake8_use_pathlib::rules::os_path_abspath(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsRmdir) {
flake8_use_pathlib::rules::os_rmdir(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsRemove) {
flake8_use_pathlib::rules::os_remove(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsUnlink) {
flake8_use_pathlib::rules::os_unlink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathExists) {
flake8_use_pathlib::rules::os_path_exists(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathExpanduser) {
flake8_use_pathlib::rules::os_path_expanduser(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathBasename) {
flake8_use_pathlib::rules::os_path_basename(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathDirname) {
flake8_use_pathlib::rules::os_path_dirname(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathIsabs) {
flake8_use_pathlib::rules::os_path_isabs(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathIsdir) {
flake8_use_pathlib::rules::os_path_isdir(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathIsfile) {
flake8_use_pathlib::rules::os_path_isfile(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathIslink) {
flake8_use_pathlib::rules::os_path_islink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsReadlink) {
flake8_use_pathlib::rules::os_readlink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(
checker, call, segments,
);
}
}
if checker.is_rule_enabled(Rule::OsSepSplit) {
flake8_use_pathlib::rules::os_sep_split(checker, call);
}
@@ -1236,6 +1270,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::NonOctalPermissions) {
ruff::rules::non_octal_permissions(checker, call);
}
if checker.is_rule_enabled(Rule::AssertRaisesException)
&& is_assert_raises_exception_call_enabled(checker.settings())
{
flake8_bugbear::rules::assert_raises_exception_call(checker, call);
}
}
Expr::Dict(dict) => {
if checker.any_rule_enabled(&[

View File

@@ -207,7 +207,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
Rule::UnnecessaryReturnNone,
Rule::ImplicitReturnValue,
Rule::ImplicitReturn,
Rule::UnnecessaryAssign,
Rule::SuperfluousElseReturn,
Rule::SuperfluousElseRaise,
Rule::SuperfluousElseContinue,

View File

@@ -64,7 +64,6 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::annotation::AnnotationContext;
use crate::docstrings::extraction::ExtractionTarget;
use crate::importer::{ImportRequest, Importer, ResolutionError};
use crate::message::diagnostic_from_violation;
use crate::noqa::NoqaMapping;
use crate::package::PackageRoot;
use crate::preview::is_undefined_export_in_dunder_init_enabled;
@@ -3158,7 +3157,7 @@ impl<'a> LintContext<'a> {
) -> DiagnosticGuard<'chk, 'a> {
DiagnosticGuard {
context: self,
diagnostic: Some(diagnostic_from_violation(kind, range, &self.source_file)),
diagnostic: Some(kind.into_diagnostic(range, &self.source_file)),
rule: T::rule(),
}
}
@@ -3177,7 +3176,7 @@ impl<'a> LintContext<'a> {
if self.is_rule_enabled(rule) {
Some(DiagnosticGuard {
context: self,
diagnostic: Some(diagnostic_from_violation(kind, range, &self.source_file)),
diagnostic: Some(kind.into_diagnostic(range, &self.source_file)),
rule,
})
} else {

View File

@@ -919,27 +919,27 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Tryceratops, "401") => (RuleGroup::Stable, rules::tryceratops::rules::VerboseLogMessage),
// flake8-use-pathlib
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathAbspath),
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath),
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsChmod),
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir),
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs),
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRename),
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReplace),
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRmdir),
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRemove),
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsUnlink),
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove),
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink),
(Flake8UsePathlib, "109") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsGetcwd),
(Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathExists),
(Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathExpanduser),
(Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsdir),
(Flake8UsePathlib, "113") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsfile),
(Flake8UsePathlib, "114") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIslink),
(Flake8UsePathlib, "115") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReadlink),
(Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExists),
(Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExpanduser),
(Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsdir),
(Flake8UsePathlib, "113") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsfile),
(Flake8UsePathlib, "114") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIslink),
(Flake8UsePathlib, "115") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReadlink),
(Flake8UsePathlib, "116") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsStat),
(Flake8UsePathlib, "117") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsabs),
(Flake8UsePathlib, "117") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsabs),
(Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin),
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathBasename),
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathDirname),
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename),
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSamefile),
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),

View File

@@ -618,8 +618,7 @@ mod tests {
use crate::fix::edits::{
add_to_dunder_all, make_redundant_alias, next_stmt_break, trailing_semicolon,
};
use crate::message::diagnostic_from_violation;
use crate::{Edit, Fix, Locator};
use crate::{Edit, Fix, Locator, Violation};
/// Parse the given source using [`Mode::Module`] and return the first statement.
fn parse_first_stmt(source: &str) -> Result<Stmt> {
@@ -750,8 +749,8 @@ x = 1 \
let diag = {
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
let mut iter = edits.into_iter();
let mut diagnostic = diagnostic_from_violation(
MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary.
// The choice of rule here is arbitrary.
let mut diagnostic = MissingNewlineAtEndOfFile.into_diagnostic(
TextRange::default(),
&SourceFileBuilder::new("<filename>", "<code>").finish(),
);

View File

@@ -172,11 +172,10 @@ mod tests {
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{Ranged, TextSize};
use crate::Locator;
use crate::fix::{FixResult, apply_fixes};
use crate::message::diagnostic_from_violation;
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
use crate::{Edit, Fix};
use crate::{Locator, Violation};
use ruff_db::diagnostic::Diagnostic;
fn create_diagnostics(
@@ -187,8 +186,7 @@ mod tests {
edit.into_iter()
.map(|edit| {
// The choice of rule here is arbitrary.
let mut diagnostic = diagnostic_from_violation(
MissingNewlineAtEndOfFile,
let mut diagnostic = MissingNewlineAtEndOfFile.into_diagnostic(
edit.range(),
&SourceFileBuilder::new(filename, source).finish(),
);

View File

@@ -5,6 +5,7 @@ use ruff_python_ast::Stmt;
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_codegen::Stylist;
use ruff_python_parser::{TokenKind, Tokens};
use ruff_python_trivia::is_python_whitespace;
use ruff_python_trivia::{PythonWhitespace, textwrap::indent};
use ruff_source_file::{LineRanges, UniversalNewlineIterator};
use ruff_text_size::{Ranged, TextSize};
@@ -306,7 +307,7 @@ fn match_semicolon(s: &str) -> Option<TextSize> {
fn match_continuation(s: &str) -> Option<TextSize> {
for (offset, c) in s.char_indices() {
match c {
' ' | '\t' => continue,
_ if is_python_whitespace(c) => continue,
'\\' => return Some(TextSize::try_from(offset).unwrap()),
_ => break,
}

View File

@@ -514,7 +514,7 @@ pub fn lint_only(
LinterResult {
has_valid_syntax: parsed.has_valid_syntax(),
has_no_syntax_errors: !diagnostics.iter().any(Diagnostic::is_syntax_error),
has_no_syntax_errors: !diagnostics.iter().any(Diagnostic::is_invalid_syntax),
diagnostics,
}
}
@@ -629,7 +629,7 @@ pub fn lint_fix<'a>(
if iterations == 0 {
has_valid_syntax = parsed.has_valid_syntax();
has_no_syntax_errors = !diagnostics.iter().any(Diagnostic::is_syntax_error);
has_no_syntax_errors = !diagnostics.iter().any(Diagnostic::is_invalid_syntax);
} else {
// If the source code had no syntax errors on the first pass, but
// does on a subsequent pass, then we've introduced a

View File

@@ -24,7 +24,6 @@ pub use sarif::SarifEmitter;
pub use text::TextEmitter;
use crate::Fix;
use crate::Violation;
use crate::registry::Rule;
mod azure;
@@ -108,28 +107,6 @@ where
diagnostic
}
// TODO(brent) We temporarily allow this to avoid updating all of the call sites to add
// references. I expect this method to go away or change significantly with the rest of the
// diagnostic refactor, but if it still exists in this form at the end of the refactor, we
// should just update the call sites.
#[expect(clippy::needless_pass_by_value)]
pub fn diagnostic_from_violation<T: Violation>(
kind: T,
range: TextRange,
file: &SourceFile,
) -> Diagnostic {
create_lint_diagnostic(
Violation::message(&kind),
Violation::fix_title(&kind),
range,
None,
None,
file.clone(),
None,
T::rule(),
)
}
struct MessageWithLocation<'a> {
message: &'a Diagnostic,
start_location: LineColumn,

View File

@@ -1225,8 +1225,6 @@ mod tests {
use ruff_source_file::{LineEnding, SourceFileBuilder};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::Edit;
use crate::message::diagnostic_from_violation;
use crate::noqa::{
Directive, LexicalError, NoqaLexerOutput, NoqaMapping, add_noqa_inner, lex_codes,
lex_file_exemption, lex_inline_noqa,
@@ -1234,6 +1232,7 @@ mod tests {
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
use crate::rules::pyflakes::rules::UnusedVariable;
use crate::rules::pyupgrade::rules::PrintfStringFormatting;
use crate::{Edit, Violation};
use crate::{Locator, generate_noqa_edits};
fn assert_lexed_ranges_match_slices(
@@ -2832,10 +2831,10 @@ mod tests {
assert_eq!(output, format!("{contents}"));
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
let messages = [diagnostic_from_violation(
UnusedVariable {
name: "x".to_string(),
},
let messages = [UnusedVariable {
name: "x".to_string(),
}
.into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
)];
@@ -2856,15 +2855,14 @@ mod tests {
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
let messages = [
diagnostic_from_violation(
AmbiguousVariableName("x".to_string()),
AmbiguousVariableName("x".to_string()).into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
),
diagnostic_from_violation(
UnusedVariable {
name: "x".to_string(),
},
UnusedVariable {
name: "x".to_string(),
}
.into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
),
@@ -2887,15 +2885,14 @@ mod tests {
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
let messages = [
diagnostic_from_violation(
AmbiguousVariableName("x".to_string()),
AmbiguousVariableName("x".to_string()).into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
),
diagnostic_from_violation(
UnusedVariable {
name: "x".to_string(),
},
UnusedVariable {
name: "x".to_string(),
}
.into_diagnostic(
TextRange::new(TextSize::from(0), TextSize::from(0)),
&source_file,
),
@@ -2931,11 +2928,8 @@ print(
"#;
let noqa_line_for = [TextRange::new(8.into(), 68.into())].into_iter().collect();
let source_file = SourceFileBuilder::new(path.to_string_lossy(), source).finish();
let messages = [diagnostic_from_violation(
PrintfStringFormatting,
TextRange::new(12.into(), 79.into()),
&source_file,
)];
let messages = [PrintfStringFormatting
.into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)];
let comment_ranges = CommentRanges::default();
let edits = generate_noqa_edits(
path,
@@ -2964,11 +2958,8 @@ foo;
bar =
";
let source_file = SourceFileBuilder::new(path.to_string_lossy(), source).finish();
let messages = [diagnostic_from_violation(
UselessSemicolon,
TextRange::new(4.into(), 5.into()),
&source_file,
)];
let messages =
[UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)];
let noqa_line_for = NoqaMapping::default();
let comment_ranges = CommentRanges::default();
let edits = generate_noqa_edits(

View File

@@ -69,6 +69,71 @@ pub(crate) const fn is_fix_os_path_getctime_enabled(settings: &LinterSettings) -
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_abspath_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_rmdir_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_unlink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_remove_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_exists_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_expanduser_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_isdir_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_isfile_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_islink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_isabs_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_readlink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_basename_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/11436
// https://github.com/astral-sh/ruff/pull/11168
pub(crate) const fn is_dunder_init_fix_unused_import_enabled(settings: &LinterSettings) -> bool {
@@ -125,3 +190,8 @@ pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19063
pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -6,11 +6,10 @@ use ruff_text_size::{TextRange, TextSize};
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::SourceFile;
use crate::IOError;
use crate::message::diagnostic_from_violation;
use crate::registry::Rule;
use crate::rules::ruff::rules::InvalidPyprojectToml;
use crate::settings::LinterSettings;
use crate::{IOError, Violation};
/// RUF200
pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings) -> Vec<Diagnostic> {
@@ -30,11 +29,8 @@ pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings)
source_file.name(),
);
if settings.rules.enabled(Rule::IOError) {
let diagnostic = diagnostic_from_violation(
IOError { message },
TextRange::default(),
source_file,
);
let diagnostic =
IOError { message }.into_diagnostic(TextRange::default(), source_file);
messages.push(diagnostic);
} else {
warn!(
@@ -56,11 +52,8 @@ pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings)
if settings.rules.enabled(Rule::InvalidPyprojectToml) {
let toml_err = err.message().to_string();
let diagnostic = diagnostic_from_violation(
InvalidPyprojectToml { message: toml_err },
range,
source_file,
);
let diagnostic =
InvalidPyprojectToml { message: toml_err }.into_diagnostic(range, source_file);
messages.push(diagnostic);
}

View File

@@ -283,7 +283,7 @@ impl Violation for SuspiciousXmlrpcImport {
///
/// ## Example
/// ```python
/// import wsgiref.handlers.CGIHandler
/// from wsgiref.handlers import CGIHandler
/// ```
///
/// ## References

View File

@@ -16,11 +16,14 @@ mod tests {
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::settings::types::PreviewMode;
use ruff_python_ast::PythonVersion;
#[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))]
#[test_case(Rule::AssertFalse, Path::new("B011.py"))]
#[test_case(Rule::AssertRaisesException, Path::new("B017.py"))]
#[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))]
#[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))]
#[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))]
#[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))]
#[test_case(Rule::ClassAsDataStructure, Path::new("class_as_data_structure.py"))]
@@ -174,4 +177,23 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))]
#[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))]
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -1,7 +1,7 @@
use std::fmt;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr, WithItem};
use ruff_python_ast::{self as ast, Arguments, Expr, WithItem};
use ruff_text_size::Ranged;
use crate::Violation;
@@ -56,6 +56,48 @@ impl fmt::Display for ExceptionKind {
}
}
fn detect_blind_exception(
semantic: &ruff_python_semantic::SemanticModel<'_>,
func: &Expr,
arguments: &Arguments,
) -> Option<ExceptionKind> {
let is_assert_raises = matches!(
func,
&Expr::Attribute(ast::ExprAttribute { ref attr, .. }) if attr.as_str() == "assertRaises"
);
let is_pytest_raises = semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"]));
if !(is_assert_raises || is_pytest_raises) {
return None;
}
if is_pytest_raises {
if arguments.find_keyword("match").is_some() {
return None;
}
if arguments
.find_positional(1)
.is_some_and(|arg| matches!(arg, Expr::StringLiteral(_) | Expr::BytesLiteral(_)))
{
return None;
}
}
let first_arg = arguments.args.first()?;
let builtin_symbol = semantic.resolve_builtin_symbol(first_arg)?;
match builtin_symbol {
"Exception" => Some(ExceptionKind::Exception),
"BaseException" => Some(ExceptionKind::BaseException),
_ => None,
}
}
/// B017
pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) {
for item in items {
@@ -73,33 +115,31 @@ pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) {
continue;
}
let [arg] = &*arguments.args else {
continue;
};
let semantic = checker.semantic();
let Some(builtin_symbol) = semantic.resolve_builtin_symbol(arg) else {
continue;
};
let exception = match builtin_symbol {
"Exception" => ExceptionKind::Exception,
"BaseException" => ExceptionKind::BaseException,
_ => continue,
};
if !(matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
|| semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["pytest", "raises"])
})
&& arguments.find_keyword("match").is_none())
if let Some(exception) =
detect_blind_exception(checker.semantic(), func.as_ref(), arguments)
{
continue;
checker.report_diagnostic(AssertRaisesException { exception }, item.range());
}
checker.report_diagnostic(AssertRaisesException { exception }, item.range());
}
}
/// B017 (call form)
pub(crate) fn assert_raises_exception_call(
checker: &Checker,
ast::ExprCall {
func,
arguments,
range,
node_index: _,
}: &ast::ExprCall,
) {
let semantic = checker.semantic();
if arguments.args.len() < 2 && arguments.find_argument("func", 1).is_none() {
return;
}
if let Some(exception) = detect_blind_exception(semantic, func.as_ref(), arguments) {
checker.report_diagnostic(AssertRaisesException { exception }, *range);
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B017.py:23:14: B017 Do not assert blind exception: `Exception`
B017_0.py:23:14: B017 Do not assert blind exception: `Exception`
|
21 | class Foobar(unittest.TestCase):
22 | def evil_raises(self) -> None:
@@ -10,7 +10,7 @@ B017.py:23:14: B017 Do not assert blind exception: `Exception`
24 | raise Exception("Evil I say!")
|
B017.py:27:14: B017 Do not assert blind exception: `BaseException`
B017_0.py:27:14: B017 Do not assert blind exception: `BaseException`
|
26 | def also_evil_raises(self) -> None:
27 | with self.assertRaises(BaseException):
@@ -18,7 +18,7 @@ B017.py:27:14: B017 Do not assert blind exception: `BaseException`
28 | raise Exception("Evil I say!")
|
B017.py:45:10: B017 Do not assert blind exception: `Exception`
B017_0.py:45:10: B017 Do not assert blind exception: `Exception`
|
44 | def test_pytest_raises():
45 | with pytest.raises(Exception):
@@ -26,7 +26,7 @@ B017.py:45:10: B017 Do not assert blind exception: `Exception`
46 | raise ValueError("Hello")
|
B017.py:48:10: B017 Do not assert blind exception: `Exception`
B017_0.py:48:10: B017 Do not assert blind exception: `Exception`
|
46 | raise ValueError("Hello")
47 |
@@ -35,7 +35,7 @@ B017.py:48:10: B017 Do not assert blind exception: `Exception`
49 | raise ValueError("Hello")
|
B017.py:57:36: B017 Do not assert blind exception: `Exception`
B017_0.py:57:36: B017 Do not assert blind exception: `Exception`
|
55 | raise ValueError("This is also fine")
56 |

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---

View File

@@ -0,0 +1,45 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B017_0.py:23:14: B017 Do not assert blind exception: `Exception`
|
21 | class Foobar(unittest.TestCase):
22 | def evil_raises(self) -> None:
23 | with self.assertRaises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
24 | raise Exception("Evil I say!")
|
B017_0.py:27:14: B017 Do not assert blind exception: `BaseException`
|
26 | def also_evil_raises(self) -> None:
27 | with self.assertRaises(BaseException):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
28 | raise Exception("Evil I say!")
|
B017_0.py:45:10: B017 Do not assert blind exception: `Exception`
|
44 | def test_pytest_raises():
45 | with pytest.raises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
46 | raise ValueError("Hello")
|
B017_0.py:48:10: B017 Do not assert blind exception: `Exception`
|
46 | raise ValueError("Hello")
47 |
48 | with pytest.raises(Exception), pytest.raises(ValueError):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
49 | raise ValueError("Hello")
|
B017_0.py:57:36: B017 Do not assert blind exception: `Exception`
|
55 | raise ValueError("This is also fine")
56 |
57 | with contextlib.nullcontext(), pytest.raises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
58 | raise ValueError("Multiple context managers")
|

View File

@@ -0,0 +1,37 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B017_1.py:20:9: B017 Do not assert blind exception: `Exception`
|
18 | class Foobar(unittest.TestCase):
19 | def call_form_raises(self) -> None:
20 | self.assertRaises(Exception, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
21 | self.assertRaises(BaseException, something_else)
|
B017_1.py:21:9: B017 Do not assert blind exception: `BaseException`
|
19 | def call_form_raises(self) -> None:
20 | self.assertRaises(Exception, something_else)
21 | self.assertRaises(BaseException, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
B017_1.py:25:5: B017 Do not assert blind exception: `Exception`
|
24 | def test_pytest_call_form() -> None:
25 | pytest.raises(Exception, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
26 | pytest.raises(BaseException, something_else)
|
B017_1.py:26:5: B017 Do not assert blind exception: `BaseException`
|
24 | def test_pytest_call_form() -> None:
25 | pytest.raises(Exception, something_else)
26 | pytest.raises(BaseException, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
27 |
28 | pytest.raises(Exception, something_else, match="hello")
|

View File

@@ -539,7 +539,21 @@ fn implicit_return(checker: &Checker, function_def: &ast::StmtFunctionDef, stmt:
}
/// RET504
fn unnecessary_assign(checker: &Checker, stack: &Stack) {
pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) {
let Stmt::FunctionDef(function_def) = function_stmt else {
return;
};
let Some(stack) = create_stack(checker, function_def) else {
return;
};
if !result_exists(&stack.returns) {
return;
}
let Some(function_scope) = checker.semantic().function_scope(function_def) else {
return;
};
for (assign, return_, stmt) in &stack.assignment_return {
// Identify, e.g., `return x`.
let Some(value) = return_.value.as_ref() else {
@@ -583,6 +597,22 @@ fn unnecessary_assign(checker: &Checker, stack: &Stack) {
continue;
}
let Some(assigned_binding) = function_scope
.get(assigned_id)
.map(|binding_id| checker.semantic().binding(binding_id))
else {
continue;
};
// Check if there's any reference made to `assigned_binding` in another scope, e.g, nested
// functions. If there is, ignore them.
if assigned_binding
.references()
.map(|reference_id| checker.semantic().reference(reference_id))
.any(|reference| reference.scope_id() != assigned_binding.scope)
{
continue;
}
let mut diagnostic = checker.report_diagnostic(
UnnecessaryAssign {
name: assigned_id.to_string(),
@@ -665,24 +695,21 @@ fn superfluous_elif_else(checker: &Checker, stack: &Stack) {
}
}
/// Run all checks from the `flake8-return` plugin.
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
let ast::StmtFunctionDef {
decorator_list,
returns,
body,
..
} = function_def;
fn create_stack<'a>(
checker: &'a Checker,
function_def: &'a ast::StmtFunctionDef,
) -> Option<Stack<'a>> {
let ast::StmtFunctionDef { body, .. } = function_def;
// Find the last statement in the function.
let Some(last_stmt) = body.last() else {
// Skip empty functions.
return;
return None;
};
// Skip functions that consist of a single return statement.
if body.len() == 1 && matches!(last_stmt, Stmt::Return(_)) {
return;
return None;
}
// Traverse the function body, to collect the stack.
@@ -696,9 +723,29 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
// Avoid false positives for generators.
if stack.is_generator {
return;
return None;
}
Some(stack)
}
/// Run all checks from the `flake8-return` plugin, but `RET504` which is ran
/// after the semantic model is fully built.
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
let ast::StmtFunctionDef {
decorator_list,
returns,
body,
..
} = function_def;
let Some(stack) = create_stack(checker, function_def) else {
return;
};
let Some(last_stmt) = body.last() else {
return;
};
if checker.any_rule_enabled(&[
Rule::SuperfluousElseReturn,
Rule::SuperfluousElseRaise,
@@ -721,10 +768,6 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
if checker.is_rule_enabled(Rule::ImplicitReturn) {
implicit_return(checker, function_def, last_stmt);
}
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
unnecessary_assign(checker, &stack);
}
} else {
if checker.is_rule_enabled(Rule::UnnecessaryReturnNone) {
// Skip functions that have a return annotation that is not `None`.

View File

@@ -247,8 +247,6 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
422 | services = a["services"]
423 | return services
| ^^^^^^^^ RET504
424 |
425 | # See: https://github.com/astral-sh/ruff/issues/18411
|
= help: Remove unnecessary assignment
@@ -260,46 +258,46 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
423 |- return services
422 |+ return a["services"]
424 423 |
425 424 | # See: https://github.com/astral-sh/ruff/issues/18411
426 425 | def f():
425 424 |
426 425 | # See: https://github.com/astral-sh/ruff/issues/14052
RET504.py:429:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
RET504.py:458:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
427 | (#=
428 | x) = 1
429 | return x
456 | (#=
457 | x) = 1
458 | return x
| ^ RET504
430 |
431 | def f():
459 |
460 | def f():
|
= help: Remove unnecessary assignment
Unsafe fix
424 424 |
425 425 | # See: https://github.com/astral-sh/ruff/issues/18411
426 426 | def f():
427 |- (#=
428 |- x) = 1
429 |- return x
427 |+ return 1
430 428 |
431 429 | def f():
432 430 | x = (1
453 453 |
454 454 | # See: https://github.com/astral-sh/ruff/issues/18411
455 455 | def f():
456 |- (#=
457 |- x) = 1
458 |- return x
456 |+ return 1
459 457 |
460 458 | def f():
461 459 | x = (1
RET504.py:434:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
RET504.py:463:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
432 | x = (1
433 | )
434 | return x
461 | x = (1
462 | )
463 | return x
| ^ RET504
|
= help: Remove unnecessary assignment
Unsafe fix
429 429 | return x
430 430 |
431 431 | def f():
432 |- x = (1
432 |+ return (1
433 433 | )
434 |- return x
458 458 | return x
459 459 |
460 460 | def f():
461 |- x = (1
461 |+ return (1
462 462 | )
463 |- return x

View File

@@ -36,6 +36,7 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_8.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_9.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("whitespace.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_1.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_2.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TC001.py"))]

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
whitespace.py:5:26: TC004 [*] Move import `builtins` out of type-checking block. Import is used for more than type hinting.
|
3 | from typing import TYPE_CHECKING \
4 |
5 | if TYPE_CHECKING: import builtins
| ^^^^^^^^ TC004
6 | builtins.print("!")
|
= help: Move out of type-checking block
Unsafe fix
1 1 | # Regression test for: https://github.com/astral-sh/ruff/issues/19175
2 2 | # there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash
3 |-from typing import TYPE_CHECKING \
3 |+from typing import TYPE_CHECKING; import builtins \
4 4 |
5 |-if TYPE_CHECKING: import builtins
5 |+if TYPE_CHECKING: pass
6 6 | builtins.print("!")

View File

@@ -1,10 +1,17 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
use ruff_python_ast::{self as ast};
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::Ranged;
pub(crate) fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
arguments
.find_keyword(name)
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
}
pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool {
expr.as_call_expr().is_some_and(|expr_call| {
checker
.semantic()
@@ -13,27 +20,22 @@ pub(crate) fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
})
}
pub(crate) fn check_os_path_get_calls(
/// We check functions that take only 1 argument, this does not apply to functions
/// with `dir_fd` argument, because `dir_fd` is not supported by pathlib,
/// so check if it's set to non-default values
pub(crate) fn check_os_pathlib_single_arg_calls(
checker: &Checker,
call: &ExprCall,
fn_name: &str,
attr: &str,
fn_argument: &str,
fix_enabled: bool,
violation: impl Violation,
) {
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_none_or(|qualified_name| qualified_name.segments() != ["os", "path", fn_name])
{
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("filename", 0) else {
let Some(arg) = call.arguments.find_argument_value(fn_argument, 0) else {
return;
};
@@ -56,10 +58,10 @@ pub(crate) fn check_os_path_get_calls(
Applicability::Safe
};
let replacement = if is_path_call(checker, arg) {
format!("{arg_code}.stat().{attr}")
let replacement = if is_pathlib_path_call(checker, arg) {
format!("{arg_code}.{attr}")
} else {
format!("{binding}({arg_code}).stat().{attr}")
format!("{binding}({arg_code}).{attr}")
};
Ok(Fix::applicable_edits(

View File

@@ -80,6 +80,48 @@ mod tests {
Ok(())
}
#[test_case(Path::new("full_name.py"))]
#[test_case(Path::new("import_as.py"))]
#[test_case(Path::new("import_from_as.py"))]
#[test_case(Path::new("import_from.py"))]
fn preview_rules(path: &Path) -> Result<()> {
let snapshot = format!("preview_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_use_pathlib").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rules(vec![
Rule::OsPathAbspath,
Rule::OsChmod,
Rule::OsMkdir,
Rule::OsMakedirs,
Rule::OsRename,
Rule::OsReplace,
Rule::OsRmdir,
Rule::OsRemove,
Rule::OsUnlink,
Rule::OsGetcwd,
Rule::OsPathExists,
Rule::OsPathExpanduser,
Rule::OsPathIsdir,
Rule::OsPathIsfile,
Rule::OsPathIslink,
Rule::OsReadlink,
Rule::OsStat,
Rule::OsPathIsabs,
Rule::OsPathJoin,
Rule::OsPathBasename,
Rule::OsPathDirname,
Rule::OsPathSamefile,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
])
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]

View File

@@ -1,19 +1,45 @@
pub(crate) use glob_rule::*;
pub(crate) use invalid_pathlib_with_suffix::*;
pub(crate) use os_path_abspath::*;
pub(crate) use os_path_basename::*;
pub(crate) use os_path_dirname::*;
pub(crate) use os_path_exists::*;
pub(crate) use os_path_expanduser::*;
pub(crate) use os_path_getatime::*;
pub(crate) use os_path_getctime::*;
pub(crate) use os_path_getmtime::*;
pub(crate) use os_path_getsize::*;
pub(crate) use os_path_isabs::*;
pub(crate) use os_path_isdir::*;
pub(crate) use os_path_isfile::*;
pub(crate) use os_path_islink::*;
pub(crate) use os_readlink::*;
pub(crate) use os_remove::*;
pub(crate) use os_rmdir::*;
pub(crate) use os_sep_split::*;
pub(crate) use os_unlink::*;
pub(crate) use path_constructor_current_directory::*;
pub(crate) use replaceable_by_pathlib::*;
mod glob_rule;
mod invalid_pathlib_with_suffix;
mod os_path_abspath;
mod os_path_basename;
mod os_path_dirname;
mod os_path_exists;
mod os_path_expanduser;
mod os_path_getatime;
mod os_path_getctime;
mod os_path_getmtime;
mod os_path_getsize;
mod os_path_isabs;
mod os_path_isdir;
mod os_path_isfile;
mod os_path_islink;
mod os_readlink;
mod os_remove;
mod os_rmdir;
mod os_sep_split;
mod os_unlink;
mod path_constructor_current_directory;
mod replaceable_by_pathlib;

View File

@@ -0,0 +1,74 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_abspath_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.abspath`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.resolve()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.abspath()`).
///
/// ## Examples
/// ```python
/// import os
///
/// file_path = os.path.abspath("../path/to/file")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// file_path = Path("../path/to/file").resolve()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathAbspath;
impl Violation for OsPathAbspath {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.abspath()` should be replaced by `Path.resolve()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).resolve()`".to_string())
}
}
/// PTH100
pub(crate) fn os_path_abspath(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "abspath"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"resolve()",
"path",
is_fix_os_path_abspath_enabled(checker.settings()),
OsPathAbspath,
);
}

View File

@@ -0,0 +1,73 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_basename_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.basename`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.name` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.basename()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.basename(__file__)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(__file__).name
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
/// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathBasename;
impl Violation for OsPathBasename {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.basename()` should be replaced by `Path.name`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).name`".to_string())
}
}
/// PTH119
pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "basename"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"name",
"p",
is_fix_os_path_basename_enabled(checker.settings()),
OsPathBasename,
);
}

View File

@@ -0,0 +1,73 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_dirname_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.dirname`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.parent` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.dirname()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.dirname(__file__)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(__file__).parent
/// ```
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent)
/// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathDirname;
impl Violation for OsPathDirname {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.dirname()` should be replaced by `Path.parent`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).parent`".to_string())
}
}
/// PTH120
pub(crate) fn os_path_dirname(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "dirname"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"parent",
"p",
is_fix_os_path_dirname_enabled(checker.settings()),
OsPathDirname,
);
}

View File

@@ -0,0 +1,73 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_exists_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.exists`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.exists()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.exists()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.exists("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").exists()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists)
/// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathExists;
impl Violation for OsPathExists {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.exists()` should be replaced by `Path.exists()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).exists()`".to_string())
}
}
/// PTH110
pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "exists"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"exists()",
"path",
is_fix_os_path_exists_enabled(checker.settings()),
OsPathExists,
);
}

View File

@@ -0,0 +1,73 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_expanduser_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.expanduser`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.expanduser()` can improve readability over the `os.path`
/// module's counterparts (e.g., as `os.path.expanduser()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.expanduser("~/films/Monty Python")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("~/films/Monty Python").expanduser()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathExpanduser;
impl Violation for OsPathExpanduser {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.expanduser()` should be replaced by `Path.expanduser()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).expanduser()`".to_string())
}
}
/// PTH111
pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "expanduser"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"expanduser()",
"path",
is_fix_os_path_expanduser_enabled(checker.settings()),
OsPathExpanduser,
);
}

View File

@@ -1,6 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getatime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -61,12 +61,15 @@ impl Violation for OsPathGetatime {
}
/// PTH203
pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "getatime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"getatime",
"st_atime",
"stat().st_atime",
"filename",
is_fix_os_path_getatime_enabled(checker.settings()),
OsPathGetatime,
);

View File

@@ -1,6 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getctime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -62,12 +62,15 @@ impl Violation for OsPathGetctime {
}
/// PTH205
pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "getctime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"getctime",
"st_ctime",
"stat().st_ctime",
"filename",
is_fix_os_path_getctime_enabled(checker.settings()),
OsPathGetctime,
);

View File

@@ -1,6 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getmtime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -62,12 +62,15 @@ impl Violation for OsPathGetmtime {
}
/// PTH204
pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "getmtime"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"getmtime",
"st_mtime",
"stat().st_mtime",
"filename",
is_fix_os_path_getmtime_enabled(checker.settings()),
OsPathGetmtime,
);

View File

@@ -1,6 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getsize_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -62,12 +62,15 @@ impl Violation for OsPathGetsize {
}
/// PTH202
pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "getsize"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"getsize",
"st_size",
"stat().st_size",
"filename",
is_fix_os_path_getsize_enabled(checker.settings()),
OsPathGetsize,
);

View File

@@ -0,0 +1,72 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isabs_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isabs`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_absolute()` can improve readability over the `os.path`
/// module's counterparts (e.g., as `os.path.isabs()`).
///
/// ## Examples
/// ```python
/// import os
///
/// if os.path.isabs(file_name):
/// print("Absolute path!")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// if Path(file_name).is_absolute():
/// print("Absolute path!")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute)
/// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsabs;
impl Violation for OsPathIsabs {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isabs()` should be replaced by `Path.is_absolute()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).is_absolute()`".to_string())
}
}
/// PTH117
pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "isabs"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"is_absolute()",
"s",
is_fix_os_path_isabs_enabled(checker.settings()),
OsPathIsabs,
);
}

View File

@@ -0,0 +1,75 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isdir_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_dir()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.isdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.isdir("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_dir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir)
/// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsdir;
impl Violation for OsPathIsdir {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isdir()` should be replaced by `Path.is_dir()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).is_dir()`".to_string())
}
}
/// PTH112
pub(crate) fn os_path_isdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "isdir"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"is_dir()",
"s",
is_fix_os_path_isdir_enabled(checker.settings()),
OsPathIsdir,
);
}

View File

@@ -0,0 +1,75 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_isfile_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.isfile`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_file()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.isfile()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.isfile("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_file()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file)
/// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsfile;
impl Violation for OsPathIsfile {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isfile()` should be replaced by `Path.is_file()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).is_file()`".to_string())
}
}
/// PTH113
pub(crate) fn os_path_isfile(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "isfile"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"is_file()",
"path",
is_fix_os_path_isfile_enabled(checker.settings()),
OsPathIsfile,
);
}

View File

@@ -0,0 +1,75 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_islink_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.islink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_symlink()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.islink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.islink("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_symlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink)
/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIslink;
impl Violation for OsPathIslink {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.islink()` should be replaced by `Path.is_symlink()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).is_symlink()`".to_string())
}
}
/// PTH114
pub(crate) fn os_path_islink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "islink"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"is_symlink()",
"path",
is_fix_os_path_islink_enabled(checker.settings()),
OsPathIslink,
);
}

View File

@@ -0,0 +1,91 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_readlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ExprCall, PythonVersion};
/// ## What it does
/// Checks for uses of `os.readlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.readlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.readlink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.readlink(file_name)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(file_name).readlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline)
/// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsReadlink;
impl Violation for OsReadlink {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.readlink()` should be replaced by `Path.readlink()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).readlink()`".to_string())
}
}
/// PTH115
pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// Python 3.9+
if checker.target_version() < PythonVersion::PY39 {
return;
}
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.readlink)
// ```text
// 0 1
// os.readlink(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
if segments != ["os", "readlink"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"readlink()",
"path",
is_fix_os_readlink_enabled(checker.settings()),
OsReadlink,
);
}

View File

@@ -0,0 +1,86 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_remove_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.remove`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.unlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.remove()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.remove("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").unlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRemove;
impl Violation for OsRemove {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.remove()` should be replaced by `Path.unlink()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).unlink()`".to_string())
}
}
/// PTH107
pub(crate) fn os_remove(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.remove)
// ```text
// 0 1
// os.remove(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
if segments != ["os", "remove"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"unlink()",
"path",
is_fix_os_remove_enabled(checker.settings()),
OsRemove,
);
}

View File

@@ -0,0 +1,86 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_rmdir_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.rmdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.rmdir()` can improve readability over the `os`
/// module's counterparts (e.g., `os.rmdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.rmdir("folder/")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("folder/").rmdir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir)
/// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRmdir;
impl Violation for OsRmdir {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.rmdir()` should be replaced by `Path.rmdir()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).rmdir()`".to_string())
}
}
/// PTH106
pub(crate) fn os_rmdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rmdir)
// ```text
// 0 1
// os.rmdir(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
if segments != ["os", "rmdir"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"rmdir()",
"path",
is_fix_os_rmdir_enabled(checker.settings()),
OsRmdir,
);
}

View File

@@ -0,0 +1,86 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_unlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.unlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.unlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.unlink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.unlink("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").unlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsUnlink;
impl Violation for OsUnlink {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.unlink()` should be replaced by `Path.unlink()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).unlink()`".to_string())
}
}
/// PTH108
pub(crate) fn os_unlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.unlink)
// ```text
// 0 1
// os.unlink(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
if segments != ["os", "unlink"] {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"unlink()",
"path",
is_fix_os_unlink_enabled(checker.settings()),
OsUnlink,
);
}

View File

@@ -54,7 +54,11 @@ impl AlwaysFixableViolation for PathConstructorCurrentDirectory {
}
/// PTH201
pub(crate) fn path_constructor_current_directory(checker: &Checker, call: &ExprCall) {
pub(crate) fn path_constructor_current_directory(
checker: &Checker,
call: &ExprCall,
segments: &[&str],
) {
let applicability = |range| {
if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
@@ -63,15 +67,9 @@ pub(crate) fn path_constructor_current_directory(checker: &Checker, call: &ExprC
}
};
let (func, arguments) = (&call.func, &call.arguments);
let arguments = &call.arguments;
if !checker
.semantic()
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["pathlib", "Path" | "PurePath"])
})
{
if !matches!(segments, ["pathlib", "Path" | "PurePath"]) {
return;
}

View File

@@ -4,14 +4,12 @@ use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_use_pathlib::helpers::is_keyword_only_argument_non_default;
use crate::rules::flake8_use_pathlib::rules::Glob;
use crate::rules::flake8_use_pathlib::violations::{
BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathAbspath,
OsPathBasename, OsPathDirname, OsPathExists, OsPathExpanduser, OsPathIsabs, OsPathIsdir,
OsPathIsfile, OsPathIslink, OsPathJoin, OsPathSamefile, OsPathSplitext, OsReadlink, OsRemove,
OsRename, OsReplace, OsRmdir, OsStat, OsSymlink, OsUnlink, PyPath,
BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathJoin,
OsPathSamefile, OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath,
};
use ruff_python_ast::PythonVersion;
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
let Some(qualified_name) = checker.semantic().resolve_qualified_name(&call.func) else {
@@ -20,8 +18,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
let range = call.func.range();
match qualified_name.segments() {
// PTH100
["os", "path", "abspath"] => checker.report_diagnostic_if_enabled(OsPathAbspath, range),
// PTH101
["os", "chmod"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
@@ -87,60 +83,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
}
checker.report_diagnostic_if_enabled(OsReplace, range)
}
// PTH106
["os", "rmdir"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rmdir)
// ```text
// 0 1
// os.rmdir(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsRmdir, range)
}
// PTH107
["os", "remove"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.remove)
// ```text
// 0 1
// os.remove(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsRemove, range)
}
// PTH108
["os", "unlink"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.unlink)
// ```text
// 0 1
// os.unlink(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsUnlink, range)
}
// PTH109
["os", "getcwd"] => checker.report_diagnostic_if_enabled(OsGetcwd, range),
["os", "getcwdb"] => checker.report_diagnostic_if_enabled(OsGetcwd, range),
// PTH110
["os", "path", "exists"] => checker.report_diagnostic_if_enabled(OsPathExists, range),
// PTH111
["os", "path", "expanduser"] => {
checker.report_diagnostic_if_enabled(OsPathExpanduser, range)
}
// PTH112
["os", "path", "isdir"] => checker.report_diagnostic_if_enabled(OsPathIsdir, range),
// PTH113
["os", "path", "isfile"] => checker.report_diagnostic_if_enabled(OsPathIsfile, range),
// PTH114
["os", "path", "islink"] => checker.report_diagnostic_if_enabled(OsPathIslink, range),
// PTH116
["os", "stat"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
@@ -159,8 +105,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
}
checker.report_diagnostic_if_enabled(OsStat, range)
}
// PTH117
["os", "path", "isabs"] => checker.report_diagnostic_if_enabled(OsPathIsabs, range),
// PTH118
["os", "path", "join"] => checker.report_diagnostic_if_enabled(
OsPathJoin {
@@ -184,10 +128,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
},
range,
),
// PTH119
["os", "path", "basename"] => checker.report_diagnostic_if_enabled(OsPathBasename, range),
// PTH120
["os", "path", "dirname"] => checker.report_diagnostic_if_enabled(OsPathDirname, range),
// PTH121
["os", "path", "samefile"] => checker.report_diagnostic_if_enabled(OsPathSamefile, range),
// PTH122
@@ -208,7 +148,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
// PTH123
["" | "builtins", "open"] => {
// `closefd` and `opener` are not supported by pathlib, so check if they are
// `closefd` and `opener` are not supported by pathlib, so check if they
// are set to non-default values.
// https://github.com/astral-sh/ruff/issues/7620
// Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open):
@@ -282,20 +222,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
range,
)
}
// PTH115
// Python 3.9+
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.readlink)
// ```text
// 0 1
// os.readlink(path, *, dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") {
return;
}
checker.report_diagnostic_if_enabled(OsReadlink, range)
}
// PTH208
["os", "listdir"] => {
if call
@@ -338,7 +264,7 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
match expr {
Expr::Name(name) => Some(name),
Expr::Call(ast::ExprCall { func, .. }) => get_name_expr(func),
Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
_ => None,
}
}
@@ -349,9 +275,3 @@ fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usi
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())
}
fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
arguments
.find_keyword(name)
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
}

View File

@@ -10,6 +10,7 @@ full_name.py:7:5: PTH100 `os.path.abspath()` should be replaced by `Path.resolve
8 | aa = os.chmod(p)
9 | aaa = os.mkdir(p)
|
= help: Replace with `Path(...).resolve()`
full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
@@ -69,6 +70,7 @@ full_name.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
14 | os.remove(p)
15 | os.unlink(p)
|
= help: Replace with `Path(...).rmdir()`
full_name.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
|
@@ -79,6 +81,7 @@ full_name.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
15 | os.unlink(p)
16 | os.getcwd(p)
|
= help: Replace with `Path(...).unlink()`
full_name.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
|
@@ -89,6 +92,7 @@ full_name.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
16 | os.getcwd(p)
17 | b = os.path.exists(p)
|
= help: Replace with `Path(...).unlink()`
full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
@@ -109,6 +113,7 @@ full_name.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists(
18 | bb = os.path.expanduser(p)
19 | bbb = os.path.isdir(p)
|
= help: Replace with `Path(...).exists()`
full_name.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
@@ -119,6 +124,7 @@ full_name.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.exp
19 | bbb = os.path.isdir(p)
20 | bbbb = os.path.isfile(p)
|
= help: Replace with `Path(...).expanduser()`
full_name.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()`
|
@@ -129,6 +135,7 @@ full_name.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()
20 | bbbb = os.path.isfile(p)
21 | bbbbb = os.path.islink(p)
|
= help: Replace with `Path(...).is_dir()`
full_name.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()`
|
@@ -139,6 +146,7 @@ full_name.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file
21 | bbbbb = os.path.islink(p)
22 | os.readlink(p)
|
= help: Replace with `Path(...).is_file()`
full_name.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()`
|
@@ -149,6 +157,7 @@ full_name.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_syml
22 | os.readlink(p)
23 | os.stat(p)
|
= help: Replace with `Path(...).is_symlink()`
full_name.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()`
|
@@ -159,6 +168,7 @@ full_name.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()
23 | os.stat(p)
24 | os.path.isabs(p)
|
= help: Replace with `Path(...).readlink()`
full_name.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
@@ -179,6 +189,7 @@ full_name.py:24:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_absol
25 | os.path.join(p, q)
26 | os.sep.join([p, q])
|
= help: Replace with `Path(...).is_absolute()`
full_name.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
@@ -219,6 +230,7 @@ full_name.py:28:1: PTH119 `os.path.basename()` should be replaced by `Path.name`
29 | os.path.dirname(p)
30 | os.path.samefile(p)
|
= help: Replace with `Path(...).name`
full_name.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent`
|
@@ -229,6 +241,7 @@ full_name.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent
30 | os.path.samefile(p)
31 | os.path.splitext(p)
|
= help: Replace with `Path(...).parent`
full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|

View File

@@ -10,6 +10,7 @@ import_as.py:7:5: PTH100 `os.path.abspath()` should be replaced by `Path.resolve
8 | aa = foo.chmod(p)
9 | aaa = foo.mkdir(p)
|
= help: Replace with `Path(...).resolve()`
import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
@@ -69,6 +70,7 @@ import_as.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
14 | foo.remove(p)
15 | foo.unlink(p)
|
= help: Replace with `Path(...).rmdir()`
import_as.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
|
@@ -79,6 +81,7 @@ import_as.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
15 | foo.unlink(p)
16 | foo.getcwd(p)
|
= help: Replace with `Path(...).unlink()`
import_as.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
|
@@ -89,6 +92,7 @@ import_as.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
16 | foo.getcwd(p)
17 | b = foo_p.exists(p)
|
= help: Replace with `Path(...).unlink()`
import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
@@ -109,6 +113,7 @@ import_as.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists(
18 | bb = foo_p.expanduser(p)
19 | bbb = foo_p.isdir(p)
|
= help: Replace with `Path(...).exists()`
import_as.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
@@ -119,6 +124,7 @@ import_as.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.exp
19 | bbb = foo_p.isdir(p)
20 | bbbb = foo_p.isfile(p)
|
= help: Replace with `Path(...).expanduser()`
import_as.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()`
|
@@ -129,6 +135,7 @@ import_as.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()
20 | bbbb = foo_p.isfile(p)
21 | bbbbb = foo_p.islink(p)
|
= help: Replace with `Path(...).is_dir()`
import_as.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()`
|
@@ -139,6 +146,7 @@ import_as.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file
21 | bbbbb = foo_p.islink(p)
22 | foo.readlink(p)
|
= help: Replace with `Path(...).is_file()`
import_as.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()`
|
@@ -149,6 +157,7 @@ import_as.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_syml
22 | foo.readlink(p)
23 | foo.stat(p)
|
= help: Replace with `Path(...).is_symlink()`
import_as.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()`
|
@@ -159,6 +168,7 @@ import_as.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()
23 | foo.stat(p)
24 | foo_p.isabs(p)
|
= help: Replace with `Path(...).readlink()`
import_as.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
@@ -179,6 +189,7 @@ import_as.py:24:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_absol
25 | foo_p.join(p, q)
26 | foo.sep.join([p, q])
|
= help: Replace with `Path(...).is_absolute()`
import_as.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
@@ -219,6 +230,7 @@ import_as.py:28:1: PTH119 `os.path.basename()` should be replaced by `Path.name`
29 | foo_p.dirname(p)
30 | foo_p.samefile(p)
|
= help: Replace with `Path(...).name`
import_as.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent`
|
@@ -229,6 +241,7 @@ import_as.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent
30 | foo_p.samefile(p)
31 | foo_p.splitext(p)
|
= help: Replace with `Path(...).parent`
import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|

View File

@@ -10,6 +10,7 @@ import_from.py:9:5: PTH100 `os.path.abspath()` should be replaced by `Path.resol
10 | aa = chmod(p)
11 | aaa = mkdir(p)
|
= help: Replace with `Path(...).resolve()`
import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
@@ -69,6 +70,7 @@ import_from.py:15:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
16 | remove(p)
17 | unlink(p)
|
= help: Replace with `Path(...).rmdir()`
import_from.py:16:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
|
@@ -79,6 +81,7 @@ import_from.py:16:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
17 | unlink(p)
18 | getcwd(p)
|
= help: Replace with `Path(...).unlink()`
import_from.py:17:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
|
@@ -89,6 +92,7 @@ import_from.py:17:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
18 | getcwd(p)
19 | b = exists(p)
|
= help: Replace with `Path(...).unlink()`
import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
@@ -109,6 +113,7 @@ import_from.py:19:5: PTH110 `os.path.exists()` should be replaced by `Path.exist
20 | bb = expanduser(p)
21 | bbb = isdir(p)
|
= help: Replace with `Path(...).exists()`
import_from.py:20:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
@@ -119,6 +124,7 @@ import_from.py:20:6: PTH111 `os.path.expanduser()` should be replaced by `Path.e
21 | bbb = isdir(p)
22 | bbbb = isfile(p)
|
= help: Replace with `Path(...).expanduser()`
import_from.py:21:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()`
|
@@ -129,6 +135,7 @@ import_from.py:21:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir
22 | bbbb = isfile(p)
23 | bbbbb = islink(p)
|
= help: Replace with `Path(...).is_dir()`
import_from.py:22:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()`
|
@@ -139,6 +146,7 @@ import_from.py:22:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_fi
23 | bbbbb = islink(p)
24 | readlink(p)
|
= help: Replace with `Path(...).is_file()`
import_from.py:23:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()`
|
@@ -149,6 +157,7 @@ import_from.py:23:9: PTH114 `os.path.islink()` should be replaced by `Path.is_sy
24 | readlink(p)
25 | stat(p)
|
= help: Replace with `Path(...).is_symlink()`
import_from.py:24:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()`
|
@@ -159,6 +168,7 @@ import_from.py:24:1: PTH115 `os.readlink()` should be replaced by `Path.readlink
25 | stat(p)
26 | isabs(p)
|
= help: Replace with `Path(...).readlink()`
import_from.py:25:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
@@ -179,6 +189,7 @@ import_from.py:26:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_abs
27 | join(p, q)
28 | sep.join((p, q))
|
= help: Replace with `Path(...).is_absolute()`
import_from.py:27:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
@@ -219,6 +230,7 @@ import_from.py:30:1: PTH119 `os.path.basename()` should be replaced by `Path.nam
31 | dirname(p)
32 | samefile(p)
|
= help: Replace with `Path(...).name`
import_from.py:31:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent`
|
@@ -229,6 +241,7 @@ import_from.py:31:1: PTH120 `os.path.dirname()` should be replaced by `Path.pare
32 | samefile(p)
33 | splitext(p)
|
= help: Replace with `Path(...).parent`
import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|

View File

@@ -10,6 +10,7 @@ import_from_as.py:14:5: PTH100 `os.path.abspath()` should be replaced by `Path.r
15 | aa = xchmod(p)
16 | aaa = xmkdir(p)
|
= help: Replace with `Path(...).resolve()`
import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
@@ -69,6 +70,7 @@ import_from_as.py:20:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
21 | xremove(p)
22 | xunlink(p)
|
= help: Replace with `Path(...).rmdir()`
import_from_as.py:21:1: PTH107 `os.remove()` should be replaced by `Path.unlink()`
|
@@ -79,6 +81,7 @@ import_from_as.py:21:1: PTH107 `os.remove()` should be replaced by `Path.unlink(
22 | xunlink(p)
23 | xgetcwd(p)
|
= help: Replace with `Path(...).unlink()`
import_from_as.py:22:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()`
|
@@ -89,6 +92,7 @@ import_from_as.py:22:1: PTH108 `os.unlink()` should be replaced by `Path.unlink(
23 | xgetcwd(p)
24 | b = xexists(p)
|
= help: Replace with `Path(...).unlink()`
import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
@@ -109,6 +113,7 @@ import_from_as.py:24:5: PTH110 `os.path.exists()` should be replaced by `Path.ex
25 | bb = xexpanduser(p)
26 | bbb = xisdir(p)
|
= help: Replace with `Path(...).exists()`
import_from_as.py:25:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
@@ -119,6 +124,7 @@ import_from_as.py:25:6: PTH111 `os.path.expanduser()` should be replaced by `Pat
26 | bbb = xisdir(p)
27 | bbbb = xisfile(p)
|
= help: Replace with `Path(...).expanduser()`
import_from_as.py:26:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()`
|
@@ -129,6 +135,7 @@ import_from_as.py:26:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_
27 | bbbb = xisfile(p)
28 | bbbbb = xislink(p)
|
= help: Replace with `Path(...).is_dir()`
import_from_as.py:27:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()`
|
@@ -139,6 +146,7 @@ import_from_as.py:27:8: PTH113 `os.path.isfile()` should be replaced by `Path.is
28 | bbbbb = xislink(p)
29 | xreadlink(p)
|
= help: Replace with `Path(...).is_file()`
import_from_as.py:28:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()`
|
@@ -149,6 +157,7 @@ import_from_as.py:28:9: PTH114 `os.path.islink()` should be replaced by `Path.is
29 | xreadlink(p)
30 | xstat(p)
|
= help: Replace with `Path(...).is_symlink()`
import_from_as.py:29:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()`
|
@@ -159,6 +168,7 @@ import_from_as.py:29:1: PTH115 `os.readlink()` should be replaced by `Path.readl
30 | xstat(p)
31 | xisabs(p)
|
= help: Replace with `Path(...).readlink()`
import_from_as.py:30:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
@@ -179,6 +189,7 @@ import_from_as.py:31:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_
32 | xjoin(p, q)
33 | s.join((p, q))
|
= help: Replace with `Path(...).is_absolute()`
import_from_as.py:32:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
@@ -219,6 +230,7 @@ import_from_as.py:35:1: PTH119 `os.path.basename()` should be replaced by `Path.
36 | xdirname(p)
37 | xsamefile(p)
|
= help: Replace with `Path(...).name`
import_from_as.py:36:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent`
|
@@ -229,6 +241,7 @@ import_from_as.py:36:1: PTH120 `os.path.dirname()` should be replaced by `Path.p
37 | xsamefile(p)
38 | xsplitext(p)
|
= help: Replace with `Path(...).parent`
import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|

View File

@@ -0,0 +1,580 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
full_name.py:7:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
5 | q = "bar"
6 |
7 | a = os.path.abspath(p)
| ^^^^^^^^^^^^^^^ PTH100
8 | aa = os.chmod(p)
9 | aaa = os.mkdir(p)
|
= help: Replace with `Path(...).resolve()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
6 7 |
7 |-a = os.path.abspath(p)
8 |+a = pathlib.Path(p).resolve()
8 9 | aa = os.chmod(p)
9 10 | aaa = os.mkdir(p)
10 11 | os.makedirs(p)
full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
7 | a = os.path.abspath(p)
8 | aa = os.chmod(p)
| ^^^^^^^^ PTH101
9 | aaa = os.mkdir(p)
10 | os.makedirs(p)
|
full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
7 | a = os.path.abspath(p)
8 | aa = os.chmod(p)
9 | aaa = os.mkdir(p)
| ^^^^^^^^ PTH102
10 | os.makedirs(p)
11 | os.rename(p)
|
full_name.py:10:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
8 | aa = os.chmod(p)
9 | aaa = os.mkdir(p)
10 | os.makedirs(p)
| ^^^^^^^^^^^ PTH103
11 | os.rename(p)
12 | os.replace(p)
|
full_name.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
9 | aaa = os.mkdir(p)
10 | os.makedirs(p)
11 | os.rename(p)
| ^^^^^^^^^ PTH104
12 | os.replace(p)
13 | os.rmdir(p)
|
full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
10 | os.makedirs(p)
11 | os.rename(p)
12 | os.replace(p)
| ^^^^^^^^^^ PTH105
13 | os.rmdir(p)
14 | os.remove(p)
|
full_name.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
11 | os.rename(p)
12 | os.replace(p)
13 | os.rmdir(p)
| ^^^^^^^^ PTH106
14 | os.remove(p)
15 | os.unlink(p)
|
= help: Replace with `Path(...).rmdir()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
10 11 | os.makedirs(p)
11 12 | os.rename(p)
12 13 | os.replace(p)
13 |-os.rmdir(p)
14 |+pathlib.Path(p).rmdir()
14 15 | os.remove(p)
15 16 | os.unlink(p)
16 17 | os.getcwd(p)
full_name.py:14:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()`
|
12 | os.replace(p)
13 | os.rmdir(p)
14 | os.remove(p)
| ^^^^^^^^^ PTH107
15 | os.unlink(p)
16 | os.getcwd(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
11 12 | os.rename(p)
12 13 | os.replace(p)
13 14 | os.rmdir(p)
14 |-os.remove(p)
15 |+pathlib.Path(p).unlink()
15 16 | os.unlink(p)
16 17 | os.getcwd(p)
17 18 | b = os.path.exists(p)
full_name.py:15:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()`
|
13 | os.rmdir(p)
14 | os.remove(p)
15 | os.unlink(p)
| ^^^^^^^^^ PTH108
16 | os.getcwd(p)
17 | b = os.path.exists(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
12 13 | os.replace(p)
13 14 | os.rmdir(p)
14 15 | os.remove(p)
15 |-os.unlink(p)
16 |+pathlib.Path(p).unlink()
16 17 | os.getcwd(p)
17 18 | b = os.path.exists(p)
18 19 | bb = os.path.expanduser(p)
full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
14 | os.remove(p)
15 | os.unlink(p)
16 | os.getcwd(p)
| ^^^^^^^^^ PTH109
17 | b = os.path.exists(p)
18 | bb = os.path.expanduser(p)
|
full_name.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
15 | os.unlink(p)
16 | os.getcwd(p)
17 | b = os.path.exists(p)
| ^^^^^^^^^^^^^^ PTH110
18 | bb = os.path.expanduser(p)
19 | bbb = os.path.isdir(p)
|
= help: Replace with `Path(...).exists()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
14 15 | os.remove(p)
15 16 | os.unlink(p)
16 17 | os.getcwd(p)
17 |-b = os.path.exists(p)
18 |+b = pathlib.Path(p).exists()
18 19 | bb = os.path.expanduser(p)
19 20 | bbb = os.path.isdir(p)
20 21 | bbbb = os.path.isfile(p)
full_name.py:18:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
16 | os.getcwd(p)
17 | b = os.path.exists(p)
18 | bb = os.path.expanduser(p)
| ^^^^^^^^^^^^^^^^^^ PTH111
19 | bbb = os.path.isdir(p)
20 | bbbb = os.path.isfile(p)
|
= help: Replace with `Path(...).expanduser()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
15 16 | os.unlink(p)
16 17 | os.getcwd(p)
17 18 | b = os.path.exists(p)
18 |-bb = os.path.expanduser(p)
19 |+bb = pathlib.Path(p).expanduser()
19 20 | bbb = os.path.isdir(p)
20 21 | bbbb = os.path.isfile(p)
21 22 | bbbbb = os.path.islink(p)
full_name.py:19:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
|
17 | b = os.path.exists(p)
18 | bb = os.path.expanduser(p)
19 | bbb = os.path.isdir(p)
| ^^^^^^^^^^^^^ PTH112
20 | bbbb = os.path.isfile(p)
21 | bbbbb = os.path.islink(p)
|
= help: Replace with `Path(...).is_dir()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
16 17 | os.getcwd(p)
17 18 | b = os.path.exists(p)
18 19 | bb = os.path.expanduser(p)
19 |-bbb = os.path.isdir(p)
20 |+bbb = pathlib.Path(p).is_dir()
20 21 | bbbb = os.path.isfile(p)
21 22 | bbbbb = os.path.islink(p)
22 23 | os.readlink(p)
full_name.py:20:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()`
|
18 | bb = os.path.expanduser(p)
19 | bbb = os.path.isdir(p)
20 | bbbb = os.path.isfile(p)
| ^^^^^^^^^^^^^^ PTH113
21 | bbbbb = os.path.islink(p)
22 | os.readlink(p)
|
= help: Replace with `Path(...).is_file()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
17 18 | b = os.path.exists(p)
18 19 | bb = os.path.expanduser(p)
19 20 | bbb = os.path.isdir(p)
20 |-bbbb = os.path.isfile(p)
21 |+bbbb = pathlib.Path(p).is_file()
21 22 | bbbbb = os.path.islink(p)
22 23 | os.readlink(p)
23 24 | os.stat(p)
full_name.py:21:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()`
|
19 | bbb = os.path.isdir(p)
20 | bbbb = os.path.isfile(p)
21 | bbbbb = os.path.islink(p)
| ^^^^^^^^^^^^^^ PTH114
22 | os.readlink(p)
23 | os.stat(p)
|
= help: Replace with `Path(...).is_symlink()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
18 19 | bb = os.path.expanduser(p)
19 20 | bbb = os.path.isdir(p)
20 21 | bbbb = os.path.isfile(p)
21 |-bbbbb = os.path.islink(p)
22 |+bbbbb = pathlib.Path(p).is_symlink()
22 23 | os.readlink(p)
23 24 | os.stat(p)
24 25 | os.path.isabs(p)
full_name.py:22:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
|
20 | bbbb = os.path.isfile(p)
21 | bbbbb = os.path.islink(p)
22 | os.readlink(p)
| ^^^^^^^^^^^ PTH115
23 | os.stat(p)
24 | os.path.isabs(p)
|
= help: Replace with `Path(...).readlink()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
19 20 | bbb = os.path.isdir(p)
20 21 | bbbb = os.path.isfile(p)
21 22 | bbbbb = os.path.islink(p)
22 |-os.readlink(p)
23 |+pathlib.Path(p).readlink()
23 24 | os.stat(p)
24 25 | os.path.isabs(p)
25 26 | os.path.join(p, q)
full_name.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
21 | bbbbb = os.path.islink(p)
22 | os.readlink(p)
23 | os.stat(p)
| ^^^^^^^ PTH116
24 | os.path.isabs(p)
25 | os.path.join(p, q)
|
full_name.py:24:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()`
|
22 | os.readlink(p)
23 | os.stat(p)
24 | os.path.isabs(p)
| ^^^^^^^^^^^^^ PTH117
25 | os.path.join(p, q)
26 | os.sep.join([p, q])
|
= help: Replace with `Path(...).is_absolute()`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
21 22 | bbbbb = os.path.islink(p)
22 23 | os.readlink(p)
23 24 | os.stat(p)
24 |-os.path.isabs(p)
25 |+pathlib.Path(p).is_absolute()
25 26 | os.path.join(p, q)
26 27 | os.sep.join([p, q])
27 28 | os.sep.join((p, q))
full_name.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
23 | os.stat(p)
24 | os.path.isabs(p)
25 | os.path.join(p, q)
| ^^^^^^^^^^^^ PTH118
26 | os.sep.join([p, q])
27 | os.sep.join((p, q))
|
full_name.py:26:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
24 | os.path.isabs(p)
25 | os.path.join(p, q)
26 | os.sep.join([p, q])
| ^^^^^^^^^^^ PTH118
27 | os.sep.join((p, q))
28 | os.path.basename(p)
|
full_name.py:27:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
25 | os.path.join(p, q)
26 | os.sep.join([p, q])
27 | os.sep.join((p, q))
| ^^^^^^^^^^^ PTH118
28 | os.path.basename(p)
29 | os.path.dirname(p)
|
full_name.py:28:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name`
|
26 | os.sep.join([p, q])
27 | os.sep.join((p, q))
28 | os.path.basename(p)
| ^^^^^^^^^^^^^^^^ PTH119
29 | os.path.dirname(p)
30 | os.path.samefile(p)
|
= help: Replace with `Path(...).name`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
25 26 | os.path.join(p, q)
26 27 | os.sep.join([p, q])
27 28 | os.sep.join((p, q))
28 |-os.path.basename(p)
29 |+pathlib.Path(p).name
29 30 | os.path.dirname(p)
30 31 | os.path.samefile(p)
31 32 | os.path.splitext(p)
full_name.py:29:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
|
27 | os.sep.join((p, q))
28 | os.path.basename(p)
29 | os.path.dirname(p)
| ^^^^^^^^^^^^^^^ PTH120
30 | os.path.samefile(p)
31 | os.path.splitext(p)
|
= help: Replace with `Path(...).parent`
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
26 27 | os.sep.join([p, q])
27 28 | os.sep.join((p, q))
28 29 | os.path.basename(p)
29 |-os.path.dirname(p)
30 |+pathlib.Path(p).parent
30 31 | os.path.samefile(p)
31 32 | os.path.splitext(p)
32 33 | with open(p) as fp:
full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
28 | os.path.basename(p)
29 | os.path.dirname(p)
30 | os.path.samefile(p)
| ^^^^^^^^^^^^^^^^ PTH121
31 | os.path.splitext(p)
32 | with open(p) as fp:
|
full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
29 | os.path.dirname(p)
30 | os.path.samefile(p)
31 | os.path.splitext(p)
| ^^^^^^^^^^^^^^^^ PTH122
32 | with open(p) as fp:
33 | fp.read()
|
full_name.py:32:6: PTH123 `open()` should be replaced by `Path.open()`
|
30 | os.path.samefile(p)
31 | os.path.splitext(p)
32 | with open(p) as fp:
| ^^^^ PTH123
33 | fp.read()
34 | open(p).close()
|
full_name.py:34:1: PTH123 `open()` should be replaced by `Path.open()`
|
32 | with open(p) as fp:
33 | fp.read()
34 | open(p).close()
| ^^^^ PTH123
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
|
full_name.py:35:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
33 | fp.read()
34 | open(p).close()
35 | os.getcwdb(p)
| ^^^^^^^^^^ PTH109
36 | os.path.join(p, *q)
37 | os.sep.join(p, *q)
|
full_name.py:36:1: PTH118 `os.path.join()` should be replaced by `Path.joinpath()`
|
34 | open(p).close()
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
| ^^^^^^^^^^^^ PTH118
37 | os.sep.join(p, *q)
|
full_name.py:37:1: PTH118 `os.sep.join()` should be replaced by `Path.joinpath()`
|
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
37 | os.sep.join(p, *q)
| ^^^^^^^^^^^ PTH118
38 |
39 | # https://github.com/astral-sh/ruff/issues/7620
|
full_name.py:46:1: PTH123 `open()` should be replaced by `Path.open()`
|
44 | open(p, closefd=False)
45 | open(p, opener=opener)
46 | open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
| ^^^^ PTH123
47 | open(p, 'r', - 1, None, None, None, True, None)
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
full_name.py:47:1: PTH123 `open()` should be replaced by `Path.open()`
|
45 | open(p, opener=opener)
46 | open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
47 | open(p, 'r', - 1, None, None, None, True, None)
| ^^^^ PTH123
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
full_name.py:65:1: PTH123 `open()` should be replaced by `Path.open()`
|
63 | open(f())
64 |
65 | open(b"foo")
| ^^^^ PTH123
66 | byte_str = b"bar"
67 | open(byte_str)
|
full_name.py:67:1: PTH123 `open()` should be replaced by `Path.open()`
|
65 | open(b"foo")
66 | byte_str = b"bar"
67 | open(byte_str)
| ^^^^ PTH123
68 |
69 | def bytes_str_func() -> bytes:
|
full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()`
|
69 | def bytes_str_func() -> bytes:
70 | return b"foo"
71 | open(bytes_str_func())
| ^^^^ PTH123
72 |
73 | # https://github.com/astral-sh/ruff/issues/17693
|

View File

@@ -0,0 +1,478 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
import_as.py:7:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
5 | q = "bar"
6 |
7 | a = foo_p.abspath(p)
| ^^^^^^^^^^^^^ PTH100
8 | aa = foo.chmod(p)
9 | aaa = foo.mkdir(p)
|
= help: Replace with `Path(...).resolve()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
6 7 |
7 |-a = foo_p.abspath(p)
8 |+a = pathlib.Path(p).resolve()
8 9 | aa = foo.chmod(p)
9 10 | aaa = foo.mkdir(p)
10 11 | foo.makedirs(p)
import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
7 | a = foo_p.abspath(p)
8 | aa = foo.chmod(p)
| ^^^^^^^^^ PTH101
9 | aaa = foo.mkdir(p)
10 | foo.makedirs(p)
|
import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
7 | a = foo_p.abspath(p)
8 | aa = foo.chmod(p)
9 | aaa = foo.mkdir(p)
| ^^^^^^^^^ PTH102
10 | foo.makedirs(p)
11 | foo.rename(p)
|
import_as.py:10:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
8 | aa = foo.chmod(p)
9 | aaa = foo.mkdir(p)
10 | foo.makedirs(p)
| ^^^^^^^^^^^^ PTH103
11 | foo.rename(p)
12 | foo.replace(p)
|
import_as.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
9 | aaa = foo.mkdir(p)
10 | foo.makedirs(p)
11 | foo.rename(p)
| ^^^^^^^^^^ PTH104
12 | foo.replace(p)
13 | foo.rmdir(p)
|
import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
10 | foo.makedirs(p)
11 | foo.rename(p)
12 | foo.replace(p)
| ^^^^^^^^^^^ PTH105
13 | foo.rmdir(p)
14 | foo.remove(p)
|
import_as.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
11 | foo.rename(p)
12 | foo.replace(p)
13 | foo.rmdir(p)
| ^^^^^^^^^ PTH106
14 | foo.remove(p)
15 | foo.unlink(p)
|
= help: Replace with `Path(...).rmdir()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
10 11 | foo.makedirs(p)
11 12 | foo.rename(p)
12 13 | foo.replace(p)
13 |-foo.rmdir(p)
14 |+pathlib.Path(p).rmdir()
14 15 | foo.remove(p)
15 16 | foo.unlink(p)
16 17 | foo.getcwd(p)
import_as.py:14:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()`
|
12 | foo.replace(p)
13 | foo.rmdir(p)
14 | foo.remove(p)
| ^^^^^^^^^^ PTH107
15 | foo.unlink(p)
16 | foo.getcwd(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
11 12 | foo.rename(p)
12 13 | foo.replace(p)
13 14 | foo.rmdir(p)
14 |-foo.remove(p)
15 |+pathlib.Path(p).unlink()
15 16 | foo.unlink(p)
16 17 | foo.getcwd(p)
17 18 | b = foo_p.exists(p)
import_as.py:15:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()`
|
13 | foo.rmdir(p)
14 | foo.remove(p)
15 | foo.unlink(p)
| ^^^^^^^^^^ PTH108
16 | foo.getcwd(p)
17 | b = foo_p.exists(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
12 13 | foo.replace(p)
13 14 | foo.rmdir(p)
14 15 | foo.remove(p)
15 |-foo.unlink(p)
16 |+pathlib.Path(p).unlink()
16 17 | foo.getcwd(p)
17 18 | b = foo_p.exists(p)
18 19 | bb = foo_p.expanduser(p)
import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
14 | foo.remove(p)
15 | foo.unlink(p)
16 | foo.getcwd(p)
| ^^^^^^^^^^ PTH109
17 | b = foo_p.exists(p)
18 | bb = foo_p.expanduser(p)
|
import_as.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
15 | foo.unlink(p)
16 | foo.getcwd(p)
17 | b = foo_p.exists(p)
| ^^^^^^^^^^^^ PTH110
18 | bb = foo_p.expanduser(p)
19 | bbb = foo_p.isdir(p)
|
= help: Replace with `Path(...).exists()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
14 15 | foo.remove(p)
15 16 | foo.unlink(p)
16 17 | foo.getcwd(p)
17 |-b = foo_p.exists(p)
18 |+b = pathlib.Path(p).exists()
18 19 | bb = foo_p.expanduser(p)
19 20 | bbb = foo_p.isdir(p)
20 21 | bbbb = foo_p.isfile(p)
import_as.py:18:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
16 | foo.getcwd(p)
17 | b = foo_p.exists(p)
18 | bb = foo_p.expanduser(p)
| ^^^^^^^^^^^^^^^^ PTH111
19 | bbb = foo_p.isdir(p)
20 | bbbb = foo_p.isfile(p)
|
= help: Replace with `Path(...).expanduser()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
15 16 | foo.unlink(p)
16 17 | foo.getcwd(p)
17 18 | b = foo_p.exists(p)
18 |-bb = foo_p.expanduser(p)
19 |+bb = pathlib.Path(p).expanduser()
19 20 | bbb = foo_p.isdir(p)
20 21 | bbbb = foo_p.isfile(p)
21 22 | bbbbb = foo_p.islink(p)
import_as.py:19:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
|
17 | b = foo_p.exists(p)
18 | bb = foo_p.expanduser(p)
19 | bbb = foo_p.isdir(p)
| ^^^^^^^^^^^ PTH112
20 | bbbb = foo_p.isfile(p)
21 | bbbbb = foo_p.islink(p)
|
= help: Replace with `Path(...).is_dir()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
16 17 | foo.getcwd(p)
17 18 | b = foo_p.exists(p)
18 19 | bb = foo_p.expanduser(p)
19 |-bbb = foo_p.isdir(p)
20 |+bbb = pathlib.Path(p).is_dir()
20 21 | bbbb = foo_p.isfile(p)
21 22 | bbbbb = foo_p.islink(p)
22 23 | foo.readlink(p)
import_as.py:20:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()`
|
18 | bb = foo_p.expanduser(p)
19 | bbb = foo_p.isdir(p)
20 | bbbb = foo_p.isfile(p)
| ^^^^^^^^^^^^ PTH113
21 | bbbbb = foo_p.islink(p)
22 | foo.readlink(p)
|
= help: Replace with `Path(...).is_file()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
17 18 | b = foo_p.exists(p)
18 19 | bb = foo_p.expanduser(p)
19 20 | bbb = foo_p.isdir(p)
20 |-bbbb = foo_p.isfile(p)
21 |+bbbb = pathlib.Path(p).is_file()
21 22 | bbbbb = foo_p.islink(p)
22 23 | foo.readlink(p)
23 24 | foo.stat(p)
import_as.py:21:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()`
|
19 | bbb = foo_p.isdir(p)
20 | bbbb = foo_p.isfile(p)
21 | bbbbb = foo_p.islink(p)
| ^^^^^^^^^^^^ PTH114
22 | foo.readlink(p)
23 | foo.stat(p)
|
= help: Replace with `Path(...).is_symlink()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
18 19 | bb = foo_p.expanduser(p)
19 20 | bbb = foo_p.isdir(p)
20 21 | bbbb = foo_p.isfile(p)
21 |-bbbbb = foo_p.islink(p)
22 |+bbbbb = pathlib.Path(p).is_symlink()
22 23 | foo.readlink(p)
23 24 | foo.stat(p)
24 25 | foo_p.isabs(p)
import_as.py:22:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
|
20 | bbbb = foo_p.isfile(p)
21 | bbbbb = foo_p.islink(p)
22 | foo.readlink(p)
| ^^^^^^^^^^^^ PTH115
23 | foo.stat(p)
24 | foo_p.isabs(p)
|
= help: Replace with `Path(...).readlink()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
19 20 | bbb = foo_p.isdir(p)
20 21 | bbbb = foo_p.isfile(p)
21 22 | bbbbb = foo_p.islink(p)
22 |-foo.readlink(p)
23 |+pathlib.Path(p).readlink()
23 24 | foo.stat(p)
24 25 | foo_p.isabs(p)
25 26 | foo_p.join(p, q)
import_as.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
21 | bbbbb = foo_p.islink(p)
22 | foo.readlink(p)
23 | foo.stat(p)
| ^^^^^^^^ PTH116
24 | foo_p.isabs(p)
25 | foo_p.join(p, q)
|
import_as.py:24:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()`
|
22 | foo.readlink(p)
23 | foo.stat(p)
24 | foo_p.isabs(p)
| ^^^^^^^^^^^ PTH117
25 | foo_p.join(p, q)
26 | foo.sep.join([p, q])
|
= help: Replace with `Path(...).is_absolute()`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
21 22 | bbbbb = foo_p.islink(p)
22 23 | foo.readlink(p)
23 24 | foo.stat(p)
24 |-foo_p.isabs(p)
25 |+pathlib.Path(p).is_absolute()
25 26 | foo_p.join(p, q)
26 27 | foo.sep.join([p, q])
27 28 | foo.sep.join((p, q))
import_as.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
23 | foo.stat(p)
24 | foo_p.isabs(p)
25 | foo_p.join(p, q)
| ^^^^^^^^^^ PTH118
26 | foo.sep.join([p, q])
27 | foo.sep.join((p, q))
|
import_as.py:26:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
24 | foo_p.isabs(p)
25 | foo_p.join(p, q)
26 | foo.sep.join([p, q])
| ^^^^^^^^^^^^ PTH118
27 | foo.sep.join((p, q))
28 | foo_p.basename(p)
|
import_as.py:27:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
25 | foo_p.join(p, q)
26 | foo.sep.join([p, q])
27 | foo.sep.join((p, q))
| ^^^^^^^^^^^^ PTH118
28 | foo_p.basename(p)
29 | foo_p.dirname(p)
|
import_as.py:28:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name`
|
26 | foo.sep.join([p, q])
27 | foo.sep.join((p, q))
28 | foo_p.basename(p)
| ^^^^^^^^^^^^^^ PTH119
29 | foo_p.dirname(p)
30 | foo_p.samefile(p)
|
= help: Replace with `Path(...).name`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
25 26 | foo_p.join(p, q)
26 27 | foo.sep.join([p, q])
27 28 | foo.sep.join((p, q))
28 |-foo_p.basename(p)
29 |+pathlib.Path(p).name
29 30 | foo_p.dirname(p)
30 31 | foo_p.samefile(p)
31 32 | foo_p.splitext(p)
import_as.py:29:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
|
27 | foo.sep.join((p, q))
28 | foo_p.basename(p)
29 | foo_p.dirname(p)
| ^^^^^^^^^^^^^ PTH120
30 | foo_p.samefile(p)
31 | foo_p.splitext(p)
|
= help: Replace with `Path(...).parent`
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib
3 4 |
4 5 | p = "/foo"
5 6 | q = "bar"
--------------------------------------------------------------------------------
26 27 | foo.sep.join([p, q])
27 28 | foo.sep.join((p, q))
28 29 | foo_p.basename(p)
29 |-foo_p.dirname(p)
30 |+pathlib.Path(p).parent
30 31 | foo_p.samefile(p)
31 32 | foo_p.splitext(p)
import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
28 | foo_p.basename(p)
29 | foo_p.dirname(p)
30 | foo_p.samefile(p)
| ^^^^^^^^^^^^^^ PTH121
31 | foo_p.splitext(p)
|
import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
29 | foo_p.dirname(p)
30 | foo_p.samefile(p)
31 | foo_p.splitext(p)
| ^^^^^^^^^^^^^^ PTH122
|

View File

@@ -0,0 +1,521 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
import_from.py:9:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
7 | q = "bar"
8 |
9 | a = abspath(p)
| ^^^^^^^ PTH100
10 | aa = chmod(p)
11 | aaa = mkdir(p)
|
= help: Replace with `Path(...).resolve()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
8 9 |
9 |-a = abspath(p)
10 |+a = pathlib.Path(p).resolve()
10 11 | aa = chmod(p)
11 12 | aaa = mkdir(p)
12 13 | makedirs(p)
import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
9 | a = abspath(p)
10 | aa = chmod(p)
| ^^^^^ PTH101
11 | aaa = mkdir(p)
12 | makedirs(p)
|
import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
9 | a = abspath(p)
10 | aa = chmod(p)
11 | aaa = mkdir(p)
| ^^^^^ PTH102
12 | makedirs(p)
13 | rename(p)
|
import_from.py:12:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
10 | aa = chmod(p)
11 | aaa = mkdir(p)
12 | makedirs(p)
| ^^^^^^^^ PTH103
13 | rename(p)
14 | replace(p)
|
import_from.py:13:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
11 | aaa = mkdir(p)
12 | makedirs(p)
13 | rename(p)
| ^^^^^^ PTH104
14 | replace(p)
15 | rmdir(p)
|
import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
12 | makedirs(p)
13 | rename(p)
14 | replace(p)
| ^^^^^^^ PTH105
15 | rmdir(p)
16 | remove(p)
|
import_from.py:15:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
13 | rename(p)
14 | replace(p)
15 | rmdir(p)
| ^^^^^ PTH106
16 | remove(p)
17 | unlink(p)
|
= help: Replace with `Path(...).rmdir()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
12 13 | makedirs(p)
13 14 | rename(p)
14 15 | replace(p)
15 |-rmdir(p)
16 |+pathlib.Path(p).rmdir()
16 17 | remove(p)
17 18 | unlink(p)
18 19 | getcwd(p)
import_from.py:16:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()`
|
14 | replace(p)
15 | rmdir(p)
16 | remove(p)
| ^^^^^^ PTH107
17 | unlink(p)
18 | getcwd(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
13 14 | rename(p)
14 15 | replace(p)
15 16 | rmdir(p)
16 |-remove(p)
17 |+pathlib.Path(p).unlink()
17 18 | unlink(p)
18 19 | getcwd(p)
19 20 | b = exists(p)
import_from.py:17:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()`
|
15 | rmdir(p)
16 | remove(p)
17 | unlink(p)
| ^^^^^^ PTH108
18 | getcwd(p)
19 | b = exists(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
14 15 | replace(p)
15 16 | rmdir(p)
16 17 | remove(p)
17 |-unlink(p)
18 |+pathlib.Path(p).unlink()
18 19 | getcwd(p)
19 20 | b = exists(p)
20 21 | bb = expanduser(p)
import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
16 | remove(p)
17 | unlink(p)
18 | getcwd(p)
| ^^^^^^ PTH109
19 | b = exists(p)
20 | bb = expanduser(p)
|
import_from.py:19:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
17 | unlink(p)
18 | getcwd(p)
19 | b = exists(p)
| ^^^^^^ PTH110
20 | bb = expanduser(p)
21 | bbb = isdir(p)
|
= help: Replace with `Path(...).exists()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
16 17 | remove(p)
17 18 | unlink(p)
18 19 | getcwd(p)
19 |-b = exists(p)
20 |+b = pathlib.Path(p).exists()
20 21 | bb = expanduser(p)
21 22 | bbb = isdir(p)
22 23 | bbbb = isfile(p)
import_from.py:20:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
18 | getcwd(p)
19 | b = exists(p)
20 | bb = expanduser(p)
| ^^^^^^^^^^ PTH111
21 | bbb = isdir(p)
22 | bbbb = isfile(p)
|
= help: Replace with `Path(...).expanduser()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
17 18 | unlink(p)
18 19 | getcwd(p)
19 20 | b = exists(p)
20 |-bb = expanduser(p)
21 |+bb = pathlib.Path(p).expanduser()
21 22 | bbb = isdir(p)
22 23 | bbbb = isfile(p)
23 24 | bbbbb = islink(p)
import_from.py:21:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
|
19 | b = exists(p)
20 | bb = expanduser(p)
21 | bbb = isdir(p)
| ^^^^^ PTH112
22 | bbbb = isfile(p)
23 | bbbbb = islink(p)
|
= help: Replace with `Path(...).is_dir()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
18 19 | getcwd(p)
19 20 | b = exists(p)
20 21 | bb = expanduser(p)
21 |-bbb = isdir(p)
22 |+bbb = pathlib.Path(p).is_dir()
22 23 | bbbb = isfile(p)
23 24 | bbbbb = islink(p)
24 25 | readlink(p)
import_from.py:22:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()`
|
20 | bb = expanduser(p)
21 | bbb = isdir(p)
22 | bbbb = isfile(p)
| ^^^^^^ PTH113
23 | bbbbb = islink(p)
24 | readlink(p)
|
= help: Replace with `Path(...).is_file()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
19 20 | b = exists(p)
20 21 | bb = expanduser(p)
21 22 | bbb = isdir(p)
22 |-bbbb = isfile(p)
23 |+bbbb = pathlib.Path(p).is_file()
23 24 | bbbbb = islink(p)
24 25 | readlink(p)
25 26 | stat(p)
import_from.py:23:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()`
|
21 | bbb = isdir(p)
22 | bbbb = isfile(p)
23 | bbbbb = islink(p)
| ^^^^^^ PTH114
24 | readlink(p)
25 | stat(p)
|
= help: Replace with `Path(...).is_symlink()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
20 21 | bb = expanduser(p)
21 22 | bbb = isdir(p)
22 23 | bbbb = isfile(p)
23 |-bbbbb = islink(p)
24 |+bbbbb = pathlib.Path(p).is_symlink()
24 25 | readlink(p)
25 26 | stat(p)
26 27 | isabs(p)
import_from.py:24:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
|
22 | bbbb = isfile(p)
23 | bbbbb = islink(p)
24 | readlink(p)
| ^^^^^^^^ PTH115
25 | stat(p)
26 | isabs(p)
|
= help: Replace with `Path(...).readlink()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
21 22 | bbb = isdir(p)
22 23 | bbbb = isfile(p)
23 24 | bbbbb = islink(p)
24 |-readlink(p)
25 |+pathlib.Path(p).readlink()
25 26 | stat(p)
26 27 | isabs(p)
27 28 | join(p, q)
import_from.py:25:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
23 | bbbbb = islink(p)
24 | readlink(p)
25 | stat(p)
| ^^^^ PTH116
26 | isabs(p)
27 | join(p, q)
|
import_from.py:26:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()`
|
24 | readlink(p)
25 | stat(p)
26 | isabs(p)
| ^^^^^ PTH117
27 | join(p, q)
28 | sep.join((p, q))
|
= help: Replace with `Path(...).is_absolute()`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
23 24 | bbbbb = islink(p)
24 25 | readlink(p)
25 26 | stat(p)
26 |-isabs(p)
27 |+pathlib.Path(p).is_absolute()
27 28 | join(p, q)
28 29 | sep.join((p, q))
29 30 | sep.join([p, q])
import_from.py:27:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
25 | stat(p)
26 | isabs(p)
27 | join(p, q)
| ^^^^ PTH118
28 | sep.join((p, q))
29 | sep.join([p, q])
|
import_from.py:28:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
26 | isabs(p)
27 | join(p, q)
28 | sep.join((p, q))
| ^^^^^^^^ PTH118
29 | sep.join([p, q])
30 | basename(p)
|
import_from.py:29:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
27 | join(p, q)
28 | sep.join((p, q))
29 | sep.join([p, q])
| ^^^^^^^^ PTH118
30 | basename(p)
31 | dirname(p)
|
import_from.py:30:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name`
|
28 | sep.join((p, q))
29 | sep.join([p, q])
30 | basename(p)
| ^^^^^^^^ PTH119
31 | dirname(p)
32 | samefile(p)
|
= help: Replace with `Path(...).name`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
27 28 | join(p, q)
28 29 | sep.join((p, q))
29 30 | sep.join([p, q])
30 |-basename(p)
31 |+pathlib.Path(p).name
31 32 | dirname(p)
32 33 | samefile(p)
33 34 | splitext(p)
import_from.py:31:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
|
29 | sep.join([p, q])
30 | basename(p)
31 | dirname(p)
| ^^^^^^^ PTH120
32 | samefile(p)
33 | splitext(p)
|
= help: Replace with `Path(...).parent`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
28 29 | sep.join((p, q))
29 30 | sep.join([p, q])
30 31 | basename(p)
31 |-dirname(p)
32 |+pathlib.Path(p).parent
32 33 | samefile(p)
33 34 | splitext(p)
34 35 | with open(p) as fp:
import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
30 | basename(p)
31 | dirname(p)
32 | samefile(p)
| ^^^^^^^^ PTH121
33 | splitext(p)
34 | with open(p) as fp:
|
import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
31 | dirname(p)
32 | samefile(p)
33 | splitext(p)
| ^^^^^^^^ PTH122
34 | with open(p) as fp:
35 | fp.read()
|
import_from.py:34:6: PTH123 `open()` should be replaced by `Path.open()`
|
32 | samefile(p)
33 | splitext(p)
34 | with open(p) as fp:
| ^^^^ PTH123
35 | fp.read()
36 | open(p).close()
|
import_from.py:36:1: PTH123 `open()` should be replaced by `Path.open()`
|
34 | with open(p) as fp:
35 | fp.read()
36 | open(p).close()
| ^^^^ PTH123
|
import_from.py:43:10: PTH123 `open()` should be replaced by `Path.open()`
|
41 | from builtins import open
42 |
43 | with open(p) as _: ... # Error
| ^^^^ PTH123
|

View File

@@ -0,0 +1,491 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
import_from_as.py:14:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
12 | q = "bar"
13 |
14 | a = xabspath(p)
| ^^^^^^^^ PTH100
15 | aa = xchmod(p)
16 | aaa = xmkdir(p)
|
= help: Replace with `Path(...).resolve()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
13 14 |
14 |-a = xabspath(p)
15 |+a = pathlib.Path(p).resolve()
15 16 | aa = xchmod(p)
16 17 | aaa = xmkdir(p)
17 18 | xmakedirs(p)
import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
14 | a = xabspath(p)
15 | aa = xchmod(p)
| ^^^^^^ PTH101
16 | aaa = xmkdir(p)
17 | xmakedirs(p)
|
import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
14 | a = xabspath(p)
15 | aa = xchmod(p)
16 | aaa = xmkdir(p)
| ^^^^^^ PTH102
17 | xmakedirs(p)
18 | xrename(p)
|
import_from_as.py:17:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
15 | aa = xchmod(p)
16 | aaa = xmkdir(p)
17 | xmakedirs(p)
| ^^^^^^^^^ PTH103
18 | xrename(p)
19 | xreplace(p)
|
import_from_as.py:18:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
16 | aaa = xmkdir(p)
17 | xmakedirs(p)
18 | xrename(p)
| ^^^^^^^ PTH104
19 | xreplace(p)
20 | xrmdir(p)
|
import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
17 | xmakedirs(p)
18 | xrename(p)
19 | xreplace(p)
| ^^^^^^^^ PTH105
20 | xrmdir(p)
21 | xremove(p)
|
import_from_as.py:20:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
18 | xrename(p)
19 | xreplace(p)
20 | xrmdir(p)
| ^^^^^^ PTH106
21 | xremove(p)
22 | xunlink(p)
|
= help: Replace with `Path(...).rmdir()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
17 18 | xmakedirs(p)
18 19 | xrename(p)
19 20 | xreplace(p)
20 |-xrmdir(p)
21 |+pathlib.Path(p).rmdir()
21 22 | xremove(p)
22 23 | xunlink(p)
23 24 | xgetcwd(p)
import_from_as.py:21:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()`
|
19 | xreplace(p)
20 | xrmdir(p)
21 | xremove(p)
| ^^^^^^^ PTH107
22 | xunlink(p)
23 | xgetcwd(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
18 19 | xrename(p)
19 20 | xreplace(p)
20 21 | xrmdir(p)
21 |-xremove(p)
22 |+pathlib.Path(p).unlink()
22 23 | xunlink(p)
23 24 | xgetcwd(p)
24 25 | b = xexists(p)
import_from_as.py:22:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()`
|
20 | xrmdir(p)
21 | xremove(p)
22 | xunlink(p)
| ^^^^^^^ PTH108
23 | xgetcwd(p)
24 | b = xexists(p)
|
= help: Replace with `Path(...).unlink()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
19 20 | xreplace(p)
20 21 | xrmdir(p)
21 22 | xremove(p)
22 |-xunlink(p)
23 |+pathlib.Path(p).unlink()
23 24 | xgetcwd(p)
24 25 | b = xexists(p)
25 26 | bb = xexpanduser(p)
import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
21 | xremove(p)
22 | xunlink(p)
23 | xgetcwd(p)
| ^^^^^^^ PTH109
24 | b = xexists(p)
25 | bb = xexpanduser(p)
|
import_from_as.py:24:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()`
|
22 | xunlink(p)
23 | xgetcwd(p)
24 | b = xexists(p)
| ^^^^^^^ PTH110
25 | bb = xexpanduser(p)
26 | bbb = xisdir(p)
|
= help: Replace with `Path(...).exists()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
21 22 | xremove(p)
22 23 | xunlink(p)
23 24 | xgetcwd(p)
24 |-b = xexists(p)
25 |+b = pathlib.Path(p).exists()
25 26 | bb = xexpanduser(p)
26 27 | bbb = xisdir(p)
27 28 | bbbb = xisfile(p)
import_from_as.py:25:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()`
|
23 | xgetcwd(p)
24 | b = xexists(p)
25 | bb = xexpanduser(p)
| ^^^^^^^^^^^ PTH111
26 | bbb = xisdir(p)
27 | bbbb = xisfile(p)
|
= help: Replace with `Path(...).expanduser()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
22 23 | xunlink(p)
23 24 | xgetcwd(p)
24 25 | b = xexists(p)
25 |-bb = xexpanduser(p)
26 |+bb = pathlib.Path(p).expanduser()
26 27 | bbb = xisdir(p)
27 28 | bbbb = xisfile(p)
28 29 | bbbbb = xislink(p)
import_from_as.py:26:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
|
24 | b = xexists(p)
25 | bb = xexpanduser(p)
26 | bbb = xisdir(p)
| ^^^^^^ PTH112
27 | bbbb = xisfile(p)
28 | bbbbb = xislink(p)
|
= help: Replace with `Path(...).is_dir()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
23 24 | xgetcwd(p)
24 25 | b = xexists(p)
25 26 | bb = xexpanduser(p)
26 |-bbb = xisdir(p)
27 |+bbb = pathlib.Path(p).is_dir()
27 28 | bbbb = xisfile(p)
28 29 | bbbbb = xislink(p)
29 30 | xreadlink(p)
import_from_as.py:27:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()`
|
25 | bb = xexpanduser(p)
26 | bbb = xisdir(p)
27 | bbbb = xisfile(p)
| ^^^^^^^ PTH113
28 | bbbbb = xislink(p)
29 | xreadlink(p)
|
= help: Replace with `Path(...).is_file()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
24 25 | b = xexists(p)
25 26 | bb = xexpanduser(p)
26 27 | bbb = xisdir(p)
27 |-bbbb = xisfile(p)
28 |+bbbb = pathlib.Path(p).is_file()
28 29 | bbbbb = xislink(p)
29 30 | xreadlink(p)
30 31 | xstat(p)
import_from_as.py:28:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()`
|
26 | bbb = xisdir(p)
27 | bbbb = xisfile(p)
28 | bbbbb = xislink(p)
| ^^^^^^^ PTH114
29 | xreadlink(p)
30 | xstat(p)
|
= help: Replace with `Path(...).is_symlink()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
25 26 | bb = xexpanduser(p)
26 27 | bbb = xisdir(p)
27 28 | bbbb = xisfile(p)
28 |-bbbbb = xislink(p)
29 |+bbbbb = pathlib.Path(p).is_symlink()
29 30 | xreadlink(p)
30 31 | xstat(p)
31 32 | xisabs(p)
import_from_as.py:29:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
|
27 | bbbb = xisfile(p)
28 | bbbbb = xislink(p)
29 | xreadlink(p)
| ^^^^^^^^^ PTH115
30 | xstat(p)
31 | xisabs(p)
|
= help: Replace with `Path(...).readlink()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
26 27 | bbb = xisdir(p)
27 28 | bbbb = xisfile(p)
28 29 | bbbbb = xislink(p)
29 |-xreadlink(p)
30 |+pathlib.Path(p).readlink()
30 31 | xstat(p)
31 32 | xisabs(p)
32 33 | xjoin(p, q)
import_from_as.py:30:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
|
28 | bbbbb = xislink(p)
29 | xreadlink(p)
30 | xstat(p)
| ^^^^^ PTH116
31 | xisabs(p)
32 | xjoin(p, q)
|
import_from_as.py:31:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()`
|
29 | xreadlink(p)
30 | xstat(p)
31 | xisabs(p)
| ^^^^^^ PTH117
32 | xjoin(p, q)
33 | s.join((p, q))
|
= help: Replace with `Path(...).is_absolute()`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
28 29 | bbbbb = xislink(p)
29 30 | xreadlink(p)
30 31 | xstat(p)
31 |-xisabs(p)
32 |+pathlib.Path(p).is_absolute()
32 33 | xjoin(p, q)
33 34 | s.join((p, q))
34 35 | s.join([p, q])
import_from_as.py:32:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator
|
30 | xstat(p)
31 | xisabs(p)
32 | xjoin(p, q)
| ^^^^^ PTH118
33 | s.join((p, q))
34 | s.join([p, q])
|
import_from_as.py:33:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
31 | xisabs(p)
32 | xjoin(p, q)
33 | s.join((p, q))
| ^^^^^^ PTH118
34 | s.join([p, q])
35 | xbasename(p)
|
import_from_as.py:34:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator
|
32 | xjoin(p, q)
33 | s.join((p, q))
34 | s.join([p, q])
| ^^^^^^ PTH118
35 | xbasename(p)
36 | xdirname(p)
|
import_from_as.py:35:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name`
|
33 | s.join((p, q))
34 | s.join([p, q])
35 | xbasename(p)
| ^^^^^^^^^ PTH119
36 | xdirname(p)
37 | xsamefile(p)
|
= help: Replace with `Path(...).name`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
32 33 | xjoin(p, q)
33 34 | s.join((p, q))
34 35 | s.join([p, q])
35 |-xbasename(p)
36 |+pathlib.Path(p).name
36 37 | xdirname(p)
37 38 | xsamefile(p)
38 39 | xsplitext(p)
import_from_as.py:36:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent`
|
34 | s.join([p, q])
35 | xbasename(p)
36 | xdirname(p)
| ^^^^^^^^ PTH120
37 | xsamefile(p)
38 | xsplitext(p)
|
= help: Replace with `Path(...).parent`
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext
10 |+import pathlib
10 11 |
11 12 | p = "/foo"
12 13 | q = "bar"
--------------------------------------------------------------------------------
33 34 | s.join((p, q))
34 35 | s.join([p, q])
35 36 | xbasename(p)
36 |-xdirname(p)
37 |+pathlib.Path(p).parent
37 38 | xsamefile(p)
38 39 | xsplitext(p)
import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
35 | xbasename(p)
36 | xdirname(p)
37 | xsamefile(p)
| ^^^^^^^^^ PTH121
38 | xsplitext(p)
|
import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
36 | xdirname(p)
37 | xsamefile(p)
38 | xsplitext(p)
| ^^^^^^^^^ PTH122
|

View File

@@ -2,51 +2,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
/// ## What it does
/// Checks for uses of `os.path.abspath`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.resolve()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.abspath()`).
///
/// ## Examples
/// ```python
/// import os
///
/// file_path = os.path.abspath("../path/to/file")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// file_path = Path("../path/to/file").resolve()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathAbspath;
impl Violation for OsPathAbspath {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.abspath()` should be replaced by `Path.resolve()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.chmod`.
///
@@ -275,141 +230,6 @@ impl Violation for OsReplace {
}
}
/// ## What it does
/// Checks for uses of `os.rmdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.rmdir()` can improve readability over the `os`
/// module's counterparts (e.g., `os.rmdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.rmdir("folder/")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("folder/").rmdir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir)
/// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRmdir;
impl Violation for OsRmdir {
#[derive_message_formats]
fn message(&self) -> String {
"`os.rmdir()` should be replaced by `Path.rmdir()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.remove`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.unlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.remove()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.remove("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").unlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRemove;
impl Violation for OsRemove {
#[derive_message_formats]
fn message(&self) -> String {
"`os.remove()` should be replaced by `Path.unlink()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.unlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.unlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.unlink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.unlink("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").unlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsUnlink;
impl Violation for OsUnlink {
#[derive_message_formats]
fn message(&self) -> String {
"`os.unlink()` should be replaced by `Path.unlink()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.getcwd` and `os.getcwdb`.
///
@@ -456,276 +276,6 @@ impl Violation for OsGetcwd {
}
}
/// ## What it does
/// Checks for uses of `os.path.exists`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.exists()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.exists()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.exists("file.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").exists()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists)
/// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathExists;
impl Violation for OsPathExists {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.exists()` should be replaced by `Path.exists()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.expanduser`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.expanduser()` can improve readability over the `os.path`
/// module's counterparts (e.g., as `os.path.expanduser()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.expanduser("~/films/Monty Python")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("~/films/Monty Python").expanduser()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathExpanduser;
impl Violation for OsPathExpanduser {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.expanduser()` should be replaced by `Path.expanduser()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.isdir`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_dir()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.isdir()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.isdir("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_dir()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir)
/// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsdir;
impl Violation for OsPathIsdir {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isdir()` should be replaced by `Path.is_dir()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.isfile`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_file()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.isfile()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.isfile("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_file()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file)
/// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsfile;
impl Violation for OsPathIsfile {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isfile()` should be replaced by `Path.is_file()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.islink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_symlink()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.islink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.islink("docs")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("docs").is_symlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink)
/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIslink;
impl Violation for OsPathIslink {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.islink()` should be replaced by `Path.is_symlink()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.readlink`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.readlink()` can improve readability over the `os`
/// module's counterparts (e.g., `os.readlink()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.readlink(file_name)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(file_name).readlink()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline)
/// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsReadlink;
impl Violation for OsReadlink {
#[derive_message_formats]
fn message(&self) -> String {
"`os.readlink()` should be replaced by `Path.readlink()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.stat`.
///
@@ -781,53 +331,6 @@ impl Violation for OsStat {
}
}
/// ## What it does
/// Checks for uses of `os.path.isabs`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.is_absolute()` can improve readability over the `os.path`
/// module's counterparts (e.g., as `os.path.isabs()`).
///
/// ## Examples
/// ```python
/// import os
///
/// if os.path.isabs(file_name):
/// print("Absolute path!")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// if Path(file_name).is_absolute():
/// print("Absolute path!")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute)
/// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathIsabs;
impl Violation for OsPathIsabs {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.isabs()` should be replaced by `Path.is_absolute()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.join`.
///
@@ -890,96 +393,6 @@ pub(crate) enum Joiner {
Joinpath,
}
/// ## What it does
/// Checks for uses of `os.path.basename`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.name` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.basename()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.basename(__file__)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(__file__).name
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
/// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathBasename;
impl Violation for OsPathBasename {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.basename()` should be replaced by `Path.name`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.dirname`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.parent` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.dirname()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.dirname(__file__)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path(__file__).parent
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent)
/// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathDirname;
impl Violation for OsPathDirname {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.dirname()` should be replaced by `Path.parent`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.path.samefile`.
///

View File

@@ -60,12 +60,14 @@ impl Violation for IndentationWithInvalidMultiple {
/// ```python
/// if True:
/// # a = 1
/// ...
/// ```
///
/// Use instead:
/// ```python
/// if True:
/// # a = 1
/// ...
/// ```
///
/// ## Formatter compatibility

View File

@@ -43,12 +43,12 @@ impl AlwaysFixableViolation for MultipleSpacesAfterKeyword {
///
/// ## Example
/// ```python
/// True and False
/// x and y
/// ```
///
/// Use instead:
/// ```python
/// True and False
/// x and y
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct MultipleSpacesBeforeKeyword;

View File

@@ -238,6 +238,9 @@ impl Violation for DocstringExtraneousYields {
///
/// ## Example
/// ```python
/// class FasterThanLightError(ArithmeticError): ...
///
///
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///
@@ -256,6 +259,9 @@ impl Violation for DocstringExtraneousYields {
///
/// Use instead:
/// ```python
/// class FasterThanLightError(ArithmeticError): ...
///
///
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///

View File

@@ -774,7 +774,7 @@ mod tests {
messages.sort_by_key(|diagnostic| diagnostic.expect_range().start());
let actual = messages
.iter()
.filter(|msg| !msg.is_syntax_error())
.filter(|msg| !msg.is_invalid_syntax())
.map(Diagnostic::name)
.collect::<Vec<_>>();
let expected: Vec<_> = expected.iter().map(|rule| rule.name().as_str()).collect();

View File

@@ -10,11 +10,11 @@ use crate::Violation;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for access to the first or last element of `str.split()` without
/// Checks for access to the first or last element of `str.split()` or `str.rsplit()` without
/// `maxsplit=1`
///
/// ## Why is this bad?
/// Calling `str.split()` without `maxsplit` set splits on every delimiter in the
/// Calling `str.split()` or `str.rsplit()` without passing `maxsplit=1` splits on every delimiter in the
/// string. When accessing only the first or last element of the result, it
/// would be more efficient to only split once.
///
@@ -29,14 +29,44 @@ use crate::checkers::ast::Checker;
/// url = "www.example.com"
/// prefix = url.split(".", maxsplit=1)[0]
/// ```
///
/// To access the last element, use `str.rsplit()` instead of `str.split()`:
/// ```python
/// url = "www.example.com"
/// suffix = url.rsplit(".", maxsplit=1)[-1]
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct MissingMaxsplitArg;
pub(crate) struct MissingMaxsplitArg {
index: SliceBoundary,
actual_split_type: String,
}
/// Represents the index of the slice used for this rule (which can only be 0 or -1)
enum SliceBoundary {
First,
Last,
}
impl Violation for MissingMaxsplitArg {
#[derive_message_formats]
fn message(&self) -> String {
"Accessing only the first or last element of `str.split()` without setting `maxsplit=1`"
.to_string()
let MissingMaxsplitArg {
index,
actual_split_type,
} = self;
let suggested_split_type = match index {
SliceBoundary::First => "split",
SliceBoundary::Last => "rsplit",
};
if actual_split_type == suggested_split_type {
format!("Pass `maxsplit=1` into `str.{actual_split_type}()`")
} else {
format!(
"Instead of `str.{actual_split_type}()`, call `str.{suggested_split_type}()` and pass `maxsplit=1`",
)
}
}
}
@@ -82,9 +112,11 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
_ => return,
};
if !matches!(index, Some(0 | -1)) {
return;
}
let slice_boundary = match index {
Some(0) => SliceBoundary::First,
Some(-1) => SliceBoundary::Last,
_ => return,
};
let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() else {
return;
@@ -129,5 +161,11 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
}
}
checker.report_diagnostic(MissingMaxsplitArg, expr.range());
checker.report_diagnostic(
MissingMaxsplitArg {
index: slice_boundary,
actual_split_type: attr.to_string(),
},
expr.range(),
);
}

Some files were not shown because too many files have changed in this diff Show More