Compare commits

..

45 Commits

Author SHA1 Message Date
Jack O'Connor
3f22497bdd WIP: defer walking function bodies to end-of-scope in SemanticIndexBuilder
This is intended to fix the one failing test from the previous commit.
And it actually does fix it! But it also causes a huge number of other
tests to fail. The minimized repro seems to be this:

```
$ cat test.py
class Foo:
    pass
foo = Foo()
$ ty check test.py
error[panic]: Panicked at crates/ty_python_semantic/src/types.rs:156:38 when checking `/tmp/test.py`: `Failed to retrieve the inferred type for an `ast::Expr` node passed to `TypeInference::expression_type()`. The `TypeInferenceBuilder` should infer and store types for all `ast::Expr` nodes in any `TypeInference` region it analyzes.`
info: This indicates a bug in ty.
info: If you could open an issue at https://github.com/astral-sh/ty/issues/new?title=%5Bpanic%5D, we'd be very appreciative!
info: Platform: linux x86_64
info: Args: ["/home/jacko/astral/ruff/target-mold/debug/ty", "check", "test.py"]
info: run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information
info: query stacktrace:
   0: FunctionType < 'db >::signature_(Id(5007))
             at crates/ty_python_semantic/src/types/function.rs:595
             cycle heads: infer_scope_types(Id(c62)) -> IterationCount(0), FunctionType < 'db >::signature_(Id(5007)) -> IterationCount(0), FunctionType < 'db >::signature_(Id(5000)) -> IterationCount(0)
   1: infer_expression_types(Id(1463))
             at crates/ty_python_semantic/src/types/infer.rs:235
   2: infer_definition_types(Id(11ab))
             at crates/ty_python_semantic/src/types/infer.rs:159
   3: infer_scope_types(Id(c62))
             at crates/ty_python_semantic/src/types/infer.rs:130
             cycle heads: infer_scope_types(Id(c62)) -> IterationCount(0)
   4: FunctionType < 'db >::signature_(Id(5000))
             at crates/ty_python_semantic/src/types/function.rs:595
   5: infer_expression_types(Id(1400))
             at crates/ty_python_semantic/src/types/infer.rs:235
   6: infer_definition_types(Id(1001))
             at crates/ty_python_semantic/src/types/infer.rs:159
   7: infer_scope_types(Id(c00))
             at crates/ty_python_semantic/src/types/infer.rs:130
   8: check_file_impl(Id(800))
             at crates/ty_project/src/lib.rs:474
```
2025-07-10 16:46:40 -07:00
Jack O'Connor
05cf7c3458 WIP: move the InvalidNonlocal check to SemanticIndexBuilder
This makes one test case fail, basically this:

```py
def f():
    def g():
        nonlocal x  # allowed!
    x = 1
```
2025-07-10 16:45:01 -07:00
Jack O'Connor
664a9a28dc [ty] add support for nonlocal statements 2025-07-10 11:13:47 -07: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
149 changed files with 7982 additions and 4609 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/.*|

76
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",
@@ -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",
@@ -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",
@@ -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",
]
@@ -4178,7 +4156,7 @@ dependencies = [
"ruff_python_ast",
"ruff_python_parser",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"smallvec",
"tracing",
@@ -4210,7 +4188,7 @@ dependencies = [
"ruff_python_ast",
"ruff_python_formatter",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -4231,7 +4209,6 @@ dependencies = [
"camino",
"colored 3.0.0",
"compact_str",
"countme",
"dir-test",
"drop_bomb",
"get-size2",
@@ -4255,7 +4232,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"schemars",
"serde",
@@ -4268,6 +4245,7 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"ty_python_semantic",
"ty_static",
"ty_test",
"ty_vendored",
]
@@ -4286,7 +4264,7 @@ dependencies = [
"ruff_notebook",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash",
"salsa",
"serde",
"serde_json",
@@ -4299,6 +4277,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 +4302,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 +4312,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

@@ -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

@@ -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

@@ -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,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

@@ -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

@@ -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

@@ -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;
@@ -671,7 +670,12 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::InvalidStarExpression
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
| SemanticSyntaxErrorKind::DuplicateParameter(_)
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_)
| SemanticSyntaxErrorKind::InvalidNonlocal(_) => {
self.semantic_errors.borrow_mut().push(error);
}
}
@@ -3158,7 +3162,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 +3181,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

@@ -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(),
);
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
missing_maxsplit_arg.py:14:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:14:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
12 | # Errors
13 | ## Test split called directly on string literal
@@ -11,7 +11,7 @@ missing_maxsplit_arg.py:14:1: PLC0207 Accessing only the first or last element o
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:15:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:15:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
13 | ## Test split called directly on string literal
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
@@ -21,7 +21,7 @@ missing_maxsplit_arg.py:15:1: PLC0207 Accessing only the first or last element o
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:16:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:16:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
@@ -30,7 +30,7 @@ missing_maxsplit_arg.py:16:1: PLC0207 Accessing only the first or last element o
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:17:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:17:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
@@ -40,7 +40,7 @@ missing_maxsplit_arg.py:17:1: PLC0207 Accessing only the first or last element o
19 | ## Test split called on string variable
|
missing_maxsplit_arg.py:20:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:20:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
19 | ## Test split called on string variable
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
@@ -49,7 +49,7 @@ missing_maxsplit_arg.py:20:1: PLC0207 Accessing only the first or last element o
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:21:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:21:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
19 | ## Test split called on string variable
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
@@ -59,7 +59,7 @@ missing_maxsplit_arg.py:21:1: PLC0207 Accessing only the first or last element o
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:22:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:22:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
@@ -68,7 +68,7 @@ missing_maxsplit_arg.py:22:1: PLC0207 Accessing only the first or last element o
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:23:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:23:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -78,7 +78,7 @@ missing_maxsplit_arg.py:23:1: PLC0207 Accessing only the first or last element o
25 | ## Test split called on class attribute
|
missing_maxsplit_arg.py:26:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:26:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
25 | ## Test split called on class attribute
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
@@ -87,7 +87,7 @@ missing_maxsplit_arg.py:26:1: PLC0207 Accessing only the first or last element o
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:27:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:27:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
25 | ## Test split called on class attribute
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
@@ -97,7 +97,7 @@ missing_maxsplit_arg.py:27:1: PLC0207 Accessing only the first or last element o
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:28:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:28:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
@@ -106,7 +106,7 @@ missing_maxsplit_arg.py:28:1: PLC0207 Accessing only the first or last element o
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:29:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:29:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -116,7 +116,7 @@ missing_maxsplit_arg.py:29:1: PLC0207 Accessing only the first or last element o
31 | ## Test split called on sliced string
|
missing_maxsplit_arg.py:32:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:32:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
31 | ## Test split called on sliced string
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -125,7 +125,7 @@ missing_maxsplit_arg.py:32:1: PLC0207 Accessing only the first or last element o
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:33:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:33:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
31 | ## Test split called on sliced string
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -135,7 +135,7 @@ missing_maxsplit_arg.py:33:1: PLC0207 Accessing only the first or last element o
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:34:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:34:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -145,7 +145,7 @@ missing_maxsplit_arg.py:34:1: PLC0207 Accessing only the first or last element o
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:35:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:35:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
@@ -155,7 +155,7 @@ missing_maxsplit_arg.py:35:1: PLC0207 Accessing only the first or last element o
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:36:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:36:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
@@ -165,7 +165,7 @@ missing_maxsplit_arg.py:36:1: PLC0207 Accessing only the first or last element o
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:37:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:37:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
@@ -174,7 +174,7 @@ missing_maxsplit_arg.py:37:1: PLC0207 Accessing only the first or last element o
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:38:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:38:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
@@ -184,7 +184,7 @@ missing_maxsplit_arg.py:38:1: PLC0207 Accessing only the first or last element o
40 | ## Test sep given as named argument
|
missing_maxsplit_arg.py:41:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:41:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
40 | ## Test sep given as named argument
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
@@ -193,7 +193,7 @@ missing_maxsplit_arg.py:41:1: PLC0207 Accessing only the first or last element o
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:42:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:42:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
40 | ## Test sep given as named argument
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
@@ -203,7 +203,7 @@ missing_maxsplit_arg.py:42:1: PLC0207 Accessing only the first or last element o
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:43:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:43:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
@@ -212,7 +212,7 @@ missing_maxsplit_arg.py:43:1: PLC0207 Accessing only the first or last element o
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:44:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:44:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
@@ -222,7 +222,7 @@ missing_maxsplit_arg.py:44:1: PLC0207 Accessing only the first or last element o
46 | ## Special cases
|
missing_maxsplit_arg.py:47:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:47:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
46 | ## Special cases
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
@@ -231,7 +231,7 @@ missing_maxsplit_arg.py:47:1: PLC0207 Accessing only the first or last element o
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:48:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:48:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
46 | ## Special cases
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
@@ -240,7 +240,7 @@ missing_maxsplit_arg.py:48:1: PLC0207 Accessing only the first or last element o
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:49:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:49:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
@@ -250,7 +250,7 @@ missing_maxsplit_arg.py:49:1: PLC0207 Accessing only the first or last element o
51 | ## Test class attribute named split
|
missing_maxsplit_arg.py:52:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:52:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
51 | ## Test class attribute named split
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
@@ -259,7 +259,7 @@ missing_maxsplit_arg.py:52:1: PLC0207 Accessing only the first or last element o
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:53:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:53:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
51 | ## Test class attribute named split
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
@@ -269,7 +269,7 @@ missing_maxsplit_arg.py:53:1: PLC0207 Accessing only the first or last element o
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:54:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:54:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
@@ -278,7 +278,7 @@ missing_maxsplit_arg.py:54:1: PLC0207 Accessing only the first or last element o
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
missing_maxsplit_arg.py:55:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:55:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -288,14 +288,14 @@ missing_maxsplit_arg.py:55:1: PLC0207 Accessing only the first or last element o
57 | ## Test unpacked dict literal kwargs
|
missing_maxsplit_arg.py:58:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:58:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
57 | ## Test unpacked dict literal kwargs
58 | "1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
|
missing_maxsplit_arg.py:179:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:179:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
177 | # Errors
178 | kwargs_without_maxsplit = {"seq": ","}
@@ -305,7 +305,7 @@ missing_maxsplit_arg.py:179:1: PLC0207 Accessing only the first or last element
181 | kwargs_with_maxsplit = {"maxsplit": 1}
|
missing_maxsplit_arg.py:182:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:182:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
180 | # OK
181 | kwargs_with_maxsplit = {"maxsplit": 1}
@@ -315,7 +315,7 @@ missing_maxsplit_arg.py:182:1: PLC0207 Accessing only the first or last element
184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
|
missing_maxsplit_arg.py:184:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
missing_maxsplit_arg.py:184:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}

View File

@@ -5,7 +5,7 @@ use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::preview::is_safe_super_call_with_parameters_fix_enabled;
use crate::{AlwaysFixableViolation, Edit, Fix};
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for `super` calls that pass redundant arguments.
@@ -57,14 +57,16 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
#[derive(ViolationMetadata)]
pub(crate) struct SuperCallWithParameters;
impl AlwaysFixableViolation for SuperCallWithParameters {
impl Violation for SuperCallWithParameters {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"Use `super()` instead of `super(__class__, self)`".to_string()
}
fn fix_title(&self) -> String {
"Remove `super()` parameters".to_string()
fn fix_title(&self) -> Option<String> {
Some("Remove `super()` parameters".to_string())
}
}
@@ -165,22 +167,26 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
return;
}
let applicability = if !checker.comment_ranges().intersects(call.arguments.range())
&& is_safe_super_call_with_parameters_fix_enabled(checker.settings())
{
Applicability::Safe
} else {
Applicability::Unsafe
};
let mut diagnostic = checker.report_diagnostic(SuperCallWithParameters, call.arguments.range());
diagnostic.set_fix(Fix::applicable_edit(
Edit::deletion(
call.arguments.start() + TextSize::new(1),
call.arguments.end() - TextSize::new(1),
),
applicability,
));
// Only provide a fix if there are no keyword arguments, since super() doesn't accept keyword arguments
if call.arguments.keywords.is_empty() {
let applicability = if !checker.comment_ranges().intersects(call.arguments.range())
&& is_safe_super_call_with_parameters_fix_enabled(checker.settings())
{
Applicability::Safe
} else {
Applicability::Unsafe
};
diagnostic.set_fix(Fix::applicable_edit(
Edit::deletion(
call.arguments.start() + TextSize::new(1),
call.arguments.end() - TextSize::new(1),
),
applicability,
));
}
}
/// Returns `true` if a call is an argumented `super` invocation.

View File

@@ -249,3 +249,53 @@ UP008.py:123:14: UP008 [*] Use `super()` instead of `super(__class__, self)`
126 |- # also a comment
127 |- ).f()
123 |+ super().f()
128 124 |
129 125 |
130 126 | # Issue #19096: super calls with keyword arguments should emit diagnostic but not be fixed
UP008.py:133:21: UP008 Use `super()` instead of `super(__class__, self)`
|
131 | class Ord(int):
132 | def __len__(self):
133 | return super(Ord, self, uhoh=True, **{"error": True}).bit_length()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
134 |
135 | class ExampleWithKeywords:
|
= help: Remove `super()` parameters
UP008.py:137:14: UP008 Use `super()` instead of `super(__class__, self)`
|
135 | class ExampleWithKeywords:
136 | def method1(self):
137 | super(ExampleWithKeywords, self, invalid=True).some_method() # Should emit diagnostic but NOT be fixed
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
138 |
139 | def method2(self):
|
= help: Remove `super()` parameters
UP008.py:140:14: UP008 Use `super()` instead of `super(__class__, self)`
|
139 | def method2(self):
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
141 |
142 | def method3(self):
|
= help: Remove `super()` parameters
UP008.py:143:14: UP008 [*] Use `super()` instead of `super(__class__, self)`
|
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
|
= help: Remove `super()` parameters
Unsafe fix
140 140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
141 141 |
142 142 | def method3(self):
143 |- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 |+ super().some_method() # Should be fixed - no keywords

View File

@@ -249,3 +249,53 @@ UP008.py:123:14: UP008 [*] Use `super()` instead of `super(__class__, self)`
126 |- # also a comment
127 |- ).f()
123 |+ super().f()
128 124 |
129 125 |
130 126 | # Issue #19096: super calls with keyword arguments should emit diagnostic but not be fixed
UP008.py:133:21: UP008 Use `super()` instead of `super(__class__, self)`
|
131 | class Ord(int):
132 | def __len__(self):
133 | return super(Ord, self, uhoh=True, **{"error": True}).bit_length()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
134 |
135 | class ExampleWithKeywords:
|
= help: Remove `super()` parameters
UP008.py:137:14: UP008 Use `super()` instead of `super(__class__, self)`
|
135 | class ExampleWithKeywords:
136 | def method1(self):
137 | super(ExampleWithKeywords, self, invalid=True).some_method() # Should emit diagnostic but NOT be fixed
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
138 |
139 | def method2(self):
|
= help: Remove `super()` parameters
UP008.py:140:14: UP008 Use `super()` instead of `super(__class__, self)`
|
139 | def method2(self):
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
141 |
142 | def method3(self):
|
= help: Remove `super()` parameters
UP008.py:143:14: UP008 [*] Use `super()` instead of `super(__class__, self)`
|
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008
|
= help: Remove `super()` parameters
Safe fix
140 140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
141 141 |
142 142 | def method3(self):
143 |- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 |+ super().some_method() # Should be fixed - no keywords

View File

@@ -292,7 +292,7 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
.chain(parsed.errors().iter().map(|parse_error| {
create_syntax_error_diagnostic(source_code.clone(), &parse_error.error, parse_error)
}))
.sorted()
.sorted_by(Diagnostic::ruff_start_ordering)
.collect();
(messages, transformed)
}
@@ -317,7 +317,7 @@ fn print_syntax_errors(errors: &[ParseError], path: &Path, source: &SourceKind)
/// Print the lint diagnostics in `diagnostics`.
fn print_diagnostics(mut diagnostics: Vec<Diagnostic>, path: &Path, source: &SourceKind) -> String {
diagnostics.retain(|msg| !msg.is_syntax_error());
diagnostics.retain(|msg| !msg.is_invalid_syntax());
if let Some(notebook) = source.as_ipy_notebook() {
print_jupyter_messages(&diagnostics, path, notebook)

View File

@@ -1,6 +1,10 @@
use std::fmt::{Debug, Display};
use crate::codes::Rule;
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::SourceFile;
use ruff_text_size::TextRange;
use crate::{codes::Rule, message::create_lint_diagnostic};
#[derive(Debug, Copy, Clone)]
pub enum FixAvailability {
@@ -28,7 +32,7 @@ pub trait ViolationMetadata {
fn explain() -> Option<&'static str>;
}
pub trait Violation: ViolationMetadata {
pub trait Violation: ViolationMetadata + Sized {
/// `None` in the case a fix is never available or otherwise Some
/// [`FixAvailability`] describing the available fix.
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
@@ -48,6 +52,20 @@ pub trait Violation: ViolationMetadata {
/// Returns the format strings used by [`message`](Violation::message).
fn message_formats() -> &'static [&'static str];
/// Convert the violation into a [`Diagnostic`].
fn into_diagnostic(self, range: TextRange, file: &SourceFile) -> Diagnostic {
create_lint_diagnostic(
self.message(),
self.fix_title(),
range,
None,
None,
file.clone(),
None,
Self::rule(),
)
}
}
/// This trait exists just to make implementing the [`Violation`] trait more

View File

@@ -0,0 +1,95 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{ImplItem, ItemImpl};
pub(crate) fn attribute_env_vars_metadata(mut input: ItemImpl) -> TokenStream {
// Verify that this is an impl for EnvVars
let impl_type = &input.self_ty;
let mut env_var_entries = Vec::new();
let mut hidden_vars = Vec::new();
// Process each item in the impl block
for item in &mut input.items {
if let ImplItem::Const(const_item) = item {
// Extract the const name and value
let const_name = &const_item.ident;
let const_expr = &const_item.expr;
// Check if the const has the #[attr_hidden] attribute
let is_hidden = const_item
.attrs
.iter()
.any(|attr| attr.path().is_ident("attr_hidden"));
// Remove our custom attributes
const_item.attrs.retain(|attr| {
!attr.path().is_ident("attr_hidden")
&& !attr.path().is_ident("attr_env_var_pattern")
});
if is_hidden {
hidden_vars.push(const_name.clone());
} else {
// Extract documentation from doc comments
let doc_attrs: Vec<_> = const_item
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.collect();
if !doc_attrs.is_empty() {
// Convert doc attributes to a single string
let doc_string = extract_doc_string(&doc_attrs);
env_var_entries.push((const_name.clone(), const_expr.clone(), doc_string));
}
}
}
}
// Generate the metadata method.
let metadata_entries: Vec<_> = env_var_entries
.iter()
.map(|(_name, expr, doc)| {
quote! {
(#expr, #doc)
}
})
.collect();
let metadata_impl = quote! {
impl #impl_type {
/// Returns metadata for all non-hidden environment variables.
pub fn metadata() -> Vec<(&'static str, &'static str)> {
vec![
#(#metadata_entries),*
]
}
}
};
quote! {
#input
#metadata_impl
}
}
/// Extract documentation from doc attributes into a single string
fn extract_doc_string(attrs: &[&syn::Attribute]) -> String {
attrs
.iter()
.filter_map(|attr| {
if let syn::Meta::NameValue(meta) = &attr.meta {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = &meta.value
{
return Some(lit_str.value().trim().to_string());
}
}
None
})
.collect::<Vec<_>>()
.join("\n")
}

View File

@@ -1,4 +1,4 @@
//! This crate implements internal macros for the `ruff` library.
//! This crate implements internal macros for the `ruff` and `ty` libraries.
use crate::cache_key::derive_cache_key;
use crate::newtype_index::generate_newtype_index;
@@ -11,6 +11,7 @@ mod combine;
mod combine_options;
mod config;
mod derive_message_formats;
mod env_vars;
mod kebab_case;
mod map_codes;
mod newtype_index;
@@ -144,3 +145,15 @@ pub fn newtype_index(_metadata: TokenStream, input: TokenStream) -> TokenStream
TokenStream::from(output)
}
/// Generates metadata for environment variables declared in the impl block.
///
/// This attribute macro should be applied to an `impl EnvVars` block.
/// It will generate a `metadata()` method that returns all non-hidden
/// environment variables with their documentation.
#[proc_macro_attribute]
pub fn attribute_env_vars_metadata(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as syn::ItemImpl);
env_vars::attribute_env_vars_metadata(input).into()
}

View File

@@ -952,6 +952,9 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start: _ } => {
write!(f, "name `{name}` is used prior to global declaration")
}
SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { name, start: _ } => {
write!(f, "name `{name}` is used prior to nonlocal declaration")
}
SemanticSyntaxErrorKind::InvalidStarExpression => {
f.write_str("Starred expression cannot be used here")
}
@@ -977,6 +980,18 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
write!(f, "nonlocal declaration not allowed at module level")
}
SemanticSyntaxErrorKind::NonlocalAndGlobal(name) => {
write!(f, "name `{name}` is nonlocal and global")
}
SemanticSyntaxErrorKind::AnnotatedGlobal(name) => {
write!(f, "annotated name `{name}` can't be global")
}
SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => {
write!(f, "annotated name `{name}` can't be nonlocal")
}
SemanticSyntaxErrorKind::InvalidNonlocal(name) => {
write!(f, "no binding for nonlocal `{name}` found")
}
}
}
}
@@ -1207,6 +1222,24 @@ pub enum SemanticSyntaxErrorKind {
/// [#111123]: https://github.com/python/cpython/issues/111123
LoadBeforeGlobalDeclaration { name: String, start: TextSize },
/// Represents the use of a `nonlocal` variable before its `nonlocal` declaration.
///
/// ## Examples
///
/// ```python
/// def f():
/// counter = 0
/// def increment():
/// print(f"Adding 1 to {counter}")
/// nonlocal counter # SyntaxError: name 'counter' is used prior to nonlocal declaration
/// counter += 1
/// ```
///
/// ## Known Issues
///
/// See [`LoadBeforeGlobalDeclaration`][Self::LoadBeforeGlobalDeclaration].
LoadBeforeNonlocalDeclaration { name: String, start: TextSize },
/// Represents the use of a starred expression in an invalid location, such as a `return` or
/// `yield` statement.
///
@@ -1307,6 +1340,41 @@ pub enum SemanticSyntaxErrorKind {
/// Represents a nonlocal declaration at module level
NonlocalDeclarationAtModuleLevel,
/// Represents the same variable declared as both nonlocal and global
NonlocalAndGlobal(String),
/// Represents a type annotation on a variable that's been declared global
AnnotatedGlobal(String),
/// Represents a type annotation on a variable that's been declared nonlocal
AnnotatedNonlocal(String),
/// Represents a nonlocal declaration with no definition in an enclosing scope
///
/// ## Examples
///
/// ```python
/// def f():
/// nonlocal x # error
///
/// # Global variables don't count.
/// x = 1
/// def f():
/// nonlocal x # error
///
/// def f():
/// x = 1
/// def g():
/// nonlocal x # allowed
///
/// # The definition can come later.
/// def f():
/// def g():
/// nonlocal x # allowed
/// x = 1
/// ```
InvalidNonlocal(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]

View File

@@ -163,7 +163,7 @@ pub(crate) fn check(
.into_iter()
.zip(noqa_edits)
.filter_map(|(message, noqa_edit)| {
if message.is_syntax_error() && !show_syntax_errors {
if message.is_invalid_syntax() && !show_syntax_errors {
None
} else {
Some(to_lsp_diagnostic(

View File

@@ -19,13 +19,13 @@ ruff_python_ast = { workspace = true }
ty_python_semantic = { workspace = true }
ty_project = { workspace = true, features = ["zstd"] }
ty_server = { workspace = true }
ty_static = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }
clap = { workspace = true, features = ["wrap_help", "string", "env"] }
clap_complete_command = { workspace = true }
colored = { workspace = true }
countme = { workspace = true, features = ["enable"] }
crossbeam = { workspace = true }
ctrlc = { version = "3.4.4" }
indicatif = { workspace = true }

3
crates/ty/docs/cli.md generated
View File

@@ -84,7 +84,8 @@ over all configuration files.</p>
<li><code>3.11</code></li>
<li><code>3.12</code></li>
<li><code>3.13</code></li>
</ul></dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-gitignore</code> to disable</p>
</ul></dd><dt id="ty-check--quiet"><a href="#ty-check--quiet"><code>--quiet</code></a></dt><dd><p>Use quiet output</p>
</dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-gitignore</code> to disable</p>
</dd><dt id="ty-check--typeshed"><a href="#ty-check--typeshed"><code>--typeshed</code></a>, <code>--custom-typeshed-dir</code> <i>path</i></dt><dd><p>Custom directory to use for stdlib typeshed stubs</p>
</dd><dt id="ty-check--verbose"><a href="#ty-check--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output (or <code>-vv</code> and <code>-vvv</code> for more verbose output)</p>
</dd><dt id="ty-check--warn"><a href="#ty-check--warn"><code>--warn</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'warn'. Can be specified multiple times.</p>

View File

@@ -0,0 +1,55 @@
# Environment variables
ty defines and respects the following environment variables:
### `TY_LOG`
If set, ty will use this value as the log level for its `--verbose` output.
Accepts any filter compatible with the `tracing_subscriber` crate.
For example:
- `TY_LOG=uv=debug` is the equivalent of `-vv` to the command line
- `TY_LOG=trace` will enable all trace-level logging.
See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
for more.
### `TY_LOG_PROFILE`
If set to `"1"` or `"true"`, ty will enable flamegraph profiling.
This creates a `tracing.folded` file that can be used to generate flame graphs
for performance analysis.
### `TY_MAX_PARALLELISM`
Specifies an upper limit for the number of tasks ty is allowed to run in parallel.
For example, how many files should be checked in parallel.
This isn't the same as a thread limit. ty may spawn additional threads
when necessary, e.g. to watch for file system changes or a dedicated UI thread.
## Externally-defined variables
ty also reads the following externally defined environment variables:
### `CONDA_PREFIX`
Used to detect an activated Conda environment location.
If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred.
### `RAYON_NUM_THREADS`
Specifies an upper limit for the number of threads ty uses when performing work in parallel.
Equivalent to `TY_MAX_PARALLELISM`.
This is a standard Rayon environment variable.
### `VIRTUAL_ENV`
Used to detect an activated virtual environment.
### `XDG_CONFIG_HOME`
Path to user-level configuration directory on Unix systems.

View File

@@ -1,11 +1,13 @@
mod args;
mod logging;
mod printer;
mod python_version;
mod version;
pub use args::Cli;
use ty_static::EnvVars;
use std::io::{self, BufWriter, Write, stdout};
use std::fmt::Write;
use std::process::{ExitCode, Termination};
use anyhow::Result;
@@ -13,6 +15,7 @@ use std::sync::Mutex;
use crate::args::{CheckCommand, Command, TerminalColor};
use crate::logging::setup_tracing;
use crate::printer::Printer;
use anyhow::{Context, anyhow};
use clap::{CommandFactory, Parser};
use colored::Colorize;
@@ -24,7 +27,7 @@ use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
use ty_project::metadata::options::ProjectOptionsOverrides;
use ty_project::watch::ProjectWatcher;
use ty_project::{Db, DummyReporter, Reporter, watch};
use ty_project::{Db, watch};
use ty_project::{ProjectDatabase, ProjectMetadata};
use ty_server::run_server;
@@ -41,6 +44,8 @@ pub fn run() -> anyhow::Result<ExitStatus> {
Command::Check(check_args) => run_check(check_args),
Command::Version => version().map(|()| ExitStatus::Success),
Command::GenerateShellCompletion { shell } => {
use std::io::stdout;
shell.generate(&mut Cli::command(), &mut stdout());
Ok(ExitStatus::Success)
}
@@ -48,7 +53,7 @@ pub fn run() -> anyhow::Result<ExitStatus> {
}
pub(crate) fn version() -> Result<()> {
let mut stdout = BufWriter::new(io::stdout().lock());
let mut stdout = Printer::default().stream_for_requested_summary().lock();
let version_info = crate::version::version();
writeln!(stdout, "ty {}", &version_info)?;
Ok(())
@@ -58,9 +63,10 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
set_colored_override(args.color);
let verbosity = args.verbosity.level();
countme::enable(verbosity.is_trace());
let _guard = setup_tracing(verbosity, args.color.unwrap_or_default())?;
let printer = Printer::default().with_verbosity(verbosity);
tracing::warn!(
"ty is pre-release software and not ready for production use. \
Expect to encounter bugs, missing features, and fatal errors.",
@@ -125,7 +131,8 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
}
let project_options_overrides = ProjectOptionsOverrides::new(config_file, options);
let (main_loop, main_loop_cancellation_token) = MainLoop::new(project_options_overrides);
let (main_loop, main_loop_cancellation_token) =
MainLoop::new(project_options_overrides, printer);
// Listen to Ctrl+C and abort the watch mode.
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -143,16 +150,14 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
main_loop.run(&mut db)?
};
let mut stdout = stdout().lock();
match std::env::var("TY_MEMORY_REPORT").as_deref() {
let mut stdout = printer.stream_for_requested_summary().lock();
match std::env::var(EnvVars::TY_MEMORY_REPORT).as_deref() {
Ok("short") => write!(stdout, "{}", db.salsa_memory_dump().display_short())?,
Ok("mypy_primer") => write!(stdout, "{}", db.salsa_memory_dump().display_mypy_primer())?,
Ok("full") => write!(stdout, "{}", db.salsa_memory_dump().display_full())?,
_ => {}
}
tracing::trace!("Counts for entire CLI run:\n{}", countme::get_all());
std::mem::forget(db);
if exit_zero {
@@ -194,12 +199,16 @@ struct MainLoop {
/// The file system watcher, if running in watch mode.
watcher: Option<ProjectWatcher>,
/// Interface for displaying information to the user.
printer: Printer,
project_options_overrides: ProjectOptionsOverrides,
}
impl MainLoop {
fn new(
project_options_overrides: ProjectOptionsOverrides,
printer: Printer,
) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
@@ -209,6 +218,7 @@ impl MainLoop {
receiver,
watcher: None,
project_options_overrides,
printer,
},
MainLoopCancellationToken { sender },
)
@@ -225,32 +235,24 @@ impl MainLoop {
// Do not show progress bars with `--watch`, indicatif does not seem to
// handle cancelling independent progress bars very well.
self.run_with_progress::<DummyReporter>(db)?;
// TODO(zanieb): We can probably use `MultiProgress` to handle this case in the future.
self.printer = self.printer.with_no_progress();
self.run(db)?;
Ok(ExitStatus::Success)
}
fn run(self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
self.run_with_progress::<IndicatifReporter>(db)
}
fn run_with_progress<R>(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus>
where
R: Reporter + Default + 'static,
{
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
let result = self.main_loop::<R>(db);
let result = self.main_loop(db);
tracing::debug!("Exiting main loop");
result
}
fn main_loop<R>(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus>
where
R: Reporter + Default + 'static,
{
fn main_loop(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
// Schedule the first check.
tracing::debug!("Starting main loop");
@@ -266,7 +268,7 @@ impl MainLoop {
// to prevent blocking the main loop here.
rayon::spawn(move || {
match salsa::Cancelled::catch(|| {
let mut reporter = R::default();
let mut reporter = IndicatifReporter::from(self.printer);
db.check_with_reporter(&mut reporter)
}) {
Ok(result) => {
@@ -301,10 +303,12 @@ impl MainLoop {
return Ok(ExitStatus::Success);
}
let mut stdout = stdout().lock();
if result.is_empty() {
writeln!(stdout, "{}", "All checks passed!".green().bold())?;
writeln!(
self.printer.stream_for_success_summary(),
"{}",
"All checks passed!".green().bold()
)?;
if self.watcher.is_none() {
return Ok(ExitStatus::Success);
@@ -313,14 +317,19 @@ impl MainLoop {
let mut max_severity = Severity::Info;
let diagnostics_count = result.len();
let mut stdout = self.printer.stream_for_details().lock();
for diagnostic in result {
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
// Only render diagnostics if they're going to be displayed, since doing
// so is expensive.
if stdout.is_enabled() {
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
}
max_severity = max_severity.max(diagnostic.severity());
}
writeln!(
stdout,
self.printer.stream_for_failure_summary(),
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
@@ -352,8 +361,6 @@ impl MainLoop {
"Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"
);
}
tracing::trace!("Counts after last check:\n{}", countme::get_all());
}
MainLoopMessage::ApplyChanges(changes) => {
@@ -382,27 +389,53 @@ impl MainLoop {
}
/// A progress reporter for `ty check`.
#[derive(Default)]
struct IndicatifReporter(Option<indicatif::ProgressBar>);
enum IndicatifReporter {
/// A constructed reporter that is not yet ready, contains the target for the progress bar.
Pending(indicatif::ProgressDrawTarget),
/// A reporter that is ready, containing a progress bar to report to.
///
/// Initialization of the bar is deferred to [`ty_project::ProgressReporter::set_files`] so we
/// do not initialize the bar too early as it may take a while to collect the number of files to
/// process and we don't want to display an empty "0/0" bar.
Initialized(indicatif::ProgressBar),
}
impl ty_project::Reporter for IndicatifReporter {
impl From<Printer> for IndicatifReporter {
fn from(printer: Printer) -> Self {
Self::Pending(printer.progress_target())
}
}
impl ty_project::ProgressReporter for IndicatifReporter {
fn set_files(&mut self, files: usize) {
let progress = indicatif::ProgressBar::new(files as u64);
progress.set_style(
let target = match std::mem::replace(
self,
IndicatifReporter::Pending(indicatif::ProgressDrawTarget::hidden()),
) {
Self::Pending(target) => target,
Self::Initialized(_) => panic!("The progress reporter should only be initialized once"),
};
let bar = indicatif::ProgressBar::with_draw_target(Some(files as u64), target);
bar.set_style(
indicatif::ProgressStyle::with_template(
"{msg:8.dim} {bar:60.green/dim} {pos}/{len} files",
)
.unwrap()
.progress_chars("--"),
);
progress.set_message("Checking");
self.0 = Some(progress);
bar.set_message("Checking");
*self = Self::Initialized(bar);
}
fn report_file(&self, _file: &ruff_db::files::File) {
if let Some(ref progress_bar) = self.0 {
progress_bar.inc(1);
match self {
IndicatifReporter::Initialized(progress_bar) => {
progress_bar.inc(1);
}
IndicatifReporter::Pending(_) => {
panic!("`report_file` called before `set_files`")
}
}
}
}

View File

@@ -12,6 +12,7 @@ use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::registry::LookupSpan;
use ty_static::EnvVars;
/// Logging flags to `#[command(flatten)]` into your CLI
#[derive(clap::Args, Debug, Clone, Default)]
@@ -23,15 +24,32 @@ pub(crate) struct Verbosity {
help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)",
action = clap::ArgAction::Count,
global = true,
overrides_with = "quiet",
)]
verbose: u8,
#[arg(
long,
help = "Use quiet output",
action = clap::ArgAction::Count,
global = true,
overrides_with = "verbose",
)]
quiet: u8,
}
impl Verbosity {
/// Returns the verbosity level based on the number of `-v` flags.
/// Returns the verbosity level based on the number of `-v` and `-q` flags.
///
/// Returns `None` if the user did not specify any verbosity flags.
pub(crate) fn level(&self) -> VerbosityLevel {
// `--quiet` and `--verbose` are mutually exclusive in Clap, so we can just check one first.
match self.quiet {
0 => {}
_ => return VerbosityLevel::Quiet,
// TODO(zanieb): Add support for `-qq` with a "silent" mode
}
match self.verbose {
0 => VerbosityLevel::Default,
1 => VerbosityLevel::Verbose,
@@ -41,9 +59,14 @@ impl Verbosity {
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)]
pub(crate) enum VerbosityLevel {
/// Quiet output. Only shows Ruff and ty events up to the [`ERROR`](tracing::Level::ERROR).
/// Silences output except for summary information.
Quiet,
/// Default output level. Only shows Ruff and ty events up to the [`WARN`](tracing::Level::WARN).
#[default]
Default,
/// Enables verbose output. Emits Ruff and ty events up to the [`INFO`](tracing::Level::INFO).
@@ -61,6 +84,7 @@ pub(crate) enum VerbosityLevel {
impl VerbosityLevel {
const fn level_filter(self) -> LevelFilter {
match self {
VerbosityLevel::Quiet => LevelFilter::ERROR,
VerbosityLevel::Default => LevelFilter::WARN,
VerbosityLevel::Verbose => LevelFilter::INFO,
VerbosityLevel::ExtraVerbose => LevelFilter::DEBUG,
@@ -84,7 +108,7 @@ pub(crate) fn setup_tracing(
use tracing_subscriber::prelude::*;
// The `TY_LOG` environment variable overrides the default log level.
let filter = if let Ok(log_env_variable) = std::env::var("TY_LOG") {
let filter = if let Ok(log_env_variable) = std::env::var(EnvVars::TY_LOG) {
EnvFilter::builder()
.parse(log_env_variable)
.context("Failed to parse directives specified in TY_LOG environment variable.")?
@@ -165,7 +189,7 @@ fn setup_profile<S>() -> (
where
S: Subscriber + for<'span> LookupSpan<'span>,
{
if let Ok("1" | "true") = std::env::var("TY_LOG_PROFILE").as_deref() {
if let Ok("1" | "true") = std::env::var(EnvVars::TY_LOG_PROFILE).as_deref() {
let (layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded")
.expect("Flame layer to be created");
(Some(layer), Some(guard))

172
crates/ty/src/printer.rs Normal file
View File

@@ -0,0 +1,172 @@
use std::io::StdoutLock;
use indicatif::ProgressDrawTarget;
use crate::logging::VerbosityLevel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Printer {
verbosity: VerbosityLevel,
no_progress: bool,
}
impl Printer {
#[must_use]
pub(crate) fn with_no_progress(self) -> Self {
Self {
verbosity: self.verbosity,
no_progress: true,
}
}
#[must_use]
pub(crate) fn with_verbosity(self, verbosity: VerbosityLevel) -> Self {
Self {
verbosity,
no_progress: self.no_progress,
}
}
/// Return the [`ProgressDrawTarget`] for this printer.
pub(crate) fn progress_target(self) -> ProgressDrawTarget {
if self.no_progress {
return ProgressDrawTarget::hidden();
}
match self.verbosity {
VerbosityLevel::Quiet => ProgressDrawTarget::hidden(),
VerbosityLevel::Default => ProgressDrawTarget::stderr(),
// Hide the progress bar when in verbose mode.
// Otherwise, it gets interleaved with log messages.
VerbosityLevel::Verbose => ProgressDrawTarget::hidden(),
VerbosityLevel::ExtraVerbose => ProgressDrawTarget::hidden(),
VerbosityLevel::Trace => ProgressDrawTarget::hidden(),
}
}
/// Return the [`Stdout`] stream for important messages.
///
/// Unlike [`Self::stdout_general`], the returned stream will be enabled when
/// [`VerbosityLevel::Quiet`] is used.
fn stdout_important(self) -> Stdout {
match self.verbosity {
VerbosityLevel::Quiet => Stdout::enabled(),
VerbosityLevel::Default => Stdout::enabled(),
VerbosityLevel::Verbose => Stdout::enabled(),
VerbosityLevel::ExtraVerbose => Stdout::enabled(),
VerbosityLevel::Trace => Stdout::enabled(),
}
}
/// Return the [`Stdout`] stream for general messages.
///
/// The returned stream will be disabled when [`VerbosityLevel::Quiet`] is used.
fn stdout_general(self) -> Stdout {
match self.verbosity {
VerbosityLevel::Quiet => Stdout::disabled(),
VerbosityLevel::Default => Stdout::enabled(),
VerbosityLevel::Verbose => Stdout::enabled(),
VerbosityLevel::ExtraVerbose => Stdout::enabled(),
VerbosityLevel::Trace => Stdout::enabled(),
}
}
/// Return the [`Stdout`] stream for a summary message that was explicitly requested by the
/// user.
///
/// For example, in `ty version` the user has requested the version information and we should
/// display it even if [`VerbosityLevel::Quiet`] is used. Or, in `ty check`, if the
/// `TY_MEMORY_REPORT` variable has been set, we should display the memory report because the
/// user has opted-in to display.
pub(crate) fn stream_for_requested_summary(self) -> Stdout {
self.stdout_important()
}
/// Return the [`Stdout`] stream for a summary message on failure.
///
/// For example, in `ty check`, this would be used for the message indicating the number of
/// diagnostics found. The failure summary should capture information that is not reflected in
/// the exit code.
pub(crate) fn stream_for_failure_summary(self) -> Stdout {
self.stdout_important()
}
/// Return the [`Stdout`] stream for a summary message on success.
///
/// For example, in `ty check`, this would be used for the message indicating that no diagnostic
/// were found. The success summary does not capture important information for users that have
/// opted-in to [`VerbosityLevel::Quiet`].
pub(crate) fn stream_for_success_summary(self) -> Stdout {
self.stdout_general()
}
/// Return the [`Stdout`] stream for detailed messages.
///
/// For example, in `ty check`, this would be used for the diagnostic output.
pub(crate) fn stream_for_details(self) -> Stdout {
self.stdout_general()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StreamStatus {
Enabled,
Disabled,
}
#[derive(Debug)]
pub(crate) struct Stdout {
status: StreamStatus,
lock: Option<StdoutLock<'static>>,
}
impl Stdout {
fn enabled() -> Self {
Self {
status: StreamStatus::Enabled,
lock: None,
}
}
fn disabled() -> Self {
Self {
status: StreamStatus::Disabled,
lock: None,
}
}
pub(crate) fn lock(mut self) -> Self {
match self.status {
StreamStatus::Enabled => {
// Drop the previous lock first, to avoid deadlocking
self.lock.take();
self.lock = Some(std::io::stdout().lock());
}
StreamStatus::Disabled => self.lock = None,
}
self
}
fn handle(&mut self) -> Box<dyn std::io::Write + '_> {
match self.lock.as_mut() {
Some(lock) => Box::new(lock),
None => Box::new(std::io::stdout()),
}
}
pub(crate) fn is_enabled(&self) -> bool {
matches!(self.status, StreamStatus::Enabled)
}
}
impl std::fmt::Write for Stdout {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
match self.status {
StreamStatus::Enabled => {
let _ = write!(self.handle(), "{s}");
Ok(())
}
StreamStatus::Disabled => Ok(()),
}
}
}

View File

@@ -14,6 +14,64 @@ use std::{
};
use tempfile::TempDir;
#[test]
fn test_quiet_output() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", "x: int = 1")?;
// By default, we emit an "all checks passed" message
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// With `quiet`, the message is not displayed
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let case = CliTest::with_file("test.py", "x: int = 'foo'")?;
// By default, we emit a diagnostic
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int`
--> test.py:1:1
|
1 | x: int = 'foo'
| ^
|
info: rule `invalid-assignment` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
// With `quiet`, the diagnostic is not displayed, just the summary message
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
success: false
exit_code: 1
----- stdout -----
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn test_run_in_sub_directory() -> anyhow::Result<()> {
let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?;

View File

@@ -10,7 +10,7 @@ use ty_python_semantic::{Completion, NameKind, SemanticModel};
use crate::Db;
use crate::find_node::covering_node;
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion> {
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion<'_>> {
let parsed = parsed_module(db, file).load(db);
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
@@ -1223,33 +1223,33 @@ quux.<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins(), @r"
bar
baz
foo
__annotations__
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__format__
__getattribute__
__getstate__
__hash__
__init__
__init_subclass__
__module__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
bar :: Unknown | Literal[2]
baz :: Unknown | Literal[3]
foo :: Unknown | Literal[1]
__annotations__ :: dict[str, Any]
__class__ :: type
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
__dict__ :: dict[str, Any]
__dir__ :: bound method object.__dir__() -> Iterable[str]
__doc__ :: str | None
__eq__ :: bound method object.__eq__(value: object, /) -> bool
__format__ :: bound method object.__format__(format_spec: str, /) -> str
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
__getstate__ :: bound method object.__getstate__() -> object
__hash__ :: bound method object.__hash__() -> int
__init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ :: bound method object.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method object.__ne__(value: object, /) -> bool
__new__ :: bound method object.__new__() -> Self
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method object.__repr__() -> str
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
__sizeof__ :: bound method object.__sizeof__() -> int
__str__ :: bound method object.__str__() -> str
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
");
}
@@ -1268,33 +1268,33 @@ quux.b<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins(), @r"
bar
baz
foo
__annotations__
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__format__
__getattribute__
__getstate__
__hash__
__init__
__init_subclass__
__module__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
bar :: Unknown | Literal[2]
baz :: Unknown | Literal[3]
foo :: Unknown | Literal[1]
__annotations__ :: dict[str, Any]
__class__ :: type
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
__dict__ :: dict[str, Any]
__dir__ :: bound method object.__dir__() -> Iterable[str]
__doc__ :: str | None
__eq__ :: bound method object.__eq__(value: object, /) -> bool
__format__ :: bound method object.__format__(format_spec: str, /) -> str
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
__getstate__ :: bound method object.__getstate__() -> object
__hash__ :: bound method object.__hash__() -> int
__init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ :: bound method object.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method object.__ne__(value: object, /) -> bool
__new__ :: bound method object.__new__() -> Self
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method object.__repr__() -> str
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
__sizeof__ :: bound method object.__sizeof__() -> int
__str__ :: bound method object.__str__() -> str
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
");
}
@@ -1321,6 +1321,89 @@ class Quux:
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
}
#[test]
fn class_attributes1() {
let test = cursor_test(
"\
class Quux:
some_attribute: int = 1
def __init__(self):
self.foo = 1
self.bar = 2
self.baz = 3
def some_method(self) -> int:
return 1
@property
def some_property(self) -> int:
return 1
@classmethod
def some_class_method(self) -> int:
return 1
@staticmethod
def some_static_method(self) -> int:
return 1
Quux.<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
mro :: def mro(self) -> list[type]
some_attribute :: int
some_class_method :: bound method <class 'Quux'>.some_class_method() -> int
some_method :: def some_method(self) -> int
some_property :: property
some_static_method :: def some_static_method(self) -> int
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
__basicsize__ :: int
__call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any
__class__ :: <class 'type'>
__delattr__ :: def __delattr__(self, name: str, /) -> None
__dict__ :: MappingProxyType[str, Any]
__dictoffset__ :: int
__dir__ :: def __dir__(self) -> Iterable[str]
__doc__ :: str | None
__eq__ :: def __eq__(self, value: object, /) -> bool
__flags__ :: int
__format__ :: def __format__(self, format_spec: str, /) -> str
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
__getstate__ :: def __getstate__(self) -> object
__hash__ :: def __hash__(self) -> int
__init__ :: def __init__(self) -> Unknown
__init_subclass__ :: def __init_subclass__(cls) -> None
__instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool
__itemsize__ :: int
__module__ :: str
__mro__ :: tuple[<class 'type'>, <class 'object'>]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls) -> Self
__or__ :: def __or__(self, value: Any, /) -> UnionType
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: def __repr__(self) -> str
__ror__ :: def __ror__(self, value: Any, /) -> UnionType
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool
__subclasses__ :: def __subclasses__(self: Self) -> list[Self]
__subclasshook__ :: bound method <class 'object'>.__subclasshook__(subclass: type, /) -> bool
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
__weakrefoffset__ :: int
");
}
// We don't yet take function parameters into account.
#[test]
fn call_prefix1() {
@@ -2366,7 +2449,22 @@ importlib.<CURSOR>
self.completions_if(|c| !c.builtin)
}
fn completions_without_builtins_with_types(&self) -> String {
self.completions_if_snapshot(
|c| !c.builtin,
|c| format!("{} :: {}", c.name, c.ty.display(&self.db)),
)
}
fn completions_if(&self, predicate: impl Fn(&Completion) -> bool) -> String {
self.completions_if_snapshot(predicate, |c| c.name.as_str().to_string())
}
fn completions_if_snapshot(
&self,
predicate: impl Fn(&Completion) -> bool,
snapshot: impl Fn(&Completion) -> String,
) -> String {
let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
if completions.is_empty() {
return "<No completions found>".to_string();
@@ -2374,7 +2472,7 @@ importlib.<CURSOR>
let included = completions
.iter()
.filter(|label| predicate(label))
.map(|completion| completion.name.as_str().to_string())
.map(snapshot)
.collect::<Vec<String>>();
if included.is_empty() {
// It'd be nice to include the actual number of

View File

@@ -242,22 +242,6 @@ mod tests {
}
pub(super) fn render_diagnostics<I, D>(&self, diagnostics: I) -> String
where
I: IntoIterator<Item = D>,
D: IntoDiagnostic,
{
let config = DisplayDiagnosticConfig::default()
.color(false)
.format(DiagnosticFormat::Full);
self.render_diagnostics_with_config(diagnostics, &config)
}
pub(super) fn render_diagnostics_with_config<I, D>(
&self,
diagnostics: I,
config: &DisplayDiagnosticConfig,
) -> String
where
I: IntoIterator<Item = D>,
D: IntoDiagnostic,
@@ -266,9 +250,12 @@ mod tests {
let mut buf = String::new();
let config = DisplayDiagnosticConfig::default()
.color(false)
.format(DiagnosticFormat::Full);
for diagnostic in diagnostics {
let diag = diagnostic.into_diagnostic();
write!(buf, "{}", diag.display(&self.db, config)).unwrap();
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
}
buf

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ use std::{cmp, fmt};
use crate::metadata::settings::file_settings;
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
use crate::{Project, ProjectMetadata, Reporter};
use crate::{ProgressReporter, Project, ProjectMetadata};
use ruff_db::Db as SourceDb;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files};
@@ -87,7 +87,7 @@ impl ProjectDatabase {
}
/// Checks all open files in the project and its dependencies, using the given reporter.
pub fn check_with_reporter(&self, reporter: &mut dyn Reporter) -> Vec<Diagnostic> {
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
let reporter = AssertUnwindSafe(reporter);
self.project().check(self, CheckMode::OpenFiles, reporter)
}
@@ -95,7 +95,7 @@ impl ProjectDatabase {
/// Check the project with the given mode.
pub fn check_with_mode(&self, mode: CheckMode) -> Vec<Diagnostic> {
let mut reporter = DummyReporter;
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn Reporter);
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn ProgressReporter);
self.project().check(self, mode, reporter)
}

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