Compare commits

...

154 Commits

Author SHA1 Message Date
konstin
7f97547b5f Add increment/decrement 2024-03-14 16:56:06 +01:00
Hoël Bagard
e944c16c46 [pycodestyle] Do not ignore lines before the first logical line in blank lines rules (#10382)
## Summary

Ignoring all lines until the first logical line does not match the
behavior from pycodestyle. This PR therefore removes the `if
state.is_not_first_logical_line` skipping the line check before the
first logical line, and applies it only to `E302`.

For example, in the snippet below a rule violation should be detected on
the second comment and on the import.

```python
# first comment




# second comment




import foo
```

Fixes #10374

## Test Plan

Add test cases, update the snapshots and verify the ecosystem check output
2024-03-14 14:05:24 +05:30
Dhruv Manilawala
5f40371ffc Use ExprFString for StringLike::FString variant (#10311)
## Summary

This PR updates the `StringLike::FString` variant to use `ExprFString`
instead of `FStringLiteralElement`.

For context, the reason it used `FStringLiteralElement` is that the node
is actually the string part of an f-string ("foo" in `f"foo{x}"`). But,
this is inconsistent with other variants where the captured value is the
_entire_ string.

This is also problematic w.r.t. implicitly concatenated strings. Any
rules which work with `StringLike::FString` doesn't account for the
string part in an implicitly concatenated f-strings. For example, we
don't flag confusable character in the first part of `"𝐁ad" f"𝐁ad
string"`, but only the second part
(https://play.ruff.rs/16071c4c-a1dd-4920-b56f-e2ce2f69c843).

### Update `PYI053`

_This is included in this PR because otherwise it requires a temporary
workaround to be compatible with the old logic._

This PR also updates the `PYI053` (`string-or-bytes-too-long`) rule for
f-string to consider _all_ the visible characters in a f-string,
including the ones which are implicitly concatenated. This is consistent
with implicitly concatenated strings and bytes.

For example,

```python
def foo(
	# We count all the characters here
    arg1: str = '51 character ' 'stringgggggggggggggggggggggggggggggggg',
	# But not here because of the `{x}` replacement field which _breaks_ them up into two chunks
    arg2: str = f'51 character {x} stringgggggggggggggggggggggggggggggggggggggggggggg',
) -> None: ...
```

This PR fixes it to consider all _visible_ characters inside an f-string
which includes expressions as well.

fixes: #10310 
fixes: #10307 

## Test Plan

Add new test cases and update the snapshots.

## Review

To facilitate the review process, the change have been split into two
commits: one which has the code change while the other has the test
cases and updated snapshots.
2024-03-14 13:30:22 +05:30
boolean
f7802ad5de [pylint] Extend docs and test in invalid-str-return-type (E307) (#10400)
## Summary

Added some docs, and a little of test cases in
`invalid-str-return-type`, mentioned in
https://github.com/astral-sh/ruff/pull/10377#pullrequestreview-1934295027

## Test Plan

On `invalid_return_type_str.py`.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-03-14 04:38:30 +00:00
Jane Lewis
e832327a56 Require --preview for ruff server (#10368)
## Summary

Fixes #10367.

While the server is still in an unstable state, requiring a `--preview`
flag would be a good way to indicate this to end users.
2024-03-13 23:52:44 +00:00
Charlie Marsh
324390607c [pylint] Include builtin warnings in useless-exception-statement (PLW0133) (#10394)
## Summary

Closes https://github.com/astral-sh/ruff/issues/10392.
2024-03-13 15:26:11 -04:00
Tri Ho
4db5c29f19 Indicated Successful Check (#8631)
<!--
Thank you for contributing to Ruff! 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?
- Does this pull request include references to any relevant issues?
-->

## Summary

Adds a successful check message after no errors were found 
Implements #8553 

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

## Test Plan

Ran a check on a test file with `cargo run -p ruff_cli -- check test.py
--no-cache` and outputted as expected.

Ran the same check with `cargo run -p ruff_cli -- check test.py
--no-cache --silent` and the command was gone as expected.

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

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
2024-03-13 19:07:11 +00:00
Charlie Marsh
e9d3f71c90 Use ruff.toml format in README (#10393)
## Summary

See feedback in
https://github.com/astral-sh/ruff/issues/4725#issuecomment-1994615409.

In the docs, we use a tabbed interface for express `ruff.toml` vs.
`pyproject.toml`. Here, it might be clearer to default to `ruff.toml`,
since it's more obviously _not_ `pyproject.toml`. But either way, this
PR attempts to clarify that there's a difference.
2024-03-13 18:45:34 +00:00
Zanie Blue
7b3ee2daff Remove F401 fix for __init__ imports by default and allow opt-in to unsafe fix (#10365)
Re-implementation of https://github.com/astral-sh/ruff/pull/5845 but
instead of deprecating the option I toggle the default. Now users can
_opt-in_ via the setting which will give them an unsafe fix to remove
the import. Otherwise, we raise violations but do not offer a fix. The
setting is a bit of a misnomer in either case, maybe we'll want to
remove it still someday.

As discussed there, I think the safe fix should be to import it as an
alias. I'm not sure. We need support for offering multiple fixes for
ideal behavior though? I think we should remove the fix entirely and
consider it separately.

Closes https://github.com/astral-sh/ruff/issues/5697
Supersedes https://github.com/astral-sh/ruff/pull/5845

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-03-13 12:58:25 -05:00
Alex Waygood
c2e15f38ee Unify enums used for internal representation of quoting style (#10383) 2024-03-13 17:19:17 +00:00
Charlie Marsh
d59433b12e Avoid removing shadowed imports that point to different symbols (#10387)
This ensures that we don't have incorrect, automated fixes for shadowed
names that actually point to different imports.

See: https://github.com/astral-sh/ruff/issues/10384.
2024-03-13 15:44:28 +00:00
Hoël Bagard
2bf1882398 docs: remove . from check and format commands (#10217)
## Summary

This PR modifies the documentation to use `ruff check` instead of `ruff
check .`, and `ruff format` instead of `ruff format .`, as discussed
[here](https://github.com/astral-sh/ruff/pull/10168#discussion_r1509976904)

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Zanie Blue <contact@zanie.dev>
2024-03-13 10:10:48 -05:00
boolean
c269c1a706 [pylint] Implement invalid-bool-return-type (E304) (#10377)
## Summary

Implement `E304` in the issue #970. Throws an error when the returning value
of `__bool__` method is not boolean.

Reference: https://pylint.readthedocs.io/en/stable/user_guide/messages/error/invalid-bool-returned.html

## Test Plan

Add test cases and run `cargo test`
2024-03-13 19:43:45 +05:30
Dhruv Manilawala
32d6f84e3d Add methods to iter over f-string elements (#10309)
## Summary

This PR adds methods on `FString` to iterate over the two different kind
of elements it can have - literals and expressions. This is similar to
the methods we have on `ExprFString`.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-03-13 08:46:55 +00:00
Auguste Lalande
93d582d734 Avoid TRIO115 if the argument is a variable (#10376)
## Summary

Fix "TRIO115 false positive with with sleep(var) where var starts as 0"
#9935 based on the discussion in the issue.

## Test Plan

Issue code added to fixture
2024-03-13 13:09:18 +05:30
KotlinIsland
05b406080a (🎁) Add issue template search terms section (#10352)
- resolves #10350

Co-authored-by: KotlinIsland <kotlinisland@users.noreply.github.com>
2024-03-12 22:32:42 -05:00
Auguste Lalande
3ed707f245 Spellcheck & grammar (#10375)
## Summary

I used `codespell` and `gramma` to identify mispellings and grammar
errors throughout the codebase and fixed them. I tried not to make any
controversial changes, but feel free to revert as you see fit.
2024-03-13 02:34:23 +00:00
Charlie Marsh
c56fb6e15a Sort hash maps in Settings display (#10370)
## Summary

We had a report of a test failure on a specific architecture, and
looking into it, I think the test assumes that the hash keys are
iterated in a specific order. This PR thus adds a variant to our
settings display macro specifically for maps and sets. Like `CacheKey`,
it sorts the keys when printing.

Closes https://github.com/astral-sh/ruff/issues/10359.
2024-03-12 15:59:38 -04:00
Charlie Marsh
dbf82233b8 Gate f-string struct size test for Rustc < 1.76 (#10371)
Closes https://github.com/astral-sh/ruff/issues/10319.
2024-03-12 15:46:36 -04:00
Zanie Blue
87afe36c87 Add test case for F401 in __init__ files (#10364)
In preparation for https://github.com/astral-sh/ruff/pull/5845
2024-03-12 13:30:17 -05:00
Alex Waygood
704fefc7ab F821: Fix false negatives in .py files when from __future__ import annotations is active (#10362) 2024-03-12 17:07:44 +00:00
Auguste Lalande
dacec7377c Fix Indexer fails to identify continuation preceded by newline #10351 (#10354)
## Summary

Fixes #10351

It seems the bug was caused by this section of code

b669306c87/crates/ruff_python_index/src/indexer.rs (L55-L58)

It's true that newline tokens cannot be immediately followed by line
continuations, but only outside parentheses. e.g. the exception
```
(
    1
    \
    + 2)
```

But why was this check put there in the first place? Is it guarding
against something else?



## Test Plan

New test was added to indexer
2024-03-12 00:35:41 -04:00
Anuraag (Rag) Agrawal
b669306c87 Fix typo in docs snippt -> snippet (#10353) 2024-03-11 22:33:40 -04:00
Auguste Lalande
b117f33075 [pycodestyle] Implement blank-line-at-end-of-file (W391) (#10243)
## Summary

Implements the [blank line at end of
file](https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes)
rule (W391) from pycodestyle. Renamed to TooManyNewlinesAtEndOfFile for
clarity.

## Test Plan

New fixtures have been added

Part of #2402
2024-03-11 22:07:59 -04:00
Auguste Lalande
c746912b9e [pycodestyle] Implement redundant-backslash (E502) (#10292)
## Summary

Implements the
[redundant-backslash](https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes)
rule (E502) from pycodestyle.

## Test Plan

New fixture has been added

Part of #2402
2024-03-11 21:15:06 -04:00
Mathieu Kniewallner
fc7139d9a5 [flake8-bandit]: Implement S610 rule (#10316)
Part of https://github.com/astral-sh/ruff/issues/1646.

## Summary

Implement `S610` rule from `flake8-bandit`. 

Upstream references:
- Implementation:
https://github.com/PyCQA/bandit/blob/1.7.8/bandit/plugins/django_sql_injection.py#L20-L97
- Test cases:
https://github.com/PyCQA/bandit/blob/1.7.8/examples/django_sql_injection_extra.py
- Test assertion:
https://github.com/PyCQA/bandit/blob/1.7.8/tests/functional/test_functional.py#L517-L524

The implementation in `bandit` targets additional arguments (`params`,
`order_by` and `select_params`) but doesn't seem to do anything with
them in the end, so I did not include them in the implementation.

Note that this rule could be prone to false positives, as ideally we
would want to check if `extra()` is tied to a [Django
queryset](https://docs.djangoproject.com/en/5.0/ref/models/querysets/),
but AFAIK Ruff is not able to resolve classes outside of the current
module.

## Test Plan

Snapshot tests
2024-03-11 20:22:02 -04:00
Charlie Marsh
f8f56186b3 [pylint] Avoid false-positive slot non-assignment for __dict__ (PLE0237) (#10348)
Closes https://github.com/astral-sh/ruff/issues/10306.
2024-03-11 18:48:56 -04:00
Charlie Marsh
02fc521369 Wrap expressions in parentheses when negating (#10346)
## Summary

When negating an expression like `a or b`, we need to wrap it in
parentheses, e.g., `not (a or b)` instead of `not a or b`, due to
operator precedence.

Closes https://github.com/astral-sh/ruff/issues/10335.

## Test Plan

`cargo test`
2024-03-11 18:20:55 -04:00
Alex Waygood
4b0666919b F821, F822: fix false positive for .pyi files; add more test coverage for .pyi files (#10341)
This PR fixes the following false positive in a `.pyi` stub file:

```py
x: int
y = x  # F821 currently emitted here, but shouldn't be in a stub file
```

In a `.py` file, this is invalid regardless of whether `from __future__ import annotations` is enabled or not. In a `.pyi` stub file, however, it's always valid, as an annotation counts as a binding in a stub file even if no value is assigned to the variable.

I also added more test coverage for `.pyi` stub files in various edge cases where ruff's behaviour is currently correct, but where `.pyi` stub files do slightly different things to `.py` files.
2024-03-11 22:15:24 +00:00
Zanie Blue
06284c3700 Add release script (#10305)
Copied over from `uv`
2024-03-11 16:26:21 -05:00
Hoël Bagard
8d73866f70 [pycodestyle] Do not trigger E225 and E275 when the next token is a ')' (#10315)
## Summary

Fixes #10295.

`E225` (`Missing whitespace around operator`) and `E275` (`Missing
whitespace after keyword`) try to add a white space even when the next
character is a `)` (which is a syntax error in most cases, the
exceptions already being handled). This causes `E202` (`Whitespace
before close bracket`) to try to remove the added whitespace, resulting
in an infinite loop when `E225`/`E275` re-add it.
This PR adds an exception in `E225` and `E275` to not trigger in case
the next token is a `)`. It is a bit simplistic, but it solves the
example given in the issue without introducing a change in behavior
(according to the fixtures).

## Test Plan

`cargo test` and the `ruff-ecosystem` check were used to check that the
PR's changes do not have side-effects.
A new fixture was added to check that running the 3 rules on the example
given in the issue does not cause ruff to fail to converge.
2024-03-11 21:23:18 +00:00
Mathieu Kniewallner
bc693ea13a [flake8-bandit] Implement upstream updates for S311, S324 and S605 (#10313)
## Summary

Pick up updates made in latest
[releases](https://github.com/PyCQA/bandit/releases) of `bandit`:
- `S311`: https://github.com/PyCQA/bandit/pull/940 and
https://github.com/PyCQA/bandit/pull/1096
- `S324`: https://github.com/PyCQA/bandit/pull/1018
- `S605`: https://github.com/PyCQA/bandit/pull/1116

## Test Plan

Snapshot tests
2024-03-11 21:07:58 +00:00
dependabot[bot]
ad84eedc18 Bump chrono from 0.4.34 to 0.4.35 (#10333) 2024-03-11 10:57:30 -04:00
dependabot[bot]
96a4f95a44 Bump js-sys from 0.3.68 to 0.3.69 (#10331) 2024-03-11 10:50:23 -04:00
dependabot[bot]
bae26b49a6 Bump wasm-bindgen from 0.2.91 to 0.2.92 (#10329)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 09:29:08 +00:00
dependabot[bot]
3d7adbc0ed Bump unicode_names2 from 1.2.1 to 1.2.2 (#10330)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 09:28:25 +00:00
dependabot[bot]
c6456b882c Bump clap from 4.5.1 to 4.5.2 (#10332)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 09:26:42 +00:00
dependabot[bot]
49eb97879a Bump the actions group with 1 update (#10334)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 09:24:59 +00:00
Jane Lewis
0c84fbb6db ruff server - A new built-in LSP for Ruff, written in Rust (#10158)
<!--
Thank you for contributing to Ruff! 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?
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR introduces the `ruff_server` crate and a new `ruff server`
command. `ruff_server` is a re-implementation of
[`ruff-lsp`](https://github.com/astral-sh/ruff-lsp), written entirely in
Rust. It brings significant performance improvements, much tighter
integration with Ruff, a foundation for supporting entirely new language
server features, and more!

This PR is an early version of `ruff_lsp` that we're calling the
**pre-release** version. Anyone is more than welcome to use it and
submit bug reports for any issues they encounter - we'll have some
documentation on how to set it up with a few common editors, and we'll
also provide a pre-release VSCode extension for those interested.

This pre-release version supports:
- **Diagnostics for `.py` files**
- **Quick fixes**
- **Full-file formatting**
- **Range formatting**
- **Multiple workspace folders**
- **Automatic linter/formatter configuration** - taken from any
`pyproject.toml` files in the workspace.

Many thanks to @MichaReiser for his [proof-of-concept
work](https://github.com/astral-sh/ruff/pull/7262), which was important
groundwork for making this PR possible.

## Architectural Decisions

I've made an executive choice to go with `lsp-server` as a base
framework for the LSP, in favor of `tower-lsp`. There were several
reasons for this:

1. I would like to avoid `async` in our implementation. LSPs are mostly
computationally bound rather than I/O bound, and `async` adds a lot of
complexity to the API, while also making harder to reason about
execution order. This leads into the second reason, which is...
2. Any handlers that mutate state should be blocking and run in the
event loop, and the state should be lock-free. This is the approach that
`rust-analyzer` uses (also with the `lsp-server`/`lsp-types` crates as a
framework), and it gives us assurances about data mutation and execution
order. `tower-lsp` doesn't support this, which has caused some
[issues](https://github.com/ebkalderon/tower-lsp/issues/284) around data
races and out-of-order handler execution.
3. In general, I think it makes sense to have tight control over
scheduling and the specifics of our implementation, in exchange for a
slightly higher up-front cost of writing it ourselves. We'll be able to
fine-tune it to our needs and support future LSP features without
depending on an upstream maintainer.

## Test Plan

The pre-release of `ruff_server` will have snapshot tests for common
document editing scenarios. An expanded test suite is on the roadmap for
future version of `ruff_server`.
2024-03-08 20:57:23 -08:00
Charlie Marsh
a892fc755d Bump version to v0.3.2 (#10304) 2024-03-09 00:24:22 +00:00
Gautier Moin
a067d87ccc Fix incorrect Parameter range for *args and **kwargs (#10283)
## Summary

Fix #10282 

This PR updates the Python grammar to include the `*` character in
`*args` `**kwargs` in the range of the `Parameter`
```
def f(*args, **kwargs): pass
#      ~~~~    ~~~~~~    <-- range before the PR
#     ^^^^^  ^^^^^^^^    <-- range after
```

The invalid syntax `def f(*, **kwargs): ...` is also now correctly
reported.

## Test Plan

Test cases were added to `function.rs`.
2024-03-08 18:57:49 -05:00
Micha Reiser
b64f2ea401 Formatter: Improve single-with item formatting for Python 3.8 or older (#10276)
## Summary

This PR changes how we format `with` statements with a single with item
for Python 3.8 or older. This change is not compatible with Black.

This is how we format a single-item with statement today 

```python
def run(data_path, model_uri):
    with pyspark.sql.SparkSession.builder.config(
        key="spark.python.worker.reuse", value=True
    ).config(key="spark.ui.enabled", value=False).master(
        "local-cluster[2, 1, 1024]"
    ).getOrCreate():
        # ignore spark log output
        spark.sparkContext.setLogLevel("OFF")
        print(score_model(spark, data_path, model_uri))
```

This is different than how we would format the same expression if it is
inside any other clause header (`while`, `if`, ...):

```python
def run(data_path, model_uri):
    while (
        pyspark.sql.SparkSession.builder.config(
            key="spark.python.worker.reuse", value=True
        )
        .config(key="spark.ui.enabled", value=False)
        .master("local-cluster[2, 1, 1024]")
        .getOrCreate()
    ):
        # ignore spark log output
        spark.sparkContext.setLogLevel("OFF")
        print(score_model(spark, data_path, model_uri))

```

Which seems inconsistent to me. 

This PR changes the formatting of the single-item with Python 3.8 or
older to match that of other clause headers.

```python
def run(data_path, model_uri):
    with (
        pyspark.sql.SparkSession.builder.config(
            key="spark.python.worker.reuse", value=True
        )
        .config(key="spark.ui.enabled", value=False)
        .master("local-cluster[2, 1, 1024]")
        .getOrCreate()
    ):
        # ignore spark log output
        spark.sparkContext.setLogLevel("OFF")
        print(score_model(spark, data_path, model_uri))
```

According to our versioning policy, this style change is gated behind a
preview flag.

## Test Plan

See added tests.

Added
2024-03-08 23:56:02 +00:00
Micha Reiser
4bce801065 Fix unstable with-items formatting (#10274)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/10267

The issue with the current formatting is that the formatter flips
between the `SingleParenthesizedContextManager` and
`ParenthesizeIfExpands` or `SingleWithTarget` because the layouts use
incompatible formatting ( `SingleParenthesizedContextManager`:
`maybe_parenthesize_expression(context)` vs `ParenthesizeIfExpands`:
`parenthesize_if_expands(item)`, `SingleWithTarget`:
`optional_parentheses(item)`.

The fix is to ensure that the layouts between which the formatter flips
when adding or removing parentheses are the same. I do this by
introducing a new `SingleWithoutTarget` layout that uses the same
formatting as `SingleParenthesizedContextManager` if it has no target
and prefer `SingleWithoutTarget` over using `ParenthesizeIfExpands` or
`SingleWithTarget`.

## Formatting change

The downside is that we now use `maybe_parenthesize_expression` over
`parenthesize_if_expands` for expressions where
`can_omit_optional_parentheses` returns `false`. This can lead to stable
formatting changes. I only found one formatting change in our ecosystem
check and, unfortunately, this is necessary to fix the instability (and
instability fixes are okay to have as part of minor changes according to
our versioning policy)

The benefit of the change is that `with` items with a single context
manager and without a target are now formatted identically to how the
same expression would be formatted in other clause headers.

## Test Plan

I ran the ecosystem check locally
2024-03-08 23:48:47 +00:00
Micha Reiser
a56d42f183 Refactor with statement formatting to have explicit layouts (#10296)
## Summary

This PR refactors the with item formatting to use more explicit layouts
to make it easier to understand the different formatting cases.

The benefit of the explicit layout is that it makes it easier to reasons
about layout transition between format runs. For example, today it's
possible that `SingleWithTarget` or `ParenthesizeIfExpands` add
parentheses around the with items for `with aaaaaaaaaa + bbbbbbbbbbbb:
pass`, resulting in `with (aaaaaaaaaa + bbbbbbbbbbbb): pass`. The
problem with this is that the next formatting pass uses the
`SingleParenthesizedContextExpression` layout that uses
`maybe_parenthesize_expression` which is different from
`parenthesize_if_expands(&expr)` or `optional_parentheses(&expr)`.

## Test Plan

`cargo test`

I ran the ecosystem checks locally and there are no changes.
2024-03-08 18:40:39 -05:00
Alex Waygood
1d97f27335 Start tracking quoting style in the AST (#10298)
This PR modifies our AST so that nodes for string literals, bytes literals and f-strings all retain the following information:
- The quoting style used (double or single quotes)
- Whether the string is triple-quoted or not
- Whether the string is raw or not

This PR is a followup to #10256. Like with that PR, this PR does not, in itself, fix any bugs. However, it means that we will have the necessary information to preserve quoting style and rawness of strings in the `ExprGenerator` in a followup PR, which will allow us to provide a fix for https://github.com/astral-sh/ruff/issues/7799.

The information is recorded on the AST nodes using a bitflag field on each node, similarly to how we recorded the information on `Tok::String`, `Tok::FStringStart` and `Tok::FStringMiddle` tokens in #10298. Rather than reusing the bitflag I used for the tokens, however, I decided to create a custom bitflag for each AST node.

Using different bitflags for each node allows us to make invalid states unrepresentable: it is valid to set a `u` prefix on a string literal, but not on a bytes literal or an f-string. It also allows us to have better debug representations for each AST node modified in this PR.
2024-03-08 19:11:47 +00:00
Micha Reiser
965adbed4b Fix trailing kwargs end of line comment after slash (#10297)
## Summary

Fixes the handling end of line comments that belong to `**kwargs` when
the `**kwargs` come after a slash.

The issue was that we missed to include the `**kwargs` start position
when determining the start of the next node coming after the `/`.

Fixes https://github.com/astral-sh/ruff/issues/10281

## Test Plan

Added test
2024-03-08 14:45:26 +00:00
Alex Waygood
c504d7ab11 Track quoting style in the tokenizer (#10256) 2024-03-08 08:40:06 +00:00
Tom Kuson
72c9f7e4c9 Include actual conditions in E712 diagnostics (#10254)
## Summary

Changes the generic recommendation to replace

```python
if foo == True: ...
```

with `if cond:` to `if foo:`.

Still uses a generic message for compound comparisons as a specific
message starts to become confusing. For example,

```python
if foo == True != False: ...
```

produces two recommendations, one of which would recommend `if True:`,
which is confusing.

Resolves [recommendation in a previous
PR](https://github.com/astral-sh/ruff/pull/8613/files#r1514915070).

## Test Plan

`cargo nextest run`
2024-03-08 01:20:56 +00:00
Charlie Marsh
57be3fce90 Treat typing.Annotated subscripts as type definitions (#10285)
## Summary

I think this code has existed since the start of `typing.Annotated`
support (https://github.com/astral-sh/ruff/pull/333), and was then
overlooked over a series of refactors.

Closes https://github.com/astral-sh/ruff/issues/10279.
2024-03-07 19:51:54 -05:00
Charlie Marsh
7a675cd822 Remove Maturin pin (#10284)
## Summary

As of
https://github.com/pypa/gh-action-pypi-publish/releases/tag/v1.8.13, all
relevant dependencies have been updated to support Metadata 2.2, so we
can remove our Maturin pin.
2024-03-07 19:42:22 -05:00
Samuel Cormier-Iijima
7b4a73d421 Fix E203 false positive for slices in format strings (#10280)
## Summary

The code later in this file that checks for slices relies on the stack
of brackets to determine the position. I'm not sure why format strings
were being excluded from this, but the tests still pass with these match
guards removed.

Closes #10278

## Test Plan

~Still needs a test.~ Test case added for this example.
2024-03-07 17:09:05 -05:00
Charlie Marsh
91af5a4b74 [pyupgrade] Allow fixes for f-string rule regardless of line length (UP032) (#10263)
## Summary

This is a follow-up to https://github.com/astral-sh/ruff/pull/10238 to
offer fixes for the f-string rule regardless of the line length of the
resulting fix. To quote Alex in the linked PR:

> Yes, from the user's perspective I'd rather have a fix that may lead
to line length issues than have to fix them myself :-) Cleaning up line
lengths is easier than changing from `"".format()` to `f""`

I agree with this position, which is that if we're going to offer a
diagnostic, we should really be offering the user the ability to fix it
-- otherwise, we're just inconveniencing them.
2024-03-07 08:59:29 -05:00
Charlie Marsh
461cdad53a Avoid repeating function calls in f-string conversions (#10265)
## Summary

Given a format string like `"{x} {x}".format(x=foo())`, we should avoid
converting to an f-string, since doing so would require repeating the
function call (`f"{foo()} {foo()}"`), which could introduce side
effects.

Closes https://github.com/astral-sh/ruff/issues/10258.
2024-03-06 23:33:19 -05:00
Charlie Marsh
b9264a5a11 Set maturin version in release.yaml (#10257)
See: https://github.com/astral-sh/uv/pull/2219
2024-03-06 21:50:40 +00:00
Charlie Marsh
ea79f616bc Bump version to v0.3.1 (#10252) 2024-03-06 19:59:04 +00:00
Tom Kuson
f999b1b617 Tweak E712 docs (#8613)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-03-06 17:54:29 +00:00
Micha Reiser
fe6afbe406 Fix ruff-action documentation to consistently use args instead of options (#10249) 2024-03-06 18:09:45 +01:00
Eero Vaher
cbd927f346 Make rule PT012 example clearer (#10248) 2024-03-06 15:47:14 +00:00
Micha Reiser
6159a8e532 [pyupgrade] Generate diagnostic for all valid f-string conversions regardless of line-length (UP032) (#10238)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/10235

This PR changes `UP032` to flag all `"".format` calls that can
technically be rewritten to an f-string, even if rewritting it to an
fstring, at least automatically, exceeds the line length (or increases
the amount by which it goes over the line length).

I looked at the Git history to understand whether the check prevents
some false positives (reported by an issue), but i haven't found a
compelling reason to limit the rule to only flag format calls that stay
in the line length limit:

* https://github.com/astral-sh/ruff/pull/7818 Changed the heuristic to
determine if the fix fits to address
https://github.com/astral-sh/ruff/discussions/7810
* https://github.com/astral-sh/ruff/pull/1905 first version of the rule 


I did take a look at pyupgrade and couldn't find a similar check, at
least not in the rule code (maybe it's checked somewhere else?)
https://github.com/asottile/pyupgrade/blob/main/pyupgrade/_plugins/fstrings.py


## Breaking Change?

This could be seen as a breaking change according to ruff's [versioning
policy](https://docs.astral.sh/ruff/versioning/):

> The behavior of a stable rule is changed
  
  * The scope of a stable rule is significantly increased
  * The intent of the rule changes
* Does not include bug fixes that follow the original intent of the rule

It does increase the scope of the rule, but it is in the original intent
of the rule (so it's not).

## Test Plan

See changed test output
2024-03-06 09:58:20 +01:00
Micha Reiser
8ea5b08700 refactor: Use QualifiedName for Imported::call_path (#10214)
## Summary

When you try to remove an internal representation leaking into another
type and end up rewriting a simple version of `smallvec`.

The goal of this PR is to replace the `Box<[&'a str]>` with
`Box<QualifiedName>` to avoid that the internal `QualifiedName`
representation leaks (and it gives us a nicer API too). However, doing
this when `QualifiedName` uses `SmallVec` internally gives us all sort
of funny lifetime errors. I was lost but @BurntSushi came to rescue me.
He figured out that `smallvec` has a variance problem which is already
tracked in https://github.com/servo/rust-smallvec/issues/146

To fix the variants problem, I could use the smallvec-2-alpha-4 or
implement our own smallvec. I went with implementing our own small vec
for this specific problem. It obviously isn't as sophisticated as
smallvec (only uses safe code), e.g. it doesn't perform any size
optimizations, but it does its job.

Other changes:

* Removed `Imported::qualified_name` (the version that returns a
`String`). This can be replaced by calling `ToString` on the qualified
name.
* Renamed `Imported::call_path` to `qualified_name` and changed its
return type to `&QualifiedName`.
* Renamed `QualifiedName::imported` to `user_defined` which is the more
common term when talking about builtins vs the rest/user defined
functions.


## Test plan

`cargo test`
2024-03-06 09:55:59 +01:00
Auguste Lalande
4c05c258de Add encoding when opening files in generate_mkdocs.py (#10244)
## Summary

Open files with utf8 encoding when writing files in generate_mkdocs.py.

The following can happen otherwise.
```
../ruff> python scripts/generate_mkdocs.py
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target\debug\ruff_dev.exe generate-docs`
Traceback (most recent call last):
  File "C:\..\ruff\scripts\generate_mkdocs.py", line 185, in <module>
    main()
  File "C:\..\scripts\generate_mkdocs.py", line 141, in main
    f.write(clean_file_content(file_content, title))
  File "C:\..\AppData\Local\Programs\Python\Python310\lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode characters in position 1396-1397: character maps to <undefined>
```

I could not determine which character was causing the issue, but opening
with utf8 encoding fixed it.

## Test Plan

Condering the change is small, I simply ran the file and confirmed it
worked, but opened to suggestion on more robust testing.
2024-03-06 05:57:27 +00:00
Micha Reiser
af6ea2f5e4 [pycodestyle]: Make blank lines in typing stub files optional (E3*) (#10098)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/10039

The [recommendation for typing stub
files](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
is to use **one** blank line to group related definitions and
otherwise omit blank lines. 

The newly added blank line rules (`E3*`) didn't account for typing stub
files and enforced two empty lines at the top level and one empty line
otherwise, making it impossible to group related definitions.

This PR implements the `E3*` rules to:

* Not enforce blank lines. The use of blank lines in typing definitions
is entirely up to the user.
* Allow at most one empty line, including between top level statements. 

## Test Plan

Added unit tests (It may look odd that many snapshots are empty but the
point is that the rule should no longer emit diagnostics)
2024-03-05 12:48:50 +01:00
Micha Reiser
46ab9dec18 [pycodestyle] Respect isort settings in blank line rules (E3*) (#10096)
## Summary

This PR changes the `E3*` rules to respect the `isort`
`lines-after-imports` and `lines-between-types` settings. Specifically,
the following rules required changing

* `TooManyBlannkLines` : Respects both settings.
* `BlankLinesTopLevel`: Respects `lines-after-imports`. Doesn't need to
respect `lines-between-types` because it only applies to classes and
functions


The downside of this approach is that `isort` and the blank line rules
emit a diagnostic when there are too many blank lines. The fixes aren't
identical, the blank line is less opinionated, but blank lines accepts
the fix of `isort`.

<details>
	<summary>Outdated approach</summary>
Fixes
https://github.com/astral-sh/ruff/issues/10077#issuecomment-1961266981

This PR changes the blank line rules to not enforce the number of blank
lines after imports (top-level) if isort is enabled and leave it to
isort to enforce the right number of lines (depends on the
`isort.lines-after-imports` and `isort.lines-between-types` settings).

The reason to give `isort` precedence over the blank line rules is that
they are configurable. Users that always want to blank lines after
imports can use `isort.lines-after-imports=2` to enforce that
(specifically for imports).

This PR does not fix the incompatibility with the formatter in pyi files
that only uses 0 to 1 blank lines. I'll address this separately.

</details>

## Review
The first commit is a small refactor that simplified implementing the
fix (and makes it easier to reason about what's mutable and what's not).


## Test Plan

I added a new test and verified that it fails with an error that the fix
never converges. I verified the snapshot output after implementing the
fix.

---------

Co-authored-by: Hoël Bagard <34478245+hoel-bagard@users.noreply.github.com>
2024-03-05 10:09:15 +00:00
Hoël Bagard
d441338358 Improve documentation of the preview mode (#10168)
## Summary

This PR was prompted by the discussion in #10153.
It adds CLI tab examples next to the `pyproject.toml` and `ruff.toml`
examples. It should be helpful for users wanting to try out the preview
mode without modifying or creating a `.toml` file.
It also adds a paragraph to try to make the effect of the preview mode
less confusing.
2024-03-05 03:08:30 +00:00
ooo oo
72599dafb6 docs: fix a rustdoc typo in C409 rule (#10233) 2024-03-05 02:33:08 +00:00
Charlie Marsh
7eaec300dd Move shell expansion into --config lookup (#10219)
## Summary

When users provide configurations via `--config`, we use `shellexpand`
to ensure that we expand signifiers like `~` and environment variables.

In https://github.com/astral-sh/ruff/pull/9599, we modified `--config`
to accept either a path or an arbitrary setting. However, the detection
(to determine whether the value is a path or a setting) was lacking the
`shellexpand` behavior -- it was downstream. So we were always treating
paths like `~/ruff.toml` as values, not paths.

Closes https://github.com/astral-sh/ruff-vscode/issues/413.
2024-03-04 12:45:50 -05:00
Micha Reiser
184241f99a Remove Expr postfix from ExprNamed, ExprIf, and ExprGenerator (#10229)
The expression types in our AST are called `ExprYield`, `ExprAwait`,
`ExprStringLiteral` etc, except `ExprNamedExpr`, `ExprIfExpr` and
`ExprGenratorExpr`. This seems to align with [Python AST's
naming](https://docs.python.org/3/library/ast.html) but feels
inconsistent and excessive.

This PR removes the `Expr` postfix from `ExprNamedExpr`, `ExprIfExpr`,
and `ExprGeneratorExpr`.
2024-03-04 12:55:01 +01:00
Alex Waygood
8b749e1d4d Make --config and --isolated global flags (#10150) 2024-03-04 11:19:40 +00:00
Steve C
8dde81a905 [pylint] - add fix for unary expressions in PLC2801 (#9587)
## Summary

Closes #9572

Don't go easy on this review!

## Test Plan

`cargo test`
2024-03-04 11:25:17 +01:00
Dominik Spicher
00300c0d9d cache: tweak generated .gitignore (#10226)
- Add a notice that this file was generated by ruff

 - Add a trailing newline
2024-03-04 10:49:51 +01:00
Micha Reiser
a6d892b1f4 Split CallPath into QualifiedName and UnqualifiedName (#10210)
## Summary

Charlie can probably explain this better than I but it turns out,
`CallPath` is used for two different things:

* To represent unqualified names like `version` where `version` can be a
local variable or imported (e.g. `from sys import version` where the
full qualified name is `sys.version`)
* To represent resolved, full qualified names

This PR splits `CallPath` into two types to make this destinction clear.

> Note: I haven't renamed all `call_path` variables to `qualified_name`
or `unqualified_name`. I can do that if that's welcomed but I first want
to get feedback on the approach and naming overall.

## Test Plan

`cargo test`
2024-03-04 09:06:51 +00:00
dependabot[bot]
ba4328226d Bump the actions group with 1 update (#10224)
Bumps the actions group with 1 update:
[extractions/setup-just](https://github.com/extractions/setup-just).

Updates `extractions/setup-just` from 1 to 2
<details>
<summary>Commits</summary>
<ul>
<li><a
href="dd310ad5a9"><code>dd310ad</code></a>
This is 2.0.0</li>
<li><a
href="b88c09d1cb"><code>b88c09d</code></a>
Upgrade GitHub Actions</li>
<li><a
href="dcec242065"><code>dcec242</code></a>
Upgrade dependencies</li>
<li><a
href="fbd91a81bd"><code>fbd91a8</code></a>
Use Node v20</li>
<li><a
href="502448742b"><code>5024487</code></a>
Build: just v1.23.0 (<a
href="https://redirect.github.com/extractions/setup-just/issues/15">#15</a>)</li>
<li><a
href="1b96160c16"><code>1b96160</code></a>
doc: Fix invalid GHA syntax in github-token example (<a
href="https://redirect.github.com/extractions/setup-just/issues/12">#12</a>)</li>
<li>See full diff in <a
href="https://github.com/extractions/setup-just/compare/v1...v2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=extractions/setup-just&package-manager=github_actions&previous-version=1&new-version=2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 10:01:07 +01:00
Micha Reiser
64f66cd8fe Refine SemanticModel lifetime bounds (#10221)
## Summary

Corrects/refines some semantic model and related lifetime bounds.

## Test Plan

`cargo check`
2024-03-04 09:21:13 +01:00
Gautier Moin
4eac9baf43 [pep8_naming] Add fixes N804 and N805 (#10215)
## Summary

This PR fixes for `invalid-first-argument` rules.
The fixes rename the first argument of methods and class methods to the
valid one. References to this argument are also renamed.
Fixes are skipped when another argument is named as the valid first
argument.
The fix is marked as unsafe due

The functions for the `N804` and `N805` rules are now merged, as they
only differ by the name of the valid first argument.
The rules were moved from the AST iteration to the deferred scopes to be
in the function scope while creating the fix.

## Test Plan

`cargo test`
2024-03-04 02:22:54 +00:00
Hoël Bagard
c27e048ff2 docs: update docker release version (#10218)
## Summary

This PR updates the docker release version from `0.1.3` to `0.3.0`
(latest), since `0.1.3` does not seem to exist anymore.
2024-03-03 21:17:54 -05:00
Charlie Marsh
737fcfd79e Remove trailing space from CapWords message (#10220) 2024-03-04 01:54:53 +00:00
Charlie Marsh
84bf333031 Accept a PEP 440 version specifier for required-version (#10216)
## Summary

Allows `required-version` to be set with a version specifier, like
`>=0.3.1`.

If a single version is provided, falls back to assuming `==0.3.1`, for
backwards compatibility.

Closes https://github.com/astral-sh/ruff/issues/10192.
2024-03-03 18:43:49 -05:00
Micha Reiser
db25a563f7 Remove unneeded lifetime bounds (#10213)
## Summary

This PR removes the unneeded lifetime `'b` from many of our `Visitor`
implementations.

The lifetime is unneeded because it is only constraint by `'a`, so we
can use `'a` directly.

## Test Plan

`cargo build`
2024-03-03 18:12:11 +00:00
Micha Reiser
e725b6fdaf CallPath newtype wrapper (#10201)
## Summary

This PR changes the `CallPath` type alias to a newtype wrapper. 

A newtype wrapper allows us to limit the API and to experiment with
alternative ways to implement matching on `CallPath`s.



## Test Plan

`cargo test`
2024-03-03 16:54:24 +01:00
Zanie Blue
fb05d218c3 Skip another invalid notebook in the OpenAI repository (#10209)
We should consider another source for notebook ecosystem checks, these
constantly have syntax errors
2024-03-03 09:41:31 -06:00
Charlie Marsh
ba7f6783e9 Avoid false-positives for parens-on-raise with futures.exception() (#10206)
## Summary

As a heuristic, we now ignore function calls that "look like" method
calls (e.g., `future.exception()`).

Closes https://github.com/astral-sh/ruff/issues/10205.
2024-03-03 00:28:51 +00:00
Charlie Marsh
7515196245 Respect external codes in file-level exemptions (#10203)
We shouldn't warn when an "external" code is used in a file-level
exemption.

Closes https://github.com/astral-sh/ruff/issues/10202.
2024-03-03 00:20:36 +00:00
Charlie Marsh
c7431828a7 Run cargo update (#10204) 2024-03-03 00:15:29 +00:00
Omer Korner
39a3031898 Update README.md, add Kraken Tech (#10197) 2024-03-02 19:02:59 -05:00
Jeremy Hiatt
c007b175ba Check for use of debugpy and ptvsd debug modules (#10177) (#10194)
## Summary

This addresses https://github.com/astral-sh/ruff/issues/10177.

## Test Plan

I added additional lines to the existing test file for T100.
2024-03-01 23:02:44 -05:00
trag1c
0cd3b07efa Removed unused variable in TRY300's example (#10190)
<!--
Thank you for contributing to Ruff! 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?
- Does this pull request include references to any relevant issues?
-->

## Summary

Removes the unnecessary `exc` variable in `TRY300`'s docs example.

## Test Plan
```
python scripts/generate_mkdocs.py; mkdocs serve -f mkdocs.public.yml
```
2024-03-01 18:50:52 -05:00
Meheret
c59d82a22e CLI: Color entire line in Diffs (#10183) 2024-03-01 13:53:45 +01:00
Greenstar
8b5daaec7d Fix broken documentation links affected by namespace changes in lint rules (#10182) 2024-03-01 12:35:29 +01:00
Micha Reiser
0373b51823 Remove indico ecosystem override (#10180) 2024-03-01 10:38:13 +01:00
Hoël Bagard
b82e87790e Fix E301 not triggering on decorated methods. (#10117)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-03-01 09:30:53 +00:00
Meheret
56d445add9 Colorize the output of ruff format --diff (#10110)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-03-01 08:55:30 +00:00
Jane Lewis
8ecdf5369a Fix RUF028 not allowing # fmt: skip on match cases (#10178)
## Summary

Fixes #10174 by allowing match cases to be enclosing nodes for
suppression comments. `else/elif` clauses are now also allowed to be
enclosing nodes.

## Test Plan
I've added the offending code from the original issue to the `RUF028`
snapshot test, and I've also expanded it to test the allowed `else/elif`
clause.
2024-03-01 00:36:23 -08:00
Michael Merickel
c9931a548f Implement isort's default-section setting (#10149)
## Summary

This fixes https://github.com/astral-sh/ruff/issues/7868.

Support isort's `default-section` feature which allows any imports that
match sections that are not in `section-order` to be mapped to a
specifically named section.


https://pycqa.github.io/isort/docs/configuration/options.html#default-section

This has a few implications:

- It is no longer required that all known sections are defined in
`section-order`.
- This is technically a bw-incompat change because currently if folks
define custom groups, and do not define a `section-order`, the code used
to add all known sections to `section-order` while emitting warnings.
**However, when this happened, users would be seeing warnings so I do
not think it should count as a bw-incompat change.**

## Test Plan

- Added a new test.
- Did not break any existing tests.

Finally, I ran the following config against Pyramid's complex codebase
that was previously using isort and this change worked there.

### pyramid's previous isort config


5f7e286b06/pyproject.toml (L22-L37)

```toml
[tool.isort]
profile = "black"
multi_line_output = 3
src_paths = ["src", "tests"]
skip_glob = ["docs/*"]
include_trailing_comma = true
force_grid_wrap = false
combine_as_imports = true
line_length = 79
force_sort_within_sections = true
no_lines_before = "THIRDPARTY"
sections = "FUTURE,THIRDPARTY,FIRSTPARTY,LOCALFOLDER"
default_section = "THIRDPARTY"
known_first_party = "pyramid"
```

### tested with ruff isort config

```toml
[tool.ruff.lint.isort]
case-sensitive = true
combine-as-imports = true
force-sort-within-sections = true
section-order = [
    "future",
    "third-party",
    "first-party",
    "local-folder",
]
default-section = "third-party"
known-first-party = [
    "pyramid",
]
```
2024-03-01 03:32:03 +00:00
Mikko Leppänen
8e0a70cfa3 [pylint] Implement useless-exception-statement (W0133) (#10176)
## Summary

This review contains a new rule for handling `useless exception
statements` (`PLW0133`). Is it based on the following pylint's rule:
[W0133](https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/pointless-exception-statement.html)


Note: this rule does not cover the case if an error is a custom
exception class.

See: [Rule request](https://github.com/astral-sh/ruff/issues/10145)

## Test Plan

```bash
cargo test & manually
```
2024-02-29 21:37:16 -05:00
Shaygan Hooshyari
cbafae022d [pylint] Implement singledispatch-method (E1519) (#10140)
Implementing the rule 

https://pylint.readthedocs.io/en/stable/user_guide/messages/error/singledispatch-method.html#singledispatch-method-e1519

Implementation simply checks the function type and name of the
decorators.
2024-03-01 02:22:30 +00:00
Micha Reiser
cea59b4425 Fix the sorting of the schema submited to schemastore (#10173) 2024-02-29 18:21:54 +00:00
Charlie Marsh
40186a26ef Use a Discord icon rather than a text link (#9961) 2024-02-29 11:20:11 -05:00
Micha Reiser
b53118ed00 Bump version to v0.3.0 (#10151)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-02-29 16:05:20 +01:00
Micha Reiser
52f4c1e41b Remove deprecated CLI option --format (#10170)
Co-authored-by: Tibor Reiss <tibor.reiss@gmail.com>
2024-02-29 13:59:08 +00:00
Micha Reiser
eceffe74a0 Deprecate ruff <path> ruff --explain, ruff --clean and ruff --generate-shell-completion (#10169) 2024-02-29 14:50:01 +01:00
Justin Sexton
c73c497477 [pydocstyle] Trim whitespace when removing blank lines after section (D413) (#10162)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-02-29 13:29:40 +00:00
Hoël Bagard
c9c98c4fe3 Fix mkdocs local link (#10167) 2024-02-29 11:35:10 +01:00
Micha Reiser
72ccb34ba6 Fix ecosystem check for indico (#10164) 2024-02-29 10:27:33 +01:00
Micha Reiser
dcc92f50cf Update black tests (#10166) 2024-02-29 10:00:51 +01:00
Micha Reiser
a6f32ddc5e Ruff 2024.2 style (#9639) 2024-02-29 09:30:54 +01:00
Jane Lewis
0293908b71 Implement RUF028 to detect useless formatter suppression comments (#9899)
<!--
Thank you for contributing to Ruff! 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?
- Does this pull request include references to any relevant issues?
-->
Fixes #6611

## Summary

This lint rule spots comments that are _intended_ to suppress or enable
the formatter, but will be ignored by the Ruff formatter.

We borrow some functions the formatter uses for determining comment
placement / putting them in context within an AST.

The analysis function uses an AST visitor to visit each comment and
attach it to the AST. It then uses that context to check:
1. Is this comment in an expression?
2. Does this comment have bad placement? (e.g. a `# fmt: skip` above a
function instead of at the end of a line)
3. Is this comment redundant?
4. Does this comment actually suppress any code?
5. Does this comment have ambiguous placement? (e.g. a `# fmt: off`
above an `else:` block)

If any of these are true, a violation is thrown. The reported reason
depends on the order of the above check-list: in other words, a `# fmt:
skip` comment on its own line within a list expression will be reported
as being in an expression, since that reason takes priority.

The lint suggests removing the comment as an unsafe fix, regardless of
the reason.

## Test Plan

A snapshot test has been created.
2024-02-28 19:21:06 +00:00
Philipp Thiel
36bc725eaa [flake8-bugbear] Avoid adding default initializers to stubs (B006) (#10152)
## Summary

Adapts the fix for rule B006 to no longer modify the body of function
stubs, while retaining the change in method signature.

## Test Plan

The existing tests for B006 were adapted to reflect this change in
behavior.

## Relevant issue

https://github.com/astral-sh/ruff/issues/10083
2024-02-28 18:19:36 +00:00
Alex Waygood
8f92da8b6c Fix the ecosystem check (#10155) 2024-02-28 17:42:06 +00:00
Charlie Marsh
a1905172a8 [flake8-bandit] Remove suspicious-lxml-import (S410) (#10154)
## Summary

The `lxml` library has been modified to address known vulnerabilities
and unsafe defaults. As such, the `defusedxml`
library is no longer necessary, `defusedxml` has deprecated its `lxml`
module.

Closes https://github.com/astral-sh/ruff/issues/10030.
2024-02-28 12:38:55 -05:00
Micha Reiser
1791e7d73b Limit isort.lines-after-imports to 1 for stub files (#9971) 2024-02-28 17:36:51 +01:00
Charlie Marsh
317d2e4c75 Remove build from the default exclusion list (#10093)
## Summary

This is a not-unpopular directory name, and it's led to tons of issues
and user confusion (most recently:
https://github.com/astral-sh/ruff-pre-commit/issues/69). I've wanted to
remove it for a long time, but we need to do so as part of a minor
release.
2024-02-28 16:30:38 +00:00
Micha Reiser
8044c24c7e Remove "Beta" Label from formatter documentation (#10144) 2024-02-28 12:47:37 +00:00
Robin Caloudis
a1e8784207 [ruff] Expand rule for list(iterable).pop(0) idiom (RUF015) (#10148)
## Summary

Currently, rule `RUF015` is not able to detect the usage of
`list(iterable).pop(0)` falling under the category of an _unnecessary
iterable allocation for accessing the first element_. This PR wants to
change that. See the underlying issue for more details.

* Provide extension to detect `list(iterable).pop(0)`, but not
`list(iterable).pop(i)` where i > 1
* Update corresponding doc

## Test Plan

* `RUF015.py` and the corresponding snap file were extended such that
their correspond to the new behaviour

Closes https://github.com/astral-sh/ruff/issues/9190

--- 

PS: I've only been working on this ticket as I haven't seen any activity
from issue assignee @rmad17, neither in this repo nor in a fork. I hope
I interpreted his inactivity correctly. Didn't mean to steal his chance.
Since I stumbled across the underlying problem myself, I wanted to offer
a solution as soon as possible.
2024-02-28 00:24:28 +00:00
Micha Reiser
8dc22d5793 Perf: Skip string normalization when possible (#10116) 2024-02-26 17:35:29 +00:00
Micha Reiser
15b87ea8be E203: Don't warn about single whitespace before tuple , (#10094) 2024-02-26 18:22:35 +01:00
Alex Waygood
c25f1cd12a Explicitly ban overriding extend as part of a --config flag (#10135) 2024-02-26 16:07:28 +00:00
Charlie Marsh
f5904a20d5 Add package name to requirements-insiders.txt (#10090) 2024-02-26 10:52:58 -05:00
Micha Reiser
77c5561646 Add parenthesized flag to ExprTuple and ExprGenerator (#9614) 2024-02-26 15:35:20 +00:00
Arkin Modi
ab4bd71755 docs: fix pycodestyle.max-line-length link (#10136) 2024-02-26 14:58:13 +01:00
Micha Reiser
5abf662365 Upgrade codspeed-criterion for perf boost (#10134) 2024-02-26 11:37:23 +00:00
Alex Waygood
14fa1c5b52 [Minor] Improve the style of some tests in crates/ruff/tests/format.rs (#10132) 2024-02-26 11:02:09 +00:00
Alex Waygood
0421c41ff7 Add Alex Waygood as a CODEOWNER for flake8-pyi (#10129) 2024-02-26 10:25:46 +00:00
dependabot[bot]
ad4695d3eb Bump serde from 1.0.196 to 1.0.197 (#10128)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:26:00 +00:00
dependabot[bot]
8c58ebee37 Bump insta from 1.34.0 to 1.35.1 (#10127)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:15:13 +00:00
dependabot[bot]
5023874355 Bump bstr from 1.9.0 to 1.9.1 (#10124)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 10:14:22 +01:00
dependabot[bot]
5554510597 Bump syn from 2.0.49 to 2.0.51 (#10126)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:14:00 +00:00
dependabot[bot]
1341e064a7 Bump serde-wasm-bindgen from 0.6.3 to 0.6.4 (#10125)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:13:15 +00:00
Micha Reiser
bd98d6884b Upgrade upload/download artifact actions in release workflow (#10105) 2024-02-26 08:23:10 +01:00
Micha Reiser
761d4d42f1 Add cold attribute to less likely printer queue branches (#10121) 2024-02-26 08:19:40 +01:00
Robin Caloudis
fc8738f52a [ruff] Avoid f-string false positives in gettext calls (RUF027) (#10118)
## Summary

It is a convention to use the `_()` alias for `gettext()`. We want to
avoid
statement expressions and assignments related to aliases of the gettext
API.
See https://docs.python.org/3/library/gettext.html for details. When one
uses `_() to mark a string for translation, the tools look for these
markers
and replace the original string with its translated counterpart. If the
string contains variable placeholders or formatting, it can complicate
the
translation process, lead to errors or incorrect translations.

## Test Plan

* Test file `RUF027_1.py` was extended such that the test reproduces the
false-positive

Closes https://github.com/astral-sh/ruff/issues/10023.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-25 18:17:56 -05:00
Micha Reiser
1711bca4a0 FString formatting: remove fstring handling in normalize_string (#10119) 2024-02-25 18:28:46 +01:00
Raphael Boidol
51ce88bb23 docs: update actions in documentation to recent versions without deprecation warnings (#10109) 2024-02-24 13:16:10 +00:00
Micha Reiser
36d8b03b5f Upgrade upload/download artifact actions in CI.yaml workflow (#10104) 2024-02-23 21:36:28 +01:00
Micha Reiser
a284c711bf Refactor trailing comma rule into explicit check and state update code (#10100) 2024-02-23 17:56:05 +01:00
Micha Reiser
8c20f14e62 Set PowerPC Page Size to 64KB (#10080) 2024-02-23 08:32:21 +01:00
Charlie Marsh
946028e358 Respect runtime-required decorators for function signatures (#10091)
## Summary

The original implementation of this applied the runtime-required context
to definitions _within_ the function, but not the signature itself. (We
had test coverage; the snapshot was just correctly showing the wrong
outcome.)

Closes https://github.com/astral-sh/ruff/issues/10089.
2024-02-23 03:33:08 +00:00
Charlie Marsh
6fe15e7289 Allow © in copyright notices (#10065)
Closes https://github.com/astral-sh/ruff/issues/10061.
2024-02-22 12:44:22 -05:00
Seo Sanghyeon
7d9ce5049a PLR0203: Delete entire statement, including semicolons (#10074)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-02-22 16:03:00 +00:00
Vladimir Iglovikov
ecd5a7035d Added Image Augmentation library Albumentations (13k stars) to Readme to "Who is using Ruff" section (#10075) 2024-02-22 16:59:13 +01:00
Arjun Munji
175c266de3 Omit repeated equality comparison for sys (#10054)
## Summary
Update PLR1714 to ignore `sys.platform` and `sys.version` checks. 
I'm not sure if these checks or if we need to add more. Please advise.

Fixes #10017

## Test Plan
Added a new test case and ran `cargo nextest run`
2024-02-20 19:03:32 +00:00
Charlie Marsh
4997c681f1 [pycodestyle] Allow os.environ modifications between imports (E402) (#10066)
## Summary

Allows, e.g.:

```python
import os

os.environ["WORLD_SIZE"] = "1"
os.putenv("CUDA_VISIBLE_DEVICES", "4")

import torch
```

For now, this is only allowed in preview.

Closes https://github.com/astral-sh/ruff/issues/10059
2024-02-20 13:24:27 -05:00
Seo Sanghyeon
7eafba2a4d [pyupgrade] Detect literals with unary operators (UP018) (#10060)
Fix #10029.
2024-02-20 18:21:06 +00:00
Ottavio Hartman
0f70c99c42 feat(ERA001): detect single-line code for try:, except:, etc. (#10057)
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-20 18:40:18 +01:00
Micha Reiser
ee4efdba96 Fix ecosystem (#10064) 2024-02-20 16:54:50 +01:00
Ottavio Hartman
0d363ab239 fix(ERA001): detect commented out case statements, add more one-line support (#10055)
## Summary

Closes #10031 

- Detect commented out `case` statements. Playground repro:
https://play.ruff.rs/5a305aa9-6e5c-4fa4-999a-8fc427ab9a23
- Add more support for one-line commented out code.

## Test Plan

Unit tested and tested with
```sh
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/eradicate/ERA001.py --no-cache --preview --select ERA001
```

TODO:
- [x] `cargo insta test`
2024-02-19 22:56:42 -05:00
Daniël van Noord
68b8abf9c6 [pylint] Add PLE1141 DictIterMissingItems (#9845)
## Summary

References https://github.com/astral-sh/ruff/issues/970.

Implements
[`dict-iter-missing-items`](https://pylint.readthedocs.io/en/latest/user_guide/messages/error/dict-iter-missing-items.html).

Took the tests from "upstream"
[here](https://github.com/DanielNoord/pylint/blob/main/tests/functional/d/dict_iter_missing_items.py).

~I wasn't able to implement code for one false positive, but it is
pretty estoric: https://github.com/pylint-dev/pylint/issues/3283. I
would personally argue that adding this check as preview rule without
supporting this specific use case is fine. I did add a "test" for it.~
This was implemented.

## Test Plan

Followed the Contributing guide to create tests, hopefully I didn't miss
any.
Also ran CI on my own fork and seemed to be all okay 😄 

~Edit: the ecosystem check seems a bit all over the place? 😅~ All good.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-19 19:56:55 +05:30
Seo Sanghyeon
1c8851e5fb Do multiline string test for W293 too (#10049) 2024-02-19 11:58:56 +00:00
Micha Reiser
4ac19993cf Add Micha as owner to formatter and parser (#10048) 2024-02-19 10:26:12 +00:00
dependabot[bot]
5cd3c6ef07 Bump anyhow from 1.0.79 to 1.0.80 (#10043)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:42 +01:00
dependabot[bot]
e94a2615a8 Bump semver from 1.0.21 to 1.0.22 (#10044)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:31 +01:00
dependabot[bot]
77f577cba7 Bump syn from 2.0.48 to 2.0.49 (#10045)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:21 +01:00
dependabot[bot]
49a46c2880 Bump clap from 4.5.0 to 4.5.1 (#10046)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:11 +01:00
dependabot[bot]
67e17e2750 Bump ureq from 2.9.5 to 2.9.6 (#10047)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:37:50 +01:00
Charlie Marsh
e1928be36e Allow boolean positionals in __post_init__ (#10027)
Closes https://github.com/astral-sh/ruff/issues/10011.
2024-02-18 15:03:17 +00:00
1022 changed files with 58392 additions and 36797 deletions

2
.gitattributes vendored
View File

@@ -2,6 +2,8 @@
crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf
crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf
crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf

6
.github/CODEOWNERS vendored
View File

@@ -7,3 +7,9 @@
# Jupyter
/crates/ruff_linter/src/jupyter/ @dhruvmanila
/crates/ruff_formatter/ @MichaReiser
/crates/ruff_python_formatter/ @MichaReiser
/crates/ruff_python_parser/ @MichaReiser
# flake8-pyi
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood

View File

@@ -3,6 +3,8 @@ Thank you for taking the time to report an issue! We're glad to have you involve
If you're filing a bug report, please consider including the following information:
* List of keywords you searched for before creating this issue. Write them down here so that others can find this issue more easily and help provide feedback.
e.g. "RUF001", "unused variable", "Jupyter notebook"
* A minimal code snippet that reproduces the bug.
* The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag.
* The current Ruff settings (any relevant sections from your `pyproject.toml`).

View File

@@ -133,7 +133,7 @@ jobs:
env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings"
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: ruff
path: target/debug/ruff
@@ -238,7 +238,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
name: Download comparison Ruff binary
id: ruff-target
with:
@@ -250,6 +250,7 @@ jobs:
with:
name: ruff
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- name: Install ruff-ecosystem
@@ -324,13 +325,13 @@ jobs:
run: |
echo ${{ github.event.number }} > pr-number
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: Upload PR Number
with:
name: pr-number
path: pr-number
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: Upload Results
with:
name: ecosystem-result
@@ -471,7 +472,7 @@ jobs:
- determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: extractions/setup-just@v1
- uses: extractions/setup-just@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -484,7 +485,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
name: Download development ruff binary
id: ruff-target
with:

View File

@@ -52,9 +52,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload sdist"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-sdist
path: dist
macos-x86_64:
@@ -80,9 +80,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-macos-x86_64
path: dist
- name: "Archive binary"
run: |
@@ -90,9 +90,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-macos-x86_64
path: |
*.tar.gz
*.sha256
@@ -119,9 +119,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-aarch64-apple-darwin
path: dist
- name: "Archive binary"
run: |
@@ -129,9 +129,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-aarch64-apple-darwin
path: |
*.tar.gz
*.sha256
@@ -170,9 +170,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
shell: bash
@@ -181,9 +181,9 @@ jobs:
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.platform.target }}
path: |
*.zip
*.sha256
@@ -218,9 +218,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
run: |
@@ -228,9 +228,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
@@ -251,8 +251,12 @@ jobs:
arch: s390x
- target: powerpc64le-unknown-linux-gnu
arch: ppc64le
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: powerpc64-unknown-linux-gnu
arch: ppc64
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
steps:
- uses: actions/checkout@v4
@@ -285,9 +289,9 @@ jobs:
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
run: |
@@ -295,9 +299,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
@@ -337,9 +341,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
run: |
@@ -347,9 +351,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
@@ -394,9 +398,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
run: |
@@ -404,9 +408,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
@@ -463,10 +467,11 @@ jobs:
# For pypi trusted publishing
id-token: write
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: wheels
pattern: wheels-*
path: wheels
merge-multiple: true
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
@@ -506,12 +511,13 @@ jobs:
# For GitHub release publishing
contents: write
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: binaries
pattern: binaries-*
path: binaries
merge-multiple: true
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
draft: true
files: binaries/*

View File

@@ -1,5 +1,56 @@
# Breaking Changes
## 0.3.0
### Ruff 2024.2 style
The formatter now formats code according to the Ruff 2024.2 style guide. Read the [changelog](./CHANGELOG.md#030) for a detailed list of stabilized style changes.
### `isort`: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971))
Previously, Ruff used one or two blank lines (or the number configured by `isort.lines-after-imports`) after imports in typing stub files (`.pyi` files).
The [typing style guide for stubs](https://typing.readthedocs.io/en/latest/source/stubs.html#style-guide) recommends using at most 1 blank line for grouping.
As of this release, `isort` now always uses one blank line after imports in stub files, the same as the formatter.
### `build` is no longer excluded by default ([#10093](https://github.com/astral-sh/ruff/pull/10093))
Ruff maintains a list of directories and files that are excluded by default. This list now consists of the following patterns:
- `.bzr`
- `.direnv`
- `.eggs`
- `.git`
- `.git-rewrite`
- `.hg`
- `.ipynb_checkpoints`
- `.mypy_cache`
- `.nox`
- `.pants.d`
- `.pyenv`
- `.pytest_cache`
- `.pytype`
- `.ruff_cache`
- `.svn`
- `.tox`
- `.venv`
- `.vscode`
- `__pypackages__`
- `_build`
- `buck-out`
- `dist`
- `node_modules`
- `site-packages`
- `venv`
Previously, the `build` directory was included in this list. However, the `build` directory tends to be a not-unpopular directory
name, and excluding it by default caused confusion. Ruff now no longer excludes `build` except if it is excluded by a `.gitignore` file
or because it is listed in `extend-exclude`.
### `--format` is no longer a valid `rule` or `linter` command option
Previously, `ruff rule` and `ruff linter` accepted the `--format <FORMAT>` option as an alias for `--output-format`. Ruff no longer
supports this alias. Please use `ruff rule --output-format <FORMAT>` and `ruff linter --output-format <FORMAT>` instead.
## 0.1.9
### `site-packages` is now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))

View File

@@ -1,5 +1,125 @@
# Changelog
## 0.3.2
### Preview features
- Improve single-`with` item formatting for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276))
### Rule changes
- \[`pyupgrade`\] Allow fixes for f-string rule regardless of line length (`UP032`) ([#10263](https://github.com/astral-sh/ruff/pull/10263))
- \[`pycodestyle`\] Include actual conditions in E712 diagnostics ([#10254](https://github.com/astral-sh/ruff/pull/10254))
### Bug fixes
- Fix trailing kwargs end of line comment after slash ([#10297](https://github.com/astral-sh/ruff/pull/10297))
- Fix unstable `with` items formatting ([#10274](https://github.com/astral-sh/ruff/pull/10274))
- Avoid repeating function calls in f-string conversions ([#10265](https://github.com/astral-sh/ruff/pull/10265))
- Fix E203 false positive for slices in format strings ([#10280](https://github.com/astral-sh/ruff/pull/10280))
- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283))
- Treat `typing.Annotated` subscripts as type definitions ([#10285](https://github.com/astral-sh/ruff/pull/10285))
## 0.3.1
### Preview features
- \[`pycodestyle`\] Fix E301 not triggering on decorated methods. ([#10117](https://github.com/astral-sh/ruff/pull/10117))
- \[`pycodestyle`\] Respect `isort` settings in blank line rules (`E3*`) ([#10096](https://github.com/astral-sh/ruff/pull/10096))
- \[`pycodestyle`\] Make blank lines in typing stub files optional (`E3*`) ([#10098](https://github.com/astral-sh/ruff/pull/10098))
- \[`pylint`\] Implement `singledispatch-method` (`E1519`) ([#10140](https://github.com/astral-sh/ruff/pull/10140))
- \[`pylint`\] Implement `useless-exception-statement` (`W0133`) ([#10176](https://github.com/astral-sh/ruff/pull/10176))
### Rule changes
- \[`flake8-debugger`\] Check for use of `debugpy` and `ptvsd` debug modules (#10177) ([#10194](https://github.com/astral-sh/ruff/pull/10194))
- \[`pyupgrade`\] Generate diagnostic for all valid f-string conversions regardless of line length (`UP032`) ([#10238](https://github.com/astral-sh/ruff/pull/10238))
- \[`pep8_naming`\] Add fixes for `N804` and `N805` ([#10215](https://github.com/astral-sh/ruff/pull/10215))
### CLI
- Colorize the output of `ruff format --diff` ([#10110](https://github.com/astral-sh/ruff/pull/10110))
- Make `--config` and `--isolated` global flags ([#10150](https://github.com/astral-sh/ruff/pull/10150))
- Correctly expand tildes and environment variables in paths passed to `--config` ([#10219](https://github.com/astral-sh/ruff/pull/10219))
### Configuration
- Accept a PEP 440 version specifier for `required-version` ([#10216](https://github.com/astral-sh/ruff/pull/10216))
- Implement isort's `default-section` setting ([#10149](https://github.com/astral-sh/ruff/pull/10149))
### Bug fixes
- Remove trailing space from `CapWords` message ([#10220](https://github.com/astral-sh/ruff/pull/10220))
- Respect external codes in file-level exemptions ([#10203](https://github.com/astral-sh/ruff/pull/10203))
- \[`flake8-raise`\] Avoid false-positives for parens-on-raise with `future.exception()` (`RSE102`) ([#10206](https://github.com/astral-sh/ruff/pull/10206))
- \[`pylint`\] Add fix for unary expressions in `PLC2801` ([#9587](https://github.com/astral-sh/ruff/pull/9587))
- \[`ruff`\] Fix RUF028 not allowing `# fmt: skip` on match cases ([#10178](https://github.com/astral-sh/ruff/pull/10178))
## 0.3.0
This release introduces the new Ruff formatter 2024.2 style and adds a new lint rule to
detect invalid formatter suppression comments.
### Preview features
- \[`flake8-bandit`\] Remove suspicious-lxml-import (`S410`) ([#10154](https://github.com/astral-sh/ruff/pull/10154))
- \[`pycodestyle`\] Allow `os.environ` modifications between imports (`E402`) ([#10066](https://github.com/astral-sh/ruff/pull/10066))
- \[`pycodestyle`\] Don't warn about a single whitespace character before a comma in a tuple (`E203`) ([#10094](https://github.com/astral-sh/ruff/pull/10094))
### Rule changes
- \[`eradicate`\] Detect commented out `case` statements (`ERA001`) ([#10055](https://github.com/astral-sh/ruff/pull/10055))
- \[`eradicate`\] Detect single-line code for `try:`, `except:`, etc. (`ERA001`) ([#10057](https://github.com/astral-sh/ruff/pull/10057))
- \[`flake8-boolean-trap`\] Allow boolean positionals in `__post_init__` ([#10027](https://github.com/astral-sh/ruff/pull/10027))
- \[`flake8-copyright`\] Allow © in copyright notices ([#10065](https://github.com/astral-sh/ruff/pull/10065))
- \[`isort`\]: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971))
- \[`pylint`\] New Rule `dict-iter-missing-items` (`PLE1141`) ([#9845](https://github.com/astral-sh/ruff/pull/9845))
- \[`pylint`\] Ignore `sys.version` and `sys.platform` (`PLR1714`) ([#10054](https://github.com/astral-sh/ruff/pull/10054))
- \[`pyupgrade`\] Detect literals with unary operators (`UP018`) ([#10060](https://github.com/astral-sh/ruff/pull/10060))
- \[`ruff`\] Expand rule for `list(iterable).pop(0)` idiom (`RUF015`) ([#10148](https://github.com/astral-sh/ruff/pull/10148))
### Formatter
This release introduces the Ruff 2024.2 style, stabilizing the following changes:
- Prefer splitting the assignment's value over the target or type annotation ([#8943](https://github.com/astral-sh/ruff/pull/8943))
- Remove blank lines before class docstrings ([#9154](https://github.com/astral-sh/ruff/pull/9154))
- Wrap multiple context managers in `with` parentheses when targeting Python 3.9 or newer ([#9222](https://github.com/astral-sh/ruff/pull/9222))
- Add a blank line after nested classes with a dummy body (`...`) in typing stub files ([#9155](https://github.com/astral-sh/ruff/pull/9155))
- Reduce vertical spacing for classes and functions with a dummy (`...`) body ([#7440](https://github.com/astral-sh/ruff/issues/7440), [#9240](https://github.com/astral-sh/ruff/pull/9240))
- Add a blank line after the module docstring ([#8283](https://github.com/astral-sh/ruff/pull/8283))
- Parenthesize long type hints in assignments ([#9210](https://github.com/astral-sh/ruff/pull/9210))
- Preserve indent for single multiline-string call-expressions ([#9673](https://github.com/astral-sh/ruff/pull/9637))
- Normalize hex escape and unicode escape sequences ([#9280](https://github.com/astral-sh/ruff/pull/9280))
- Format module docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725))
### CLI
- Explicitly disallow `extend` as part of a `--config` flag ([#10135](https://github.com/astral-sh/ruff/pull/10135))
- Remove `build` from the default exclusion list ([#10093](https://github.com/astral-sh/ruff/pull/10093))
- Deprecate `ruff <path>`, `ruff --explain`, `ruff --clean`, and `ruff --generate-shell-completion` in favor of `ruff check <path>`, `ruff rule`, `ruff clean`, and `ruff generate-shell-completion` ([#10169](https://github.com/astral-sh/ruff/pull/10169))
- Remove the deprecated CLI option `--format` from `ruff rule` and `ruff linter` ([#10170](https://github.com/astral-sh/ruff/pull/10170))
### Bug fixes
- \[`flake8-bugbear`\] Avoid adding default initializers to stubs (`B006`) ([#10152](https://github.com/astral-sh/ruff/pull/10152))
- \[`flake8-type-checking`\] Respect runtime-required decorators for function signatures ([#10091](https://github.com/astral-sh/ruff/pull/10091))
- \[`pycodestyle`\] Mark fixes overlapping with a multiline string as unsafe (`W293`) ([#10049](https://github.com/astral-sh/ruff/pull/10049))
- \[`pydocstyle`\] Trim whitespace when removing blank lines after section (`D413`) ([#10162](https://github.com/astral-sh/ruff/pull/10162))
- \[`pylint`\] Delete entire statement, including semicolons (`PLR0203`) ([#10074](https://github.com/astral-sh/ruff/pull/10074))
- \[`ruff`\] Avoid f-string false positives in `gettext` calls (`RUF027`) ([#10118](https://github.com/astral-sh/ruff/pull/10118))
- Fix `ruff` crashing on PowerPC systems because of too small page size ([#10080](https://github.com/astral-sh/ruff/pull/10080))
### Performance
- Add cold attribute to less likely printer queue branches in the formatter ([#10121](https://github.com/astral-sh/ruff/pull/10121))
- Skip unnecessary string normalization in the formatter ([#10116](https://github.com/astral-sh/ruff/pull/10116))
### Documentation
- Remove "Beta" Label from formatter documentation ([#10144](https://github.com/astral-sh/ruff/pull/10144))
- `line-length` option: fix link to `pycodestyle.max-line-length` ([#10136](https://github.com/astral-sh/ruff/pull/10136))
## 0.2.2
Highlights include:

View File

@@ -316,7 +316,7 @@ To preview any changes to the documentation locally:
```
The documentation should then be available locally at
[http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/).
[http://127.0.0.1:8000/ruff/](http://127.0.0.1:8000/ruff/).
## Release Process
@@ -329,13 +329,13 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
### Creating a new release
We use an experimental in-house tool for managing releases.
1. Install `rooster`: `pip install git+https://github.com/zanieb/rooster@main`
1. Run `rooster release`; this command will:
1. Install `uv`: `curl -LsSf https://astral.sh/uv/install.sh | sh`
1. Run `./scripts/release/bump.sh`; this command will:
- Generate a temporary virtual environment with `rooster`
- Generate a changelog entry in `CHANGELOG.md`
- Update versions in `pyproject.toml` and `Cargo.toml`
- Update references to versions in the `README.md` and documentation
- Display contributors for the release
1. The changelog should then be editorialized for consistency
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details
@@ -359,7 +359,7 @@ We use an experimental in-house tool for managing releases.
1. Open the draft release in the GitHub release section
1. Copy the changelog for the release into the GitHub release
- See previous releases for formatting of section headers
1. Generate the contributor list with `rooster contributors` and add to the release notes
1. Append the contributors from the `bump.sh` script
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
1. One can determine if an update is needed when
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.

528
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,50 +14,55 @@ license = "MIT"
[workspace.dependencies]
aho-corasick = { version = "1.1.2" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.79" }
anyhow = { version = "1.0.80" }
argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.13" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
bstr = { version = "1.9.0" }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.34", default-features = false, features = ["clock"] }
clap = { version = "4.4.18", features = ["derive"] }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
clap = { version = "4.5.2", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.3.3", default-features = false }
codspeed-criterion-compat = { version = "2.4.0", default-features = false }
colored = { version = "2.1.0" }
configparser = { version = "3.0.3" }
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version ="3.0.1"}
countme = { version = "3.0.1" }
criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dirs = { version = "5.0.0" }
drop_bomb = { version = "0.1.5" }
env_logger = { version ="0.10.1"}
env_logger = { version = "0.10.1" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
fs-err = { version ="2.11.0"}
fs-err = { version = "2.11.0" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
hexf-parse = { version ="0.2.1"}
hexf-parse = { version = "0.2.1" }
ignore = { version = "0.4.22" }
imara-diff ={ version = "0.1.5"}
imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" }
indicatif ={ version = "0.17.8"}
indoc ={ version = "2.0.4"}
insta = { version = "1.34.0", feature = ["filters", "glob"] }
indicatif = { version = "0.17.8" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1", feature = ["filters", "glob"] }
insta-cmd = { version = "0.4.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.12.1" }
js-sys = { version = "0.3.67" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "0.1.2" }
lalrpop-util = { version = "0.20.0", default-features = false }
lexical-parse-float = { version = "0.8.0", features = ["format"] }
libc = { version = "0.2.153" }
libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" }
lsp-server = { version = "0.7.6" }
lsp-types = { version = "0.95.0", features = ["proposed"] }
memchr = { version = "2.7.1" }
mimalloc = { version ="0.1.39"}
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "6.1.1" }
once_cell = { version = "1.19.0" }
@@ -75,39 +80,39 @@ regex = { version = "1.10.2" }
result-like = { version = "0.5.0" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" }
seahash = { version ="4.1.0"}
semver = { version = "1.0.21" }
serde = { version = "1.0.196", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.3" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.4" }
serde_json = { version = "1.0.113" }
serde_test = { version = "1.0.152" }
serde_with = { version = "3.6.0", default-features = false, features = ["macros"] }
shellexpand = { version = "3.0.0" }
shlex = { version ="1.3.0"}
shlex = { version = "1.3.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.1" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.40" }
tempfile = { version ="3.9.0"}
syn = { version = "2.0.51" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.57" }
tikv-jemallocator = { version ="0.5.0"}
tikv-jemallocator = { version = "0.5.0" }
toml = { version = "0.8.9" }
tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-tree = { version = "0.2.4" }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version ="0.9"}
unic-ucd-category = { version = "0.9" }
unicode-ident = { version = "1.0.12" }
unicode-width = { version = "0.1.11" }
unicode_names2 = { version = "1.2.1" }
ureq = { version = "2.9.1" }
unicode_names2 = { version = "1.2.2" }
ureq = { version = "2.9.6" }
url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
walkdir = { version = "2.3.2" }
wasm-bindgen = { version = "0.2.84" }
wasm-bindgen = { version = "0.2.92" }
wasm-bindgen-test = { version = "0.3.40" }
wild = { version = "2" }

View File

@@ -7,8 +7,9 @@
[![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/astral-sh/ruff/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff/actions)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/astral-sh)
[**Discord**](https://discord.com/invite/astral-sh) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
[**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
An extremely fast Python linter and code formatter, written in Rust.
@@ -128,7 +129,7 @@ and with [a variety of other package managers](https://docs.astral.sh/ruff/insta
To run Ruff as a linter, try any of the following:
```shell
ruff check . # Lint all files in the current directory (and any subdirectories).
ruff check # Lint all files in the current directory (and any subdirectories).
ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories).
ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`.
ruff check path/to/code/to/file.py # Lint `file.py`.
@@ -138,7 +139,7 @@ ruff check @arguments.txt # Lint using an input file, treating its con
Or, to run Ruff as a formatter:
```shell
ruff format . # Format all files in the current directory (and any subdirectories).
ruff format # Format all files in the current directory (and any subdirectories).
ruff format path/to/code/ # Format all files in `/path/to/code` (and any subdirectories).
ruff format path/to/code/*.py # Format all `.py` files in `/path/to/code`.
ruff format path/to/code/to/file.py # Format `file.py`.
@@ -150,7 +151,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.2.2
rev: v0.3.2
hooks:
# Run the linter.
- id: ruff
@@ -172,7 +173,7 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
```
@@ -182,10 +183,9 @@ Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml`
[_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/)
for a complete list of all configuration options).
If left unspecified, Ruff's default configuration is equivalent to:
If left unspecified, Ruff's default configuration is equivalent to the following `ruff.toml` file:
```toml
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
@@ -223,7 +223,7 @@ indent-width = 4
# Assume Python 3.8
target-version = "py38"
[tool.ruff.lint]
[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
@@ -235,7 +235,7 @@ unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
[format]
# Like Black, use double quotes for strings.
quote-style = "double"
@@ -249,11 +249,20 @@ skip-magic-trailing-comma = false
line-ending = "auto"
```
Some configuration options can be provided via the command-line, such as those related to
rule enablement and disablement, file discovery, and logging level:
Note that, in a `pyproject.toml`, each section header should be prefixed with `tool.ruff`. For
example, `[lint]` should be replaced with `[tool.ruff.lint]`.
Some configuration options can be provided via dedicated command-line arguments, such as those
related to rule enablement and disablement, file discovery, and logging level:
```shell
ruff check path/to/code/ --select F401 --select F403 --quiet
ruff check --select F401 --select F403 --quiet
```
The remaining configuration options can be provided through a catch-all `--config` argument:
```shell
ruff check --config "lint.per-file-ignores = {'some_file.py' = ['F841']}"
```
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format`
@@ -378,6 +387,7 @@ Ruff is released under the MIT license.
Ruff is used by a number of major open-source projects and companies, including:
- [Albumentations](https://github.com/albumentations-team/albumentations)
- Amazon ([AWS SAM](https://github.com/aws/serverless-application-model))
- Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python))
- [Apache Airflow](https://github.com/apache/airflow)
@@ -404,6 +414,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Ibis](https://github.com/ibis-project/ibis)
- [ivy](https://github.com/unifyai/ivy)
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Kraken Tech](https://kraken.tech/)
- [LangChain](https://github.com/hwchase17/langchain)
- [Litestar](https://litestar.dev/)
- [LlamaIndex](https://github.com/jerryjliu/llama_index)

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.2.2"
version = "0.3.2"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -20,6 +20,7 @@ ruff_macros = { path = "../ruff_macros" }
ruff_notebook = { path = "../ruff_notebook" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_server = { path = "../ruff_server" }
ruff_source_file = { path = "../ruff_source_file" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_workspace = { path = "../ruff_workspace" }
@@ -52,6 +53,8 @@ tempfile = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["registry"]}
tracing-tree = { workspace = true }
walkdir = { workspace = true }
wild = { workspace = true }

View File

@@ -28,6 +28,52 @@ use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::options::{Options, PycodestyleOptions};
use ruff_workspace::resolver::ConfigurationTransformer;
/// All configuration options that can be passed "globally",
/// i.e., can be passed to all subcommands
#[derive(Debug, Default, Clone, clap::Args)]
pub struct GlobalConfigArgs {
#[clap(flatten)]
log_level_args: LogLevelArgs,
/// Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`),
/// or a TOML `<KEY> = <VALUE>` pair
/// (such as you might find in a `ruff.toml` configuration file)
/// overriding a specific configuration option.
/// Overrides of individual settings using this option always take precedence
/// over all configuration files, including configuration files that were also
/// specified using `--config`.
#[arg(
long,
action = clap::ArgAction::Append,
value_name = "CONFIG_OPTION",
value_parser = ConfigArgumentParser,
global = true,
help_heading = "Global options",
)]
pub config: Vec<SingleConfigArgument>,
/// Ignore all configuration files.
//
// Note: We can't mark this as conflicting with `--config` here
// as `--config` can be used for specifying configuration overrides
// as well as configuration files.
// Specifying a configuration file conflicts with `--isolated`;
// specifying a configuration override does not.
// If a user specifies `ruff check --isolated --config=ruff.toml`,
// we emit an error later on, after the initial parsing by clap.
#[arg(long, help_heading = "Global options", global = true)]
pub isolated: bool,
}
impl GlobalConfigArgs {
pub fn log_level(&self) -> LogLevel {
LogLevel::from(&self.log_level_args)
}
#[must_use]
fn partition(self) -> (LogLevel, Vec<SingleConfigArgument>, bool) {
(self.log_level(), self.config, self.isolated)
}
}
#[derive(Debug, Parser)]
#[command(
author,
@@ -38,9 +84,9 @@ use ruff_workspace::resolver::ConfigurationTransformer;
#[command(version)]
pub struct Args {
#[command(subcommand)]
pub command: Command,
pub(crate) command: Command,
#[clap(flatten)]
pub log_level_args: LogLevelArgs,
pub(crate) global_options: GlobalConfigArgs,
}
#[allow(clippy::large_enum_variant)]
@@ -63,10 +109,6 @@ pub enum Command {
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
},
/// List or describe the available configuration options.
Config { option: Option<String> },
@@ -75,10 +117,6 @@ pub enum Command {
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
},
/// Clear any caches in the current directory and any subdirectories.
#[clap(alias = "--clean")]
@@ -88,6 +126,8 @@ pub enum Command {
GenerateShellCompletion { shell: clap_complete_command::Shell },
/// Run the Ruff formatter on the given files or directories.
Format(FormatCommand),
/// Run the language server.
Server(ServerCommand),
/// Display Ruff's version
Version {
#[arg(long, value_enum, default_value = "text")]
@@ -161,20 +201,6 @@ pub struct CheckCommand {
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
/// Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`),
/// or a TOML `<KEY> = <VALUE>` pair
/// (such as you might find in a `ruff.toml` configuration file)
/// overriding a specific configuration option.
/// Overrides of individual settings using this option always take precedence
/// over all configuration files, including configuration files that were also
/// specified using `--config`.
#[arg(
long,
action = clap::ArgAction::Append,
value_name = "CONFIG_OPTION",
value_parser = ConfigArgumentParser,
)]
pub config: Vec<SingleConfigArgument>,
/// Comma-separated list of rule codes to enable (or ALL, to enable all rules).
#[arg(
long,
@@ -306,17 +332,6 @@ pub struct CheckCommand {
/// Disable cache reads.
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
pub no_cache: bool,
/// Ignore all configuration files.
//
// Note: We can't mark this as conflicting with `--config` here
// as `--config` can be used for specifying configuration overrides
// as well as configuration files.
// Specifying a configuration file conflicts with `--isolated`;
// specifying a configuration override does not.
// If a user specifies `ruff check --isolated --config=ruff.toml`,
// we emit an error later on, after the initial parsing by clap.
#[arg(long, help_heading = "Miscellaneous")]
pub isolated: bool,
/// Path to the cache directory.
#[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")]
pub cache_dir: Option<PathBuf>,
@@ -408,20 +423,6 @@ pub struct FormatCommand {
/// difference between the current file and how the formatted file would look like.
#[arg(long)]
pub diff: bool,
/// Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`),
/// or a TOML `<KEY> = <VALUE>` pair
/// (such as you might find in a `ruff.toml` configuration file)
/// overriding a specific configuration option.
/// Overrides of individual settings using this option always take precedence
/// over all configuration files, including configuration files that were also
/// specified using `--config`.
#[arg(
long,
action = clap::ArgAction::Append,
value_name = "CONFIG_OPTION",
value_parser = ConfigArgumentParser,
)]
pub config: Vec<SingleConfigArgument>,
/// Disable cache reads.
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
@@ -462,17 +463,6 @@ pub struct FormatCommand {
/// Set the line-length.
#[arg(long, help_heading = "Format configuration")]
pub line_length: Option<LineLength>,
/// Ignore all configuration files.
//
// Note: We can't mark this as conflicting with `--config` here
// as `--config` can be used for specifying configuration overrides
// as well as configuration files.
// Specifying a configuration file conflicts with `--isolated`;
// specifying a configuration override does not.
// If a user specifies `ruff check --isolated --config=ruff.toml`,
// we emit an error later on, after the initial parsing by clap.
#[arg(long, help_heading = "Miscellaneous")]
pub isolated: bool,
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
@@ -506,6 +496,13 @@ pub struct FormatCommand {
pub range: Option<FormatRange>,
}
#[derive(Clone, Debug, clap::Parser)]
pub struct ServerCommand {
/// Enable preview mode; required for regular operation
#[arg(long)]
pub(crate) preview: bool,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum HelpFormat {
Text,
@@ -513,7 +510,7 @@ pub enum HelpFormat {
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, clap::Args)]
#[derive(Debug, Default, Clone, clap::Args)]
pub struct LogLevelArgs {
/// Enable verbose logging.
#[arg(
@@ -561,6 +558,10 @@ impl From<&LogLevelArgs> for LogLevel {
/// Configuration-related arguments passed via the CLI.
#[derive(Default)]
pub struct ConfigArguments {
/// Whether the user specified --isolated on the command line
pub(crate) isolated: bool,
/// The logging level to be used, derived from command-line arguments passed
pub(crate) log_level: LogLevel,
/// Path to a pyproject.toml or ruff.toml configuration file (etc.).
/// Either 0 or 1 configuration file paths may be provided on the command line.
config_file: Option<PathBuf>,
@@ -581,21 +582,19 @@ impl ConfigArguments {
}
fn from_cli_arguments(
config_options: Vec<SingleConfigArgument>,
global_options: GlobalConfigArgs,
per_flag_overrides: ExplicitConfigOverrides,
isolated: bool,
) -> anyhow::Result<Self> {
let mut new = Self {
per_flag_overrides,
..Self::default()
};
let (log_level, config_options, isolated) = global_options.partition();
let mut config_file: Option<PathBuf> = None;
let mut overrides = Configuration::default();
for option in config_options {
match option {
SingleConfigArgument::SettingsOverride(overridden_option) => {
let overridden_option = Arc::try_unwrap(overridden_option)
.unwrap_or_else(|option| option.deref().clone());
new.overrides = new.overrides.combine(Configuration::from_options(
overrides = overrides.combine(Configuration::from_options(
overridden_option,
None,
&path_dedot::CWD,
@@ -614,7 +613,7 @@ The argument `--config={}` cannot be used with `--isolated`
path.display()
);
}
if let Some(ref config_file) = new.config_file {
if let Some(ref config_file) = config_file {
let (first, second) = (config_file.display(), path.display());
bail!(
"\
@@ -625,11 +624,17 @@ You cannot specify more than one configuration file on the command line.
"
);
}
new.config_file = Some(path);
config_file = Some(path);
}
}
}
Ok(new)
Ok(Self {
isolated,
log_level,
config_file,
overrides,
per_flag_overrides,
})
}
}
@@ -643,7 +648,10 @@ impl ConfigurationTransformer for ConfigArguments {
impl CheckCommand {
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(self) -> anyhow::Result<(CheckArguments, ConfigArguments)> {
pub fn partition(
self,
global_options: GlobalConfigArgs,
) -> anyhow::Result<(CheckArguments, ConfigArguments)> {
let check_arguments = CheckArguments {
add_noqa: self.add_noqa,
diff: self.diff,
@@ -652,7 +660,6 @@ impl CheckCommand {
exit_zero: self.exit_zero,
files: self.files,
ignore_noqa: self.ignore_noqa,
isolated: self.isolated,
no_cache: self.no_cache,
output_file: self.output_file,
show_files: self.show_files,
@@ -696,8 +703,7 @@ impl CheckCommand {
extension: self.extension,
};
let config_args =
ConfigArguments::from_cli_arguments(self.config, cli_overrides, self.isolated)?;
let config_args = ConfigArguments::from_cli_arguments(global_options, cli_overrides)?;
Ok((check_arguments, config_args))
}
}
@@ -705,12 +711,14 @@ impl CheckCommand {
impl FormatCommand {
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(self) -> anyhow::Result<(FormatArguments, ConfigArguments)> {
pub fn partition(
self,
global_options: GlobalConfigArgs,
) -> anyhow::Result<(FormatArguments, ConfigArguments)> {
let format_arguments = FormatArguments {
check: self.check,
diff: self.diff,
files: self.files,
isolated: self.isolated,
no_cache: self.no_cache,
stdin_filename: self.stdin_filename,
range: self.range,
@@ -730,8 +738,7 @@ impl FormatCommand {
..ExplicitConfigOverrides::default()
};
let config_args =
ConfigArguments::from_cli_arguments(self.config, cli_overrides, self.isolated)?;
let config_args = ConfigArguments::from_cli_arguments(global_options, cli_overrides)?;
Ok((format_arguments, config_args))
}
}
@@ -745,38 +752,34 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
}
}
/// Enumeration of various ways in which a --config CLI flag
/// could be invalid
#[derive(Debug)]
enum TomlParseFailureKind {
SyntaxError,
UnknownOption,
enum InvalidConfigFlagReason {
InvalidToml(toml::de::Error),
/// It was valid TOML, but not a valid ruff config file.
/// E.g. the user tried to select a rule that doesn't exist,
/// or tried to enable a setting that doesn't exist
ValidTomlButInvalidRuffSchema(toml::de::Error),
/// It was a valid ruff config file, but the user tried to pass a
/// value for `extend` as part of the config override.
// `extend` is special, because it affects which config files we look at
/// in the first place. We currently only parse --config overrides *after*
/// we've combined them with all the arguments from the various config files
/// that we found, so trying to override `extend` as part of a --config
/// override is forbidden.
ExtendPassedViaConfigFlag,
}
impl std::fmt::Display for TomlParseFailureKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let display = match self {
Self::SyntaxError => "The supplied argument is not valid TOML",
Self::UnknownOption => {
impl InvalidConfigFlagReason {
const fn description(&self) -> &'static str {
match self {
Self::InvalidToml(_) => "The supplied argument is not valid TOML",
Self::ValidTomlButInvalidRuffSchema(_) => {
"Could not parse the supplied argument as a `ruff.toml` configuration option"
}
};
write!(f, "{display}")
}
}
#[derive(Debug)]
struct TomlParseFailure {
kind: TomlParseFailureKind,
underlying_error: toml::de::Error,
}
impl std::fmt::Display for TomlParseFailure {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let TomlParseFailure {
kind,
underlying_error,
} = self;
let display = format!("{kind}:\n\n{underlying_error}");
write!(f, "{}", display.trim_end())
Self::ExtendPassedViaConfigFlag => "Cannot include `extend` in a --config flag value",
}
}
}
@@ -818,27 +821,38 @@ impl TypedValueParser for ConfigArgumentParser {
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let path_to_config_file = PathBuf::from(value);
if path_to_config_file.exists() {
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
// Convert to UTF-8.
let Some(value) = value.to_str() else {
// But respect non-UTF-8 paths.
let path_to_config_file = PathBuf::from(value);
if path_to_config_file.is_file() {
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
}
return Err(clap::Error::new(clap::error::ErrorKind::InvalidUtf8));
};
// Expand environment variables and tildes.
if let Ok(path_to_config_file) =
shellexpand::full(value).map(|config| PathBuf::from(&*config))
{
if path_to_config_file.is_file() {
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
}
}
let value = value
.to_str()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
let toml_parse_error = match toml::Table::from_str(value) {
Ok(table) => match table.try_into() {
Ok(option) => return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option))),
Err(underlying_error) => TomlParseFailure {
kind: TomlParseFailureKind::UnknownOption,
underlying_error,
},
},
Err(underlying_error) => TomlParseFailure {
kind: TomlParseFailureKind::SyntaxError,
underlying_error,
let config_parse_error = match toml::Table::from_str(value) {
Ok(table) => match table.try_into::<Options>() {
Ok(option) => {
if option.extend.is_none() {
return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option)));
}
InvalidConfigFlagReason::ExtendPassedViaConfigFlag
}
Err(underlying_error) => {
InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error)
}
},
Err(underlying_error) => InvalidConfigFlagReason::InvalidToml(underlying_error),
};
let mut new_error = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
@@ -853,6 +867,21 @@ impl TypedValueParser for ConfigArgumentParser {
clap::error::ContextValue::String(value.to_string()),
);
let underlying_error = match &config_parse_error {
InvalidConfigFlagReason::ExtendPassedViaConfigFlag => {
let tip = config_parse_error.description().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip]),
);
return Err(new_error);
}
InvalidConfigFlagReason::InvalidToml(underlying_error)
| InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error) => {
underlying_error
}
};
// small hack so that multiline tips
// have the same indent on the left-hand side:
let tip_indent = " ".repeat(" tip: ".len());
@@ -877,16 +906,20 @@ A `--config` flag must either be a path to a `.toml` configuration file
"
It looks like you were trying to pass a path to a configuration file.
The path `{value}` does not exist"
The path `{value}` does not point to a configuration file"
));
}
} else if value.contains('=') {
tip.push_str(&format!("\n\n{toml_parse_error}"));
tip.push_str(&format!(
"\n\n{}:\n\n{underlying_error}",
config_parse_error.description()
));
}
let tip = tip.trim_end().to_owned().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip.into()]),
clap::error::ContextValue::StyledStrs(vec![tip]),
);
Err(new_error)
@@ -941,7 +974,6 @@ pub struct CheckArguments {
pub exit_zero: bool,
pub files: Vec<PathBuf>,
pub ignore_noqa: bool,
pub isolated: bool,
pub no_cache: bool,
pub output_file: Option<PathBuf>,
pub show_files: bool,
@@ -959,7 +991,6 @@ pub struct FormatArguments {
pub no_cache: bool,
pub diff: bool,
pub files: Vec<PathBuf>,
pub isolated: bool,
pub stdin_filename: Option<PathBuf>,
pub range: Option<FormatRange>,
}

View File

@@ -383,7 +383,7 @@ pub(crate) fn init(path: &Path) -> Result<()> {
let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() {
let mut file = fs::File::create(gitignore_path)?;
file.write_all(b"*")?;
file.write_all(b"# Automatically created by ruff.\n*\n")?;
}
Ok(())

View File

@@ -61,13 +61,8 @@ impl FormatMode {
pub(crate) fn format(
cli: FormatArguments,
config_arguments: &ConfigArguments,
log_level: LogLevel,
) -> Result<ExitStatus> {
let pyproject_config = resolve(
cli.isolated,
config_arguments,
cli.stdin_filename.as_deref(),
)?;
let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?;
let mode = FormatMode::from_cli(&cli);
let files = resolve_default_files(cli.files, false);
let (paths, resolver) = python_files_in_path(&files, &pyproject_config, config_arguments)?;
@@ -202,7 +197,7 @@ pub(crate) fn format(
}
// Report on the formatting changes.
if log_level >= LogLevel::Default {
if config_arguments.log_level >= LogLevel::Default {
if mode.is_diff() {
// Allow piping the diff to e.g. a file by writing the summary to stderr
results.write_summary(&mut stderr().lock())?;
@@ -532,7 +527,7 @@ impl<'a> FormatResults<'a> {
})
.sorted_unstable_by_key(|(path, _, _)| *path)
{
unformatted.diff(formatted, Some(path), f)?;
write!(f, "{}", unformatted.diff(formatted, Some(path)).unwrap())?;
}
Ok(())

View File

@@ -23,11 +23,7 @@ pub(crate) fn format_stdin(
cli: &FormatArguments,
config_arguments: &ConfigArguments,
) -> Result<ExitStatus> {
let pyproject_config = resolve(
cli.isolated,
config_arguments,
cli.stdin_filename.as_deref(),
)?;
let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?;
let mut resolver = Resolver::new(&pyproject_config);
warn_incompatible_formatter_settings(&resolver);
@@ -122,9 +118,13 @@ fn format_source_code(
}
FormatMode::Check => {}
FormatMode::Diff => {
source_kind
.diff(formatted, path, &mut stdout().lock())
.map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
use std::io::Write;
write!(
&mut stdout().lock(),
"{}",
source_kind.diff(formatted, path).unwrap()
)
.map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
}
},
FormattedSource::Unchanged => {

View File

@@ -7,6 +7,7 @@ pub(crate) mod format;
pub(crate) mod format_stdin;
pub(crate) mod linter;
pub(crate) mod rule;
pub(crate) mod server;
pub(crate) mod show_files;
pub(crate) mod show_settings;
pub(crate) mod version;

View File

@@ -0,0 +1,73 @@
use crate::ExitStatus;
use anyhow::Result;
use ruff_linter::logging::LogLevel;
use ruff_server::Server;
use tracing::{level_filters::LevelFilter, metadata::Level, subscriber::Interest, Metadata};
use tracing_subscriber::{
layer::{Context, Filter, SubscriberExt},
Layer, Registry,
};
use tracing_tree::time::Uptime;
pub(crate) fn run_server(preview: bool, log_level: LogLevel) -> Result<ExitStatus> {
if !preview {
tracing::error!("--preview needs to be provided as a command line argument while the server is still unstable.\nFor example: `ruff server --preview`");
return Ok(ExitStatus::Error);
}
let trace_level = if log_level == LogLevel::Verbose {
Level::TRACE
} else {
Level::DEBUG
};
let subscriber = Registry::default().with(
tracing_tree::HierarchicalLayer::default()
.with_indent_lines(true)
.with_indent_amount(2)
.with_bracketed_fields(true)
.with_targets(true)
.with_writer(|| Box::new(std::io::stderr()))
.with_timer(Uptime::default())
.with_filter(LoggingFilter { trace_level }),
);
tracing::subscriber::set_global_default(subscriber)?;
let server = Server::new()?;
server.run().map(|()| ExitStatus::Success)
}
struct LoggingFilter {
trace_level: Level,
}
impl LoggingFilter {
fn is_enabled(&self, meta: &Metadata<'_>) -> bool {
let filter = if meta.target().starts_with("ruff") {
self.trace_level
} else {
Level::INFO
};
meta.level() <= &filter
}
}
impl<S> Filter<S> for LoggingFilter {
fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
self.is_enabled(meta)
}
fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
if self.is_enabled(meta) {
Interest::always()
} else {
Interest::never()
}
}
fn max_level_hint(&self) -> Option<LevelFilter> {
Some(LevelFilter::from_level(self.trace_level))
}
}

View File

@@ -3,6 +3,7 @@
use std::borrow::Cow;
use std::fs::File;
use std::io;
use std::io::Write;
use std::ops::{Add, AddAssign};
use std::path::Path;
@@ -289,10 +290,10 @@ pub(crate) fn lint_path(
match fix_mode {
flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?,
flags::FixMode::Diff => {
source_kind.diff(
transformed.as_ref(),
Some(path),
write!(
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, Some(path)).unwrap()
)?;
}
flags::FixMode::Generate => {}
@@ -442,7 +443,11 @@ pub(crate) fn lint_stdin(
flags::FixMode::Diff => {
// But only write a diff if it's non-empty.
if !fixed.is_empty() {
source_kind.diff(transformed.as_ref(), path, &mut io::stdout().lock())?;
write!(
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, path).unwrap()
)?;
}
}
flags::FixMode::Generate => {}

View File

@@ -7,6 +7,7 @@ use std::process::ExitCode;
use std::sync::mpsc::channel;
use anyhow::Result;
use args::{GlobalConfigArgs, ServerCommand};
use clap::CommandFactory;
use colored::Colorize;
use log::warn;
@@ -18,7 +19,7 @@ use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_workspace::Settings;
use crate::args::{Args, CheckCommand, Command, FormatCommand, HelpFormat};
use crate::args::{Args, CheckCommand, Command, FormatCommand};
use crate::printer::{Flags as PrinterFlags, Printer};
pub mod args;
@@ -114,20 +115,12 @@ fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> {
}
}
/// Get the actual value of the `format` desired from either `output_format`
/// or `format`, and warn the user if they're using the deprecated form.
fn resolve_help_output_format(output_format: HelpFormat, format: Option<HelpFormat>) -> HelpFormat {
if format.is_some() {
warn_user!("The `--format` argument is deprecated. Use `--output-format` instead.");
}
format.unwrap_or(output_format)
}
pub fn run(
Args {
command,
log_level_args,
global_options,
}: Args,
deprecated_alias_warning: Option<&'static str>,
) -> Result<ExitStatus> {
{
let default_panic_hook = std::panic::take_hook();
@@ -155,8 +148,11 @@ pub fn run(
#[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok());
let log_level = LogLevel::from(&log_level_args);
set_up_logging(&log_level)?;
set_up_logging(global_options.log_level())?;
if let Some(deprecated_alias_warning) = deprecated_alias_warning {
warn_user!("{}", deprecated_alias_warning);
}
match command {
Command::Version { output_format } => {
@@ -166,10 +162,8 @@ pub fn run(
Command::Rule {
rule,
all,
format,
mut output_format,
output_format,
} => {
output_format = resolve_help_output_format(output_format, format);
if all {
commands::rule::rules(output_format)?;
}
@@ -182,47 +176,46 @@ pub fn run(
commands::config::config(option.as_deref())?;
Ok(ExitStatus::Success)
}
Command::Linter {
format,
mut output_format,
} => {
output_format = resolve_help_output_format(output_format, format);
Command::Linter { output_format } => {
commands::linter::linter(output_format)?;
Ok(ExitStatus::Success)
}
Command::Clean => {
commands::clean::clean(log_level)?;
commands::clean::clean(global_options.log_level())?;
Ok(ExitStatus::Success)
}
Command::GenerateShellCompletion { shell } => {
shell.generate(&mut Args::command(), &mut stdout());
Ok(ExitStatus::Success)
}
Command::Check(args) => check(args, log_level),
Command::Format(args) => format(args, log_level),
Command::Check(args) => check(args, global_options),
Command::Format(args) => format(args, global_options),
Command::Server(args) => server(args, global_options.log_level()),
}
}
fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition()?;
fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition(global_options)?;
if is_stdin(&cli.files, cli.stdin_filename.as_deref()) {
commands::format_stdin::format_stdin(&cli, &config_arguments)
} else {
commands::format::format(cli, &config_arguments, log_level)
commands::format::format(cli, &config_arguments)
}
}
pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition()?;
#[allow(clippy::needless_pass_by_value)] // TODO: remove once we start taking arguments from here
fn server(args: ServerCommand, log_level: LogLevel) -> Result<ExitStatus> {
let ServerCommand { preview } = args;
commands::server::run_server(preview, log_level)
}
pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition(global_options)?;
// Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy.
let pyproject_config = resolve::resolve(
cli.isolated,
&config_arguments,
cli.stdin_filename.as_deref(),
)?;
let pyproject_config = resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?;
let mut writer: Box<dyn Write> = match cli.output_file {
Some(path) if !cli.watch => {
@@ -313,7 +306,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
}
let modifications =
commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments)?;
if modifications > 0 && log_level >= LogLevel::Default {
if modifications > 0 && config_arguments.log_level >= LogLevel::Default {
let s = if modifications == 1 { "" } else { "s" };
#[allow(clippy::print_stderr)]
{
@@ -325,7 +318,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
let printer = Printer::new(
output_format,
log_level,
config_arguments.log_level,
fix_mode,
unsafe_fixes,
printer_flags,
@@ -382,11 +375,8 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
};
if matches!(change_kind, ChangeKind::Configuration) {
pyproject_config = resolve::resolve(
cli.isolated,
&config_arguments,
cli.stdin_filename.as_deref(),
)?;
pyproject_config =
resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?;
}
Printer::clear_screen()?;
printer.write_to_user("File change detected...\n");

View File

@@ -27,26 +27,42 @@ pub fn main() -> ExitCode {
let mut args =
argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
// Clap doesn't support default subcommands but we want to run `check` by
// default for convenience and backwards-compatibility, so we just
// preprocess the arguments accordingly before passing them to Clap.
if let Some(arg) = args.get(1) {
if arg
.to_str()
.is_some_and(|arg| !Command::has_subcommand(rewrite_legacy_subcommand(arg)))
// We can't use `warn_user` here because logging isn't set up at this point
// and we also don't know if the user runs ruff with quiet.
// Keep the message and pass it to `run` that is responsible for emitting the warning.
let deprecated_alias_warning = match args.get(1).and_then(|arg| arg.to_str()) {
// Deprecated aliases that are handled by clap
Some("--explain") => {
Some("`ruff --explain <RULE>` is deprecated. Use `ruff rule <RULE>` instead.")
}
Some("--clean") => {
Some("`ruff --clean` is deprecated. Use `ruff clean` instead.")
}
Some("--generate-shell-completion") => {
Some("`ruff --generate-shell-completion <SHELL>` is deprecated. Use `ruff generate-shell-completion <SHELL>` instead.")
}
// Deprecated `ruff` alias to `ruff check`
// Clap doesn't support default subcommands but we want to run `check` by
// default for convenience and backwards-compatibility, so we just
// preprocess the arguments accordingly before passing them to Clap.
Some(arg) if !Command::has_subcommand(arg)
&& arg != "-h"
&& arg != "--help"
&& arg != "-V"
&& arg != "--version"
&& arg != "help"
{
args.insert(1, "check".into());
}
}
&& arg != "help" => {
{
args.insert(1, "check".into());
Some("`ruff <path>` is deprecated. Use `ruff check <path>` instead.")
}
},
_ => None
};
let args = Args::parse_from(args);
match run(args) {
match run(args, deprecated_alias_warning) {
Ok(code) => code.into(),
Err(err) => {
#[allow(clippy::print_stderr)]
@@ -65,12 +81,3 @@ pub fn main() -> ExitCode {
}
}
}
fn rewrite_legacy_subcommand(cmd: &str) -> &str {
match cmd {
"--explain" => "rule",
"--clean" => "clean",
"--generate-shell-completion" => "generate-shell-completion",
cmd => cmd,
}
}

View File

@@ -118,6 +118,8 @@ impl Printer {
} else if remaining > 0 {
let s = if remaining == 1 { "" } else { "s" };
writeln!(writer, "Found {remaining} error{s}.")?;
} else if remaining == 0 {
writeln!(writer, "All checks passed!")?;
}
if let Some(fixables) = fixables {

View File

@@ -1,4 +1,4 @@
use std::path::{Path, PathBuf};
use std::path::Path;
use anyhow::Result;
use log::debug;
@@ -16,12 +16,11 @@ use crate::args::ConfigArguments;
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
pub fn resolve(
isolated: bool,
config_arguments: &ConfigArguments,
stdin_filename: Option<&Path>,
) -> Result<PyprojectConfig> {
// First priority: if we're running in isolated mode, use the default settings.
if isolated {
if config_arguments.isolated {
let config = config_arguments.transform(Configuration::default());
let settings = config.into_settings(&path_dedot::CWD)?;
debug!("Isolated mode, not reading any pyproject.toml");
@@ -35,13 +34,8 @@ pub fn resolve(
// Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
if let Some(pyproject) = config_arguments
.config_file()
.map(|config| config.display().to_string())
.map(|config| shellexpand::full(&config).map(|config| PathBuf::from(config.as_ref())))
.transpose()?
{
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, config_arguments)?;
if let Some(pyproject) = config_arguments.config_file() {
let settings = resolve_root_settings(pyproject, Relativity::Cwd, config_arguments)?;
debug!(
"Using user-specified configuration file at: {}",
pyproject.display()
@@ -49,7 +43,7 @@ pub fn resolve(
return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Fixed,
settings,
Some(pyproject),
Some(pyproject.to_path_buf()),
));
}

View File

@@ -12,9 +12,10 @@ const STDIN: &str = "l = 1";
fn ruff_check(show_source: Option<bool>, output_format: Option<String>) -> Command {
let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
let output_format = output_format.unwrap_or(format!("{}", SerializationFormat::default(false)));
cmd.arg("--output-format");
cmd.arg(output_format);
cmd.arg("--no-cache");
cmd.arg("check")
.arg("--output-format")
.arg(output_format)
.arg("--no-cache");
match show_source {
Some(true) => {
cmd.arg("--show-source");

View File

@@ -7,10 +7,15 @@ use std::str;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use regex::escape;
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
fn tempdir_filter(tempdir: &TempDir) -> String {
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
}
#[test]
fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
@@ -18,7 +23,7 @@ fn default_options() {
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print('Should\'t change quotes')
print('Shouldn\'t change quotes')
if condition:
@@ -33,7 +38,7 @@ if condition:
arg1,
arg2,
):
print("Should't change quotes")
print("Shouldn't change quotes")
if condition:
@@ -106,7 +111,7 @@ fn nonexistent_config_file() {
option
It looks like you were trying to pass a path to a configuration file.
The path `foo.toml` does not exist
The path `foo.toml` does not point to a configuration file
For more information, try '--help'.
"###);
@@ -147,28 +152,29 @@ fn too_many_config_files() -> Result<()> {
let ruff2_dot_toml = tempdir.path().join("ruff2.toml");
fs::File::create(&ruff_dot_toml)?;
fs::File::create(&ruff2_dot_toml)?;
let expected_stderr = format!(
"\
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config={}` or `--config={}`.
For more information, try `--help`.
",
ruff_dot_toml.display(),
ruff2_dot_toml.display(),
);
let cmd = Command::new(get_cargo_bin(BIN_NAME))
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--config")
.arg(&ruff2_dot_toml)
.arg(".")
.output()?;
let stderr = std::str::from_utf8(&cmd.stderr)?;
assert_eq!(stderr, expected_stderr);
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config=[TMP]/ruff.toml` or `--config=[TMP]/ruff2.toml`.
For more information, try `--help`.
"###);
});
Ok(())
}
@@ -177,27 +183,29 @@ fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
fs::File::create(&ruff_dot_toml)?;
let expected_stderr = format!(
"\
ruff failed
Cause: The argument `--config={}` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
",
ruff_dot_toml.display(),
);
let cmd = Command::new(get_cargo_bin(BIN_NAME))
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--isolated")
.arg(".")
.output()?;
let stderr = std::str::from_utf8(&cmd.stderr)?;
assert_eq!(stderr, expected_stderr);
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The argument `--config=[TMP]/ruff.toml` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
"###);
});
Ok(())
}
@@ -350,58 +358,52 @@ def f(x):
'''
pass
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def f(x):
"""
Something about `f`. And an example:
success: true
exit_code: 0
----- stdout -----
def f(x):
"""
Something about `f`. And an example:
.. code-block:: python
.. code-block:: python
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
foo, bar, quux = (
this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
)
Another example:
```py
foo, bar, quux = (
this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
)
```
Another example:
And another:
```py
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
```
>>> foo, bar, quux = (
... this_is_a_long_line(
... lion,
... hippo,
... lemur,
... bear,
... )
... )
"""
pass
And another:
>>> (
... foo,
... bar,
... quux,
... ) = this_is_a_long_line(
... lion,
... hippo,
... lemur,
... bear,
... )
"""
pass
----- stderr -----
"###);
----- stderr -----
"###);
Ok(())
}

View File

@@ -71,6 +71,7 @@ impl<'a> RuffCheck<'a> {
/// Generate a [`Command`] for the `ruff check` command.
fn build(self) -> Command {
let mut cmd = ruff_cmd();
cmd.arg("check");
if let Some(output_format) = self.output_format {
cmd.args(["--output-format", output_format]);
}
@@ -100,6 +101,7 @@ fn stdin_success() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -221,6 +223,7 @@ fn stdin_source_type_pyi() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -589,6 +592,7 @@ fn stdin_fix_when_no_issues_should_still_print_contents() {
print(sys.version)
----- stderr -----
All checks passed!
"###);
}
@@ -805,13 +809,13 @@ fn full_output_format() {
}
#[test]
fn explain_status_codes_f401() {
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "F401"]));
fn rule_f401() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401"]));
}
#[test]
fn explain_status_codes_ruf404() {
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "RUF404"]), @r###"
fn rule_invalid_rule_name() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "RUF404"]), @r###"
success: false
exit_code: 2
----- stdout -----
@@ -1022,6 +1026,7 @@ fn preview_disabled_direct() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: Selection `RUF911` has no effect because preview is not enabled.
@@ -1038,6 +1043,7 @@ fn preview_disabled_prefix_empty() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: Selection `RUF91` has no effect because preview is not enabled.
@@ -1054,6 +1060,7 @@ fn preview_disabled_does_not_warn_for_empty_ignore_selections() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1069,6 +1076,7 @@ fn preview_disabled_does_not_warn_for_empty_fixable_selections() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1174,6 +1182,7 @@ fn removed_indirect() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1204,6 +1213,7 @@ fn redirect_indirect() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1306,6 +1316,7 @@ fn deprecated_indirect_preview_enabled() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1348,7 +1359,7 @@ fn unreadable_pyproject_toml() -> Result<()> {
// Don't `--isolated` since the configuration discovery is where the error happens
let args = Args::parse_from(["", "check", "--no-cache", tempdir.path().to_str().unwrap()]);
let err = run(args).err().context("Unexpected success")?;
let err = run(args, None).err().context("Unexpected success")?;
assert_eq!(
err.chain()
.map(std::string::ToString::to_string)
@@ -1382,6 +1393,7 @@ fn unreadable_dir() -> Result<()> {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: Encountered error: Permission denied (os error 13)
@@ -1896,6 +1908,7 @@ def log(x, base) -> float:
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###

View File

@@ -12,7 +12,7 @@ use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
const STDIN_BASE_OPTIONS: &[&str] = &["--no-cache", "--output-format", "concise"];
const STDIN_BASE_OPTIONS: &[&str] = &["check", "--no-cache", "--output-format", "concise"];
fn tempdir_filter(tempdir: &TempDir) -> String {
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
@@ -246,7 +246,6 @@ OTHER = "OTHER"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
// Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude
@@ -293,7 +292,6 @@ inline-quotes = "single"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
@@ -386,7 +384,6 @@ inline-quotes = "single"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
@@ -435,7 +432,6 @@ inline-quotes = "single"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
@@ -495,12 +491,12 @@ ignore = ["D203", "D212"]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(sub_dir)
.arg("check")
.args(STDIN_BASE_OPTIONS)
, @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: No Python files found under the given path(s)
@@ -527,7 +523,7 @@ fn nonexistent_config_file() {
option
It looks like you were trying to pass a path to a configuration file.
The path `foo.toml` does not exist
The path `foo.toml` does not point to a configuration file
For more information, try '--help'.
"###);
@@ -595,6 +591,24 @@ fn too_many_config_files() -> Result<()> {
Ok(())
}
#[test]
fn extend_passed_via_config_argument() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "extend = 'foo.toml'", "."]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'extend = 'foo.toml'' for '--config <CONFIG_OPTION>'
tip: Cannot include `extend` in a --config flag value
For more information, try '--help'.
"###);
}
#[test]
fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -820,6 +834,7 @@ fn complex_config_setting_overridden_via_cli() -> Result<()> {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -836,7 +851,7 @@ fn deprecated_config_option_overridden_via_cli() {
success: false
exit_code: 1
----- stdout -----
-:1:7: N801 Class name `lowercase` should use CapWords convention
-:1:7: N801 Class name `lowercase` should use CapWords convention
Found 1 error.
----- stderr -----
@@ -903,7 +918,6 @@ include = ["*.ipy"]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--extension", "ipy:ipynb"])
@@ -921,3 +935,236 @@ include = ["*.ipy"]
Ok(())
}
#[test]
fn file_noqa_external() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
external = ["AAA"]
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
# flake8: noqa: AAA101, BBB102
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:3:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
warning: Invalid rule code provided to `# ruff: noqa` at -:2: BBB102
"###);
});
Ok(())
}
#[test]
fn required_version_exact_mismatch() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
required-version = "0.1.0"
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Required version `==0.1.0` does not match the running version `[VERSION]`
"###);
});
Ok(())
}
#[test]
fn required_version_exact_match() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
format!(
r#"
required-version = "{version}"
"#
),
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}
#[test]
fn required_version_bound_mismatch() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
format!(
r#"
required-version = ">{version}"
"#
),
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
"###);
});
Ok(())
}
#[test]
fn required_version_bound_match() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
required-version = ">=0.1.0"
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}
/// Expand environment variables in `--config` paths provided via the CLI.
#[test]
fn config_expand() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
ruff_toml,
r#"
[lint]
select = ["F"]
ignore = ["F841"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg("${NAME}.toml")
.env("NAME", "ruff")
.arg("-")
.current_dir(tempdir.path())
.pass_stdin(r#"
import os
def func():
x = 1
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(())
}

View File

@@ -3,7 +3,7 @@ source: crates/ruff/tests/integration_test.rs
info:
program: ruff
args:
- "--explain"
- rule
- F401
---
success: true
@@ -34,6 +34,11 @@ marking it as unused, as in:
from module import member as member
```
## Fix safety
When `ignore_init_module_imports` is disabled, fixes can remove for unused imports in `__init__` files.
These fixes are considered unsafe because they can change the public interface.
## Example
```python
import numpy as np # unused import
@@ -68,4 +73,3 @@ else:
- [Typing documentation: interface conventions](https://typing.readthedocs.io/en/latest/source/libraries.html#library-interface-public-and-private-symbols)
----- stderr -----

View File

@@ -44,7 +44,6 @@ file_resolver.exclude = [
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
@@ -202,7 +201,7 @@ linter.allowed_confusables = []
linter.builtins = []
linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
linter.external = []
linter.ignore_init_module_imports = false
linter.ignore_init_module_imports = true
linter.logger_objects = []
linter.namespace_packages = []
linter.src = [
@@ -232,7 +231,7 @@ linter.flake8_bandit.check_typed_exception = false
linter.flake8_bugbear.extend_immutable_calls = []
linter.flake8_builtins.builtins_ignorelist = []
linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}(-\d{4})*
linter.flake8_copyright.author = none
linter.flake8_copyright.min_file_size = 0
linter.flake8_errmsg.max_string_length = 0
@@ -242,7 +241,22 @@ linter.flake8_gettext.functions_names = [
ngettext,
]
linter.flake8_implicit_str_concat.allow_multiline = true
linter.flake8_import_conventions.aliases = {"matplotlib": "mpl", "matplotlib.pyplot": "plt", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "networkx": "nx", "plotly.express": "px", "polars": "pl", "numpy": "np", "panel": "pn", "pyarrow": "pa", "altair": "alt", "tkinter": "tk", "holoviews": "hv"}
linter.flake8_import_conventions.aliases = {
altair = alt,
holoviews = hv,
matplotlib = mpl,
matplotlib.pyplot = plt,
networkx = nx,
numpy = np,
pandas = pd,
panel = pn,
plotly.express = px,
polars = pl,
pyarrow = pa,
seaborn = sns,
tensorflow = tf,
tkinter = tk,
}
linter.flake8_import_conventions.banned_aliases = {}
linter.flake8_import_conventions.banned_from = []
linter.flake8_pytest_style.fixture_parentheses = true
@@ -312,6 +326,7 @@ linter.isort.section_order = [
known { type = first_party },
known { type = local_folder },
]
linter.isort.default_section = known { type = third_party }
linter.isort.no_sections = false
linter.isort.from_first = false
linter.isort.length_sort = false
@@ -366,4 +381,3 @@ formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
----- stderr -----

View File

@@ -0,0 +1,105 @@
//! Tests for the --version command
use std::fs;
use std::process::Command;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
const VERSION_FILTER: [(&str, &str); 1] = [(
r"\d+\.\d+\.\d+(\+\d+)?( \(\w{9} \d\d\d\d-\d\d-\d\d\))?",
"[VERSION]",
)];
#[test]
fn version_basics() {
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("version"), @r###"
success: true
exit_code: 0
----- stdout -----
ruff [VERSION]
----- stderr -----
"###
);
});
}
/// `--config` is a global option,
/// so it's allowed to pass --config to subcommands such as `version`
/// -- the flag is simply ignored
#[test]
fn config_option_allowed_but_ignored() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
fs::File::create(&ruff_dot_toml)?;
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.arg("version")
.arg("--config")
.arg(&ruff_dot_toml)
.args(["--config", "lint.isort.extra-standard-library = ['foo', 'bar']"]), @r###"
success: true
exit_code: 0
----- stdout -----
ruff [VERSION]
----- stderr -----
"###
);
});
Ok(())
}
#[test]
fn config_option_ignored_but_validated() {
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.arg("version")
.args(["--config", "foo = bar"]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'foo = bar' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
The supplied argument is not valid TOML:
TOML parse error at line 1, column 7
|
1 | foo = bar
| ^
invalid string
expected `"`, `'`
For more information, try '--help'.
"###
);
});
}
/// `--isolated` is also a global option,
#[test]
fn isolated_option_allowed() {
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("version").arg("--isolated"), @r###"
success: true
exit_code: 0
----- stdout -----
ruff [VERSION]
----- stderr -----
"###
);
});
}

View File

@@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
use ruff::args::{ConfigArguments, FormatArguments, FormatCommand, LogLevelArgs};
use ruff::args::{ConfigArguments, FormatArguments, FormatCommand, GlobalConfigArgs, LogLevelArgs};
use ruff::resolve::resolve;
use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel;
@@ -43,7 +43,7 @@ fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, ConfigArgumen
.no_binary_name(true)
.get_matches_from(dirs);
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
let (cli, config_arguments) = arguments.partition()?;
let (cli, config_arguments) = arguments.partition(GlobalConfigArgs::default())?;
Ok((cli, config_arguments))
}
@@ -52,11 +52,7 @@ fn find_pyproject_config(
cli: &FormatArguments,
config_arguments: &ConfigArguments,
) -> anyhow::Result<PyprojectConfig> {
let mut pyproject_config = resolve(
cli.isolated,
config_arguments,
cli.stdin_filename.as_deref(),
)?;
let mut pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?;
// We don't want to format pyproject.toml
pyproject_config.settings.file_resolver.include = FilePatternSet::try_from_iter([
FilePattern::Builtin("*.py"),

View File

@@ -4,8 +4,8 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use ruff::check;
use ruff_linter::logging::{set_up_logging, LogLevel};
use ruff::{args::GlobalConfigArgs, check};
use ruff_linter::logging::set_up_logging;
use std::process::ExitCode;
mod format_dev;
@@ -28,6 +28,8 @@ const ROOT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../");
struct Args {
#[command(subcommand)]
command: Command,
#[clap(flatten)]
global_options: GlobalConfigArgs,
}
#[derive(Subcommand)]
@@ -57,8 +59,6 @@ enum Command {
Repeat {
#[clap(flatten)]
args: ruff::args::CheckCommand,
#[clap(flatten)]
log_level_args: ruff::args::LogLevelArgs,
/// Run this many times
#[clap(long)]
repeat: usize,
@@ -75,9 +75,12 @@ enum Command {
}
fn main() -> Result<ExitCode> {
let args = Args::parse();
let Args {
command,
global_options,
} = Args::parse();
#[allow(clippy::print_stdout)]
match args.command {
match command {
Command::GenerateAll(args) => generate_all::main(&args)?,
Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
@@ -89,14 +92,12 @@ fn main() -> Result<ExitCode> {
Command::PrintTokens(args) => print_tokens::main(&args)?,
Command::RoundTrip(args) => round_trip::main(&args)?,
Command::Repeat {
args,
args: subcommand_args,
repeat,
log_level_args,
} => {
let log_level = LogLevel::from(&log_level_args);
set_up_logging(&log_level)?;
set_up_logging(global_options.log_level())?;
for _ in 0..repeat {
check(args.clone(), log_level)?;
check(subcommand_args.clone(), global_options.clone())?;
}
}
Command::FormatDev(args) => {

View File

@@ -37,7 +37,7 @@ pub trait Buffer {
#[doc(hidden)]
fn elements(&self) -> &[FormatElement];
/// Glue for usage of the [`write!`] macro with implementors of this trait.
/// Glue for usage of the [`write!`] macro with implementers of this trait.
///
/// This method should generally not be invoked manually, but rather through the [`write!`] macro itself.
///

View File

@@ -545,6 +545,10 @@ impl PrintedRange {
&self.code
}
pub fn into_code(self) -> String {
self.code
}
/// The range the formatted code corresponds to in the source document.
pub fn source_range(&self) -> TextRange {
self.source_range

View File

@@ -78,27 +78,28 @@ impl<'a> PrintQueue<'a> {
impl<'a> Queue<'a> for PrintQueue<'a> {
fn pop(&mut self) -> Option<&'a FormatElement> {
let elements = self.element_slices.last_mut()?;
elements.next().or_else(|| {
self.element_slices.pop();
let elements = self.element_slices.last_mut()?;
elements.next()
})
elements.next().or_else(
#[cold]
|| {
self.element_slices.pop();
let elements = self.element_slices.last_mut()?;
elements.next()
},
)
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
let mut slices = self.element_slices.iter().rev();
let slice = slices.next()?;
match slice.as_slice().first() {
Some(element) => Some(element),
None => {
if let Some(next_elements) = slices.next() {
next_elements.as_slice().first()
} else {
None
}
}
}
slice.as_slice().first().or_else(
#[cold]
|| {
slices
.next()
.and_then(|next_elements| next_elements.as_slice().first())
},
)
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {
@@ -146,24 +147,30 @@ impl<'a, 'print> FitsQueue<'a, 'print> {
impl<'a, 'print> Queue<'a> for FitsQueue<'a, 'print> {
fn pop(&mut self) -> Option<&'a FormatElement> {
self.queue.pop().or_else(|| {
if let Some(next_slice) = self.rest_elements.next_back() {
self.queue.extend_back(next_slice.as_slice());
self.queue.pop()
} else {
None
}
})
self.queue.pop().or_else(
#[cold]
|| {
if let Some(next_slice) = self.rest_elements.next_back() {
self.queue.extend_back(next_slice.as_slice());
self.queue.pop()
} else {
None
}
},
)
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
self.queue.top_with_interned().or_else(|| {
if let Some(next_elements) = self.rest_elements.as_slice().last() {
next_elements.as_slice().first()
} else {
None
}
})
self.queue.top_with_interned().or_else(
#[cold]
|| {
if let Some(next_elements) = self.rest_elements.as_slice().last() {
next_elements.as_slice().first()
} else {
None
}
},
)
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.2.2"
version = "0.3.2"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -60,7 +60,6 @@ regex = { workspace = true }
result-like = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
similar = { workspace = true }

View File

@@ -28,3 +28,11 @@ dictionary = {
}
#import os # noqa
# case 1:
# try:
# try: # with comment
# try: print()
# except:
# except Foo:
# except Exception as e: print(e)

View File

@@ -18,3 +18,7 @@ func("0.0.0.0")
def my_func():
x = "0.0.0.0"
print(x)
# Implicit string concatenation
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0"

View File

@@ -18,6 +18,13 @@ with open("/dev/shm/unit/test", "w") as f:
with open("/foo/bar", "w") as f:
f.write("def")
# Implicit string concatenation
with open("/tmp/" "abc", "w") as f:
f.write("def")
with open("/tmp/abc" f"/tmp/abc", "w") as f:
f.write("def")
# Using `tempfile` module should be ok
import tempfile
from tempfile import TemporaryDirectory

View File

@@ -0,0 +1,22 @@
import os
import random
import a_lib
# OK
random.SystemRandom()
# Errors
random.Random()
random.random()
random.randrange()
random.randint()
random.choice()
random.choices()
random.uniform()
random.triangular()
random.randbytes()
# Unrelated
os.urandom()
a_lib.random()

View File

@@ -1,52 +1,47 @@
import crypt
import hashlib
from hashlib import new as hashlib_new
from hashlib import sha1 as hashlib_sha1
# Invalid
# Errors
hashlib.new('md5')
hashlib.new('md4', b'test')
hashlib.new(name='md5', data=b'test')
hashlib.new('MD4', data=b'test')
hashlib.new('sha1')
hashlib.new('sha1', data=b'test')
hashlib.new('sha', data=b'test')
hashlib.new(name='SHA', data=b'test')
hashlib.sha(data=b'test')
hashlib.md5()
hashlib_new('sha1')
hashlib_sha1('sha1')
# usedforsecurity arg only available in Python 3.9+
hashlib.new('sha1', usedforsecurity=True)
# Valid
crypt.crypt("test", salt=crypt.METHOD_CRYPT)
crypt.crypt("test", salt=crypt.METHOD_MD5)
crypt.crypt("test", salt=crypt.METHOD_BLOWFISH)
crypt.crypt("test", crypt.METHOD_BLOWFISH)
crypt.mksalt(crypt.METHOD_CRYPT)
crypt.mksalt(crypt.METHOD_MD5)
crypt.mksalt(crypt.METHOD_BLOWFISH)
# OK
hashlib.new('sha256')
hashlib.new('SHA512')
hashlib.sha256(data=b'test')
# usedforsecurity arg only available in Python 3.9+
hashlib_new(name='sha1', usedforsecurity=False)
# usedforsecurity arg only available in Python 3.9+
hashlib_sha1(name='sha1', usedforsecurity=False)
# usedforsecurity arg only available in Python 3.9+
hashlib.md4(usedforsecurity=False)
# usedforsecurity arg only available in Python 3.9+
hashlib.new(name='sha256', usedforsecurity=False)
crypt.crypt("test")
crypt.crypt("test", salt=crypt.METHOD_SHA256)
crypt.crypt("test", salt=crypt.METHOD_SHA512)
crypt.mksalt()
crypt.mksalt(crypt.METHOD_SHA256)
crypt.mksalt(crypt.METHOD_SHA512)

View File

@@ -1,4 +1,5 @@
import os
import subprocess
import commands
import popen2
@@ -16,6 +17,8 @@ popen2.Popen3("true")
popen2.Popen4("true")
commands.getoutput("true")
commands.getstatusoutput("true")
subprocess.getoutput("true")
subprocess.getstatusoutput("true")
# Check command argument looks unsafe.

View File

@@ -0,0 +1,34 @@
from django.contrib.auth.models import User
# Errors
User.objects.filter(username='admin').extra(dict(could_be='insecure'))
User.objects.filter(username='admin').extra(select=dict(could_be='insecure'))
User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'})
User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')})
User.objects.filter(username='admin').extra(where=['%secure' % 'nos'])
User.objects.filter(username='admin').extra(where=['{}secure'.format('no')])
query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --'
User.objects.filter(username='admin').extra(select={'test': query})
where_var = ['1=1) OR 1=1 AND (1=1']
User.objects.filter(username='admin').extra(where=where_var)
where_str = '1=1) OR 1=1 AND (1=1'
User.objects.filter(username='admin').extra(where=[where_str])
tables_var = ['django_content_type" WHERE "auth_user"."username"="admin']
User.objects.all().extra(tables=tables_var).distinct()
tables_str = 'django_content_type" WHERE "auth_user"."username"="admin'
User.objects.all().extra(tables=[tables_str]).distinct()
# OK
User.objects.filter(username='admin').extra(
select={'test': 'secure'},
where=['secure'],
tables=['secure']
)
User.objects.filter(username='admin').extra({'test': 'secure'})
User.objects.filter(username='admin').extra(select={'test': 'secure'})
User.objects.filter(username='admin').extra(where=['secure'])

View File

@@ -119,3 +119,16 @@ def func(x: bool):
settings(True)
from dataclasses import dataclass, InitVar
@dataclass
class Fit:
force: InitVar[bool] = False
def __post_init__(self, force: bool) -> None:
print(force)
Fit(force=True)

View File

@@ -14,9 +14,6 @@ reversed(sorted(x, reverse=not x))
reversed(sorted(i for i in range(42)))
reversed(sorted((i for i in range(42)), reverse=True))
def reversed(*args, **kwargs):
return None
reversed(sorted(x, reverse=True))
# Regression test for: https://github.com/astral-sh/ruff/issues/10335
reversed(sorted([1, 2, 3], reverse=False or True))
reversed(sorted([1, 2, 3], reverse=(False or True)))

View File

@@ -7,7 +7,19 @@ from pdb import set_trace as st
from celery.contrib.rdb import set_trace
from celery.contrib import rdb
import celery.contrib.rdb
from debugpy import wait_for_client
import debugpy
from ptvsd import break_into_debugger
from ptvsd import enable_attach
from ptvsd import wait_for_attach
import ptvsd
breakpoint()
st()
set_trace()
debugpy.breakpoint()
wait_for_client()
debugpy.listen(1234)
enable_attach()
break_into_debugger()
wait_for_attach()

View File

@@ -64,3 +64,5 @@ def not_warnings_dot_deprecated(
"Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
)
def not_a_deprecated_function() -> None: ...
fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053

View File

@@ -40,4 +40,7 @@ f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004
# Make sure we do not unescape quotes
this_is_fine = "This is an \\'escaped\\' quote"
this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash"
this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" # Q004
# Invalid escapes in bytestrings are also triggered:
x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004

View File

@@ -93,3 +93,15 @@ def func():
# OK
raise func()
# OK
future = executor.submit(float, "a")
if future.exception():
raise future.exception()
# RSE102
future = executor.submit(float, "a")
if future.exception():
raise future.Exception()

View File

@@ -10,7 +10,7 @@ async def func():
trio.sleep(0) # TRIO115
foo = 0
trio.sleep(foo) # TRIO115
trio.sleep(foo) # OK
trio.sleep(1) # OK
time.sleep(0) # OK
@@ -20,26 +20,26 @@ async def func():
trio.sleep(bar)
x, y = 0, 2000
trio.sleep(x) # TRIO115
trio.sleep(x) # OK
trio.sleep(y) # OK
(a, b, [c, (d, e)]) = (1, 2, (0, [4, 0]))
trio.sleep(c) # TRIO115
trio.sleep(c) # OK
trio.sleep(d) # OK
trio.sleep(e) # TRIO115
trio.sleep(e) # OK
m_x, m_y = 0
trio.sleep(m_y) # OK
trio.sleep(m_x) # OK
m_a = m_b = 0
trio.sleep(m_a) # TRIO115
trio.sleep(m_b) # TRIO115
trio.sleep(m_a) # OK
trio.sleep(m_b) # OK
m_c = (m_d, m_e) = (0, 0)
trio.sleep(m_c) # OK
trio.sleep(m_d) # TRIO115
trio.sleep(m_e) # TRIO115
trio.sleep(m_d) # OK
trio.sleep(m_e) # OK
def func():
@@ -63,4 +63,16 @@ def func():
import trio
if (walrus := 0) == 0:
trio.sleep(walrus) # TRIO115
trio.sleep(walrus) # OK
def func():
import trio
async def main() -> None:
sleep = 0
for _ in range(2):
await trio.sleep(sleep) # OK
sleep = 10
trio.run(main)

View File

@@ -0,0 +1,9 @@
from __future__ import annotations
import django.settings
from library import foo
import os
import pytz
import sys
from . import local

View File

@@ -0,0 +1,16 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object
class Thing(object):
name: str
def __init__(self, name: str):
self.name = name

View File

@@ -0,0 +1,9 @@
from __future__ import annotations
import os
import django.settings
from library import foo
import pytz
from . import local
import sys

View File

@@ -50,6 +50,29 @@ class MetaClass(ABCMeta):
def static_method(not_cls) -> bool:
return False
class ClsInArgsClass(ABCMeta):
def cls_as_argument(this, cls):
pass
def cls_as_pos_only_argument(this, cls, /):
pass
def cls_as_kw_only_argument(this, *, cls):
pass
def cls_as_varags(this, *cls):
pass
def cls_as_kwargs(this, **cls):
pass
class RenamingInMethodBodyClass(ABCMeta):
def bad_method(this):
this = this
this
def bad_method(this):
self = this
def func(x):
return x

View File

@@ -61,7 +61,7 @@ class PosOnlyClass:
def good_method_pos_only(self, blah, /, something: str):
pass
def bad_method_pos_only(this, blah, /, self, something: str):
def bad_method_pos_only(this, blah, /, something: str):
pass
@@ -93,3 +93,27 @@ class ModelClass:
@foobar.thisisstatic
def badstatic(foo):
pass
class SelfInArgsClass:
def self_as_argument(this, self):
pass
def self_as_pos_only_argument(this, self, /):
pass
def self_as_kw_only_argument(this, *, self):
pass
def self_as_varags(this, *self):
pass
def self_as_kwargs(this, **self):
pass
class RenamingInMethodBodyClass:
def bad_method(this):
this = this
this
def bad_method(this):
self = this

View File

@@ -0,0 +1,8 @@
# These rules test for intentional "odd" formatting. Using a formatter fixes that
[E{1,2,3}*.py]
generated_code = true
ij_formatter_enabled = false
[W*.py]
generated_code = true
ij_formatter_enabled = false

View File

@@ -147,3 +147,15 @@ ham[upper : ]
#: E203:1:10
ham[upper :]
#: Okay
ham[lower +1 :, "columnname"]
#: E203:1:13
ham[lower + 1 :, "columnname"]
#: Okay
f"{ham[lower +1 :, "columnname"]}"
#: E203:1:13
f"{ham[lower + 1 :, "columnname"]}"

View File

@@ -0,0 +1 @@
a = (1 or)

View File

@@ -466,6 +466,29 @@ class Class:
# end
# E301
class Class:
"""Class for minimal repo."""
columns = []
@classmethod
def cls_method(cls) -> None:
pass
# end
# E301
class Class:
"""Class for minimal repo."""
def method(cls) -> None:
pass
@classmethod
def cls_method(cls) -> None:
pass
# end
# E302
"""Main module."""
def fn():

View File

@@ -0,0 +1,50 @@
import json
from typing import Any, Sequence
class MissingCommand(TypeError): ...
class AnoherClass: ...
def a(): ...
@overload
def a(arg: int): ...
@overload
def a(arg: int, name: str): ...
def grouped1(): ...
def grouped2(): ...
def grouped3( ): ...
class BackendProxy:
backend_module: str
backend_object: str | None
backend: Any
def grouped1(): ...
def grouped2(): ...
def grouped3( ): ...
@decorated
def with_blank_line(): ...
def ungrouped(): ...
a = "test"
def function_def():
pass
b = "test"
def outer():
def inner():
pass
def inner2():
pass
class Foo: ...
class Bar: ...

View File

@@ -0,0 +1,4 @@
"""Test where the error is after the module's docstring."""
def fn():
pass

View File

@@ -0,0 +1,4 @@
"Test where the first line is a comment, " + "and the rule violation follows it."
def fn():
pass

View File

@@ -0,0 +1,5 @@
def fn1():
pass
def fn2():
pass

View File

@@ -0,0 +1,4 @@
print("Test where the first line is a statement, and the rule violation follows it.")
def fn():
pass

View File

@@ -0,0 +1,6 @@
# Test where the first line is a comment, and the rule violation follows it.
def fn():
pass

View File

@@ -0,0 +1,6 @@
"""Test where the error is after the module's docstring."""
def fn():
pass

View File

@@ -0,0 +1,6 @@
"Test where the first line is a comment, " + "and the rule violation follows it."
def fn():
pass

View File

@@ -0,0 +1,6 @@
print("Test where the first line is a statement, and the rule violation follows it.")
def fn():
pass

View File

@@ -0,0 +1,62 @@
import json
from typing import Any, Sequence
class MissingCommand(TypeError): ... # noqa: N818
class BackendProxy:
backend_module: str
backend_object: str | None
backend: Any
if __name__ == "__main__":
import abcd
abcd.foo()
def __init__(self, backend_module: str, backend_obj: str | None) -> None: ...
if TYPE_CHECKING:
import os
from typing_extensions import TypeAlias
abcd.foo()
def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any:
...
if TYPE_CHECKING:
from typing_extensions import TypeAlias
def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any:
...
def _exit(self) -> None: ...
def _optional_commands(self) -> dict[str, bool]: ...
def run(argv: Sequence[str]) -> int: ...
def read_line(fd: int = 0) -> bytearray: ...
def flush() -> None: ...
from typing import Any, Sequence
class MissingCommand(TypeError): ... # noqa: N818

View File

@@ -0,0 +1,62 @@
import json
from typing import Any, Sequence
class MissingCommand(TypeError): ... # noqa: N818
class BackendProxy:
backend_module: str
backend_object: str | None
backend: Any
if __name__ == "__main__":
import abcd
abcd.foo()
def __init__(self, backend_module: str, backend_obj: str | None) -> None: ...
if TYPE_CHECKING:
import os
from typing_extensions import TypeAlias
abcd.foo()
def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any:
...
if TYPE_CHECKING:
from typing_extensions import TypeAlias
def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any:
...
def _exit(self) -> None: ...
def _optional_commands(self) -> dict[str, bool]: ...
def run(argv: Sequence[str]) -> int: ...
def read_line(fd: int = 0) -> bytearray: ...
def flush() -> None: ...
from typing import Any, Sequence
class MissingCommand(TypeError): ... # noqa: N818

View File

@@ -0,0 +1,7 @@
import os
os.environ["WORLD_SIZE"] = "1"
os.putenv("CUDA_VISIBLE_DEVICES", "4")
del os.environ["WORLD_SIZE"]
import torch

View File

@@ -0,0 +1,88 @@
a = 2 + 2
a = (2 + 2)
a = 2 + \
3 \
+ 4
a = (3 -\
2 + \
7)
z = 5 + \
(3 -\
2 + \
7) + \
4
b = [2 +
2]
b = [
2 + 4 + 5 + \
44 \
- 5
]
c = (True and
False \
or False \
and True \
)
c = (True and
False)
d = True and \
False or \
False \
and not True
s = {
'x': 2 + \
2
}
s = {
'x': 2 +
2
}
x = {2 + 4 \
+ 3}
y = (
2 + 2 # \
+ 3 # \
+ 4 \
+ 3
)
x = """
(\\
)
"""
("""hello \
""")
("hello \
")
x = "abc" \
"xyz"
x = ("abc" \
"xyz")
def foo():
x = (a + \
2)

View File

@@ -14,3 +14,6 @@ class Chassis(RobotModuleTemplate):
" \
\
'''blank line with whitespace
inside a multiline string'''

View File

@@ -0,0 +1,14 @@
# Unix style
def foo() -> None:
pass
def bar() -> None:
pass
if __name__ == '__main__':
foo()
bar()

View File

@@ -0,0 +1,13 @@
# Unix style
def foo() -> None:
pass
def bar() -> None:
pass
if __name__ == '__main__':
foo()
bar()

View File

@@ -0,0 +1,17 @@
# Windows style
def foo() -> None:
pass
def bar() -> None:
pass
if __name__ == '__main__':
foo()
bar()

View File

@@ -0,0 +1,13 @@
# Windows style
def foo() -> None:
pass
def bar() -> None:
pass
if __name__ == '__main__':
foo()
bar()

View File

@@ -0,0 +1,5 @@
# This is fine
def foo():
pass
# Some comment

View File

@@ -10,7 +10,7 @@ def f1():
# Here's a standalone comment that's over the limit.
x = 2
# Another standalone that is preceded by a newline and indent toke and is over the limit.
# Another standalone that is preceded by a newline and indent token and is over the limit.
print("Here's a string that's over the limit, but it's not a docstring.")

View File

@@ -10,7 +10,7 @@ def f1():
# Here's a standalone comment that's over theß9💣2.
x = 2
# Another standalone that is preceded by a newline and indent toke and is over theß9💣2.
# Another standalone that is preceded by a newline and indent token and is over theß9💣2.
print("Here's a string that's over theß9💣2, but it's not a ß9💣2ing.")

View File

@@ -57,3 +57,15 @@ def func():
Returns:
the value"""
def func():
"""Do something.
Args:
x: the value
with a hanging indent
Returns:
the value
"""

View File

@@ -0,0 +1,7 @@
"""Test: ensure that we treat strings in `typing.Annotation` as type definitions."""
from pathlib import Path
from re import RegexFlag
from typing import Annotated
p: Annotated["Path", int] = 1

View File

@@ -0,0 +1,6 @@
"""Regression test for: https://github.com/astral-sh/ruff/issues/10384"""
import datetime
from datetime import datetime
datetime(1, 2, 3)

View File

@@ -0,0 +1,16 @@
"""Test case: strings used within calls within type annotations."""
from typing import Callable
import bpy
from mypy_extensions import VarArg
class LightShow(bpy.types.Operator):
label = "Create Character"
name = "lightshow.letter_creation"
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # OK
def f(x: Callable[[VarArg("os")], None]): # F821
pass

View File

@@ -0,0 +1,44 @@
"""Tests for constructs allowed in `.pyi` stub files but not at runtime"""
from typing import Optional, TypeAlias, Union
__version__: str
__author__: str
# Forward references:
MaybeCStr: TypeAlias = Optional[CStr] # valid in a `.pyi` stub file, not in a `.py` runtime file
MaybeCStr2: TypeAlias = Optional["CStr"] # always okay
CStr: TypeAlias = Union[C, str] # valid in a `.pyi` stub file, not in a `.py` runtime file
CStr2: TypeAlias = Union["C", str] # always okay
# References to a class from inside the class:
class C:
other: C = ... # valid in a `.pyi` stub file, not in a `.py` runtime file
other2: "C" = ... # always okay
def from_str(self, s: str) -> C: ... # valid in a `.pyi` stub file, not in a `.py` runtime file
def from_str2(self, s: str) -> "C": ... # always okay
# Circular references:
class A:
foo: B # valid in a `.pyi` stub file, not in a `.py` runtime file
foo2: "B" # always okay
bar: dict[str, B] # valid in a `.pyi` stub file, not in a `.py` runtime file
bar2: dict[str, "A"] # always okay
class B:
foo: A # always okay
bar: dict[str, A] # always okay
class Leaf: ...
class Tree(list[Tree | Leaf]): ... # valid in a `.pyi` stub file, not in a `.py` runtime file
class Tree2(list["Tree | Leaf"]): ... # always okay
# Annotations are treated as assignments in .pyi files, but not in .py files
class MyClass:
foo: int
bar = foo # valid in a `.pyi` stub file, not in a `.py` runtime file
bar = "foo" # always okay
baz: MyClass
eggs = baz # valid in a `.pyi` stub file, not in a `.py` runtime file
eggs = "baz" # always okay

View File

@@ -0,0 +1,44 @@
"""Tests for constructs allowed in `.pyi` stub files but not at runtime"""
from typing import Optional, TypeAlias, Union
__version__: str
__author__: str
# Forward references:
MaybeCStr: TypeAlias = Optional[CStr] # valid in a `.pyi` stub file, not in a `.py` runtime file
MaybeCStr2: TypeAlias = Optional["CStr"] # always okay
CStr: TypeAlias = Union[C, str] # valid in a `.pyi` stub file, not in a `.py` runtime file
CStr2: TypeAlias = Union["C", str] # always okay
# References to a class from inside the class:
class C:
other: C = ... # valid in a `.pyi` stub file, not in a `.py` runtime file
other2: "C" = ... # always okay
def from_str(self, s: str) -> C: ... # valid in a `.pyi` stub file, not in a `.py` runtime file
def from_str2(self, s: str) -> "C": ... # always okay
# Circular references:
class A:
foo: B # valid in a `.pyi` stub file, not in a `.py` runtime file
foo2: "B" # always okay
bar: dict[str, B] # valid in a `.pyi` stub file, not in a `.py` runtime file
bar2: dict[str, "A"] # always okay
class B:
foo: A # always okay
bar: dict[str, A] # always okay
class Leaf: ...
class Tree(list[Tree | Leaf]): ... # valid in a `.pyi` stub file, not in a `.py` runtime file
class Tree2(list["Tree | Leaf"]): ... # always okay
# Annotations are treated as assignments in .pyi files, but not in .py files
class MyClass:
foo: int
bar = foo # valid in a `.pyi` stub file, not in a `.py` runtime file
bar = "foo" # always okay
baz: MyClass
eggs = baz # valid in a `.pyi` stub file, not in a `.py` runtime file
eggs = "baz" # always okay

View File

@@ -0,0 +1,48 @@
"""Tests for constructs allowed when `__future__` annotations are enabled but not otherwise"""
from __future__ import annotations
from typing import Optional, TypeAlias, Union
__version__: str
__author__: str
# References to a class from inside the class:
class C:
other: C = ... # valid when `__future__.annotations are enabled
other2: "C" = ... # always okay
def from_str(self, s: str) -> C: ... # valid when `__future__.annotations are enabled
def from_str2(self, s: str) -> "C": ... # always okay
# Circular references:
class A:
foo: B # valid when `__future__.annotations are enabled
foo2: "B" # always okay
bar: dict[str, B] # valid when `__future__.annotations are enabled
bar2: dict[str, "A"] # always okay
class B:
foo: A # always okay
bar: dict[str, A] # always okay
# Annotations are treated as assignments in .pyi files, but not in .py files
class MyClass:
foo: int
bar = foo # Still invalid even when `__future__.annotations` are enabled
bar = "foo" # always okay
baz: MyClass
eggs = baz # Still invalid even when `__future__.annotations` are enabled
eggs = "baz" # always okay
# Forward references:
MaybeDStr: TypeAlias = Optional[DStr] # Still invalid even when `__future__.annotations` are enabled
MaybeDStr2: TypeAlias = Optional["DStr"] # always okay
DStr: TypeAlias = Union[D, str] # Still invalid even when `__future__.annotations` are enabled
DStr2: TypeAlias = Union["D", str] # always okay
class D: ...
# More circular references
class Leaf: ...
class Tree(list[Tree | Leaf]): ... # Still invalid even when `__future__.annotations` are enabled
class Tree2(list["Tree | Leaf"]): ... # always okay

View File

@@ -0,0 +1,10 @@
"""Test: inner class annotation."""
class RandomClass:
def bad_func(self) -> InnerClass: ... # F821
def good_func(self) -> OuterClass.InnerClass: ... # Okay
class OuterClass:
class InnerClass: ...
def good_func(self) -> InnerClass: ... # Okay

View File

@@ -0,0 +1,4 @@
a = 1
b: int # Considered a binding in a `.pyi` stub file, not in a `.py` runtime file
__all__ = ["a", "b", "c"] # c is flagged as missing; b is not

View File

@@ -0,0 +1,32 @@
from typing import Any
d = {1: 1, 2: 2}
d_tuple = {(1, 2): 3, (4, 5): 6}
d_tuple_annotated: Any = {(1, 2): 3, (4, 5): 6}
d_tuple_incorrect_tuple = {(1,): 3, (4, 5): 6}
l = [1, 2]
s1 = {1, 2}
s2 = {1, 2, 3}
# Errors
for k, v in d:
pass
for k, v in d_tuple_incorrect_tuple:
pass
# Non errors
for k, v in d.items():
pass
for k in d.keys():
pass
for i, v in enumerate(l):
pass
for i, v in s1.intersection(s2):
pass
for a, b in d_tuple:
pass
for a, b in d_tuple_annotated:
pass

View File

@@ -0,0 +1,37 @@
# These testcases should raise errors
class Float:
def __bool__(self):
return 3.05 # [invalid-bool-return]
class Int:
def __bool__(self):
return 0 # [invalid-bool-return]
class Str:
def __bool__(self):
x = "ruff"
return x # [invalid-bool-return]
# TODO: Once Ruff has better type checking
def return_int():
return 3
class ComplexReturn:
def __bool__(self):
return return_int() # [invalid-bool-return]
# These testcases should NOT raise errors
class Bool:
def __bool__(self):
return True
class Bool2:
def __bool__(self):
x = True
return x

View File

@@ -1,28 +1,36 @@
class Str:
def __str__(self):
return 1
# These testcases should raise errors
class Float:
def __str__(self):
return 3.05
class Int:
def __str__(self):
return 1
class Int2:
def __str__(self):
return 0
class Bool:
def __str__(self):
return False
class Str2:
def __str__(self):
x = "ruff"
return x
# TODO fixme once Ruff has better type checking
# TODO: Once Ruff has better type checking
def return_int():
return 3
class ComplexReturn:
def __str__(self):
return return_int()
return return_int()
# These testcases should NOT raise errors
class Str:
def __str__(self):
return "ruff"
class Str2:
def __str__(self):
x = "ruff"
return x

View File

@@ -17,3 +17,14 @@ class Fruit:
return choice(Fruit.COLORS)
pick_one_color = staticmethod(pick_one_color)
class Class:
def class_method(cls):
pass
class_method = classmethod(class_method);another_statement
def static_method():
pass
static_method = staticmethod(static_method);

View File

@@ -54,3 +54,15 @@ class StudentE(StudentD):
def setup(self):
pass
class StudentF(object):
__slots__ = ("name", "__dict__")
def __init__(self, name, middle_name):
self.name = name
self.middle_name = middle_name # [assigning-non-slot]
self.setup()
def setup(self):
pass

View File

@@ -51,3 +51,7 @@ foo == foo or foo == bar # Self-comparison.
foo[0] == "a" or foo[0] == "b" # Subscripts.
foo() == "a" or foo() == "b" # Calls.
import sys
sys.platform == "win32" or sys.platform == "emscripten" # sys attributes

View File

@@ -0,0 +1,39 @@
from functools import singledispatch, singledispatchmethod
@singledispatch
def convert_position(position):
pass
class Board:
@singledispatch # [singledispatch-method]
@classmethod
def convert_position(cls, position):
pass
@singledispatch # [singledispatch-method]
def move(self, position):
pass
@singledispatchmethod
def place(self, position):
pass
@singledispatch
@staticmethod
def do(position):
pass
# False negative (flagged by Pylint).
@convert_position.register
@classmethod
def _(cls, position: str) -> tuple:
position_a, position_b = position.split(",")
return (int(position_a), int(position_b))
# False negative (flagged by Pylint).
@convert_position.register
@classmethod
def _(cls, position: tuple) -> str:
return f"{position[0]},{position[1]}"

View File

@@ -1,6 +1,6 @@
from typing import Any
a = 2
print((3.0).__add__(4.0)) # PLC2801
print((3.0).__sub__(4.0)) # PLC2801
print((3.0).__mul__(4.0)) # PLC2801
@@ -17,6 +17,67 @@ print((3.0).__str__()) # PLC2801
print((3.0).__repr__()) # PLC2801
print([1, 2, 3].__len__()) # PLC2801
print((1).__neg__()) # PLC2801
print(-a.__sub__(1)) # PLC2801
print(-(a).__sub__(1)) # PLC2801
print(-(-a.__sub__(1))) # PLC2801
print((5 - a).__sub__(1)) # PLC2801
print(-(5 - a).__sub__(1)) # PLC2801
print(-(-5 - a).__sub__(1)) # PLC2801
print(+-+-+-a.__sub__(1)) # PLC2801
print(a.__rsub__(2 - 1)) # PLC2801
print(a.__sub__(((((1)))))) # PLC2801
print(a.__sub__(((((2 - 1)))))) # PLC2801
print(a.__sub__(
3
+
4
))
print(a.__rsub__(
3
+
4
))
print(2 * a.__add__(3)) # PLC2801
x = 2 * a.__add__(3) # PLC2801
x = 2 * -a.__add__(3) # PLC2801
x = a.__add__(3) # PLC2801
x = -a.__add__(3) # PLC2801
x = (-a).__add__(3) # PLC2801
x = -(-a).__add__(3) # PLC2801
# Calls
print(a.__call__()) # PLC2801 (no fix, intentional)
# Lambda expressions
blah = lambda: a.__add__(1) # PLC2801
# If expressions
print(a.__add__(1) if a > 0 else a.__sub__(1)) # PLC2801
# Dict/Set/List/Tuple
print({"a": a.__add__(1)}) # PLC2801
print({a.__add__(1)}) # PLC2801
print([a.__add__(1)]) # PLC2801
print((a.__add__(1),)) # PLC2801
# Comprehension variants
print({i: i.__add__(1) for i in range(5)}) # PLC2801
print({i.__add__(1) for i in range(5)}) # PLC2801
print([i.__add__(1) for i in range(5)]) # PLC2801
print((i.__add__(1) for i in range(5))) # PLC2801
# Generators
gen = (i.__add__(1) for i in range(5)) # PLC2801
print(next(gen))
# Subscripts
print({"a": a.__add__(1)}["a"]) # PLC2801
# Starred
print(*[a.__add__(1)]) # PLC2801
# Slices
print([a.__add__(1), a.__sub__(1)][0:1]) # PLC2801
class Thing:

View File

@@ -0,0 +1,121 @@
from abc import ABC, abstractmethod
from contextlib import suppress
# Test case 1: Useless exception statement
def func():
AssertionError("This is an assertion error") # PLW0133
# Test case 2: Useless exception statement in try-except block
def func():
try:
Exception("This is an exception") # PLW0133
except Exception as err:
pass
# Test case 3: Useless exception statement in if statement
def func():
if True:
RuntimeError("This is an exception") # PLW0133
# Test case 4: Useless exception statement in class
def func():
class Class:
def __init__(self):
TypeError("This is an exception") # PLW0133
# Test case 5: Useless exception statement in function
def func():
def inner():
IndexError("This is an exception") # PLW0133
inner()
# Test case 6: Useless exception statement in while loop
def func():
while True:
KeyError("This is an exception") # PLW0133
# Test case 7: Useless exception statement in abstract class
def func():
class Class(ABC):
@abstractmethod
def method(self):
NotImplementedError("This is an exception") # PLW0133
# Test case 8: Useless exception statement inside context manager
def func():
with suppress(AttributeError):
AttributeError("This is an exception") # PLW0133
# Test case 9: Useless exception statement in parentheses
def func():
(RuntimeError("This is an exception")) # PLW0133
# Test case 10: Useless exception statement in continuation
def func():
x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
# Test case 11: Useless warning statement
def func():
UserWarning("This is an assertion error") # PLW0133
# Non-violation test cases: PLW0133
# Test case 1: Used exception statement in try-except block
def func():
raise Exception("This is an exception") # OK
# Test case 2: Used exception statement in if statement
def func():
if True:
raise ValueError("This is an exception") # OK
# Test case 3: Used exception statement in class
def func():
class Class:
def __init__(self):
raise TypeError("This is an exception") # OK
# Test case 4: Exception statement used in list comprehension
def func():
[ValueError("This is an exception") for i in range(10)] # OK
# Test case 5: Exception statement used when initializing a dictionary
def func():
{i: TypeError("This is an exception") for i in range(10)} # OK
# Test case 6: Exception statement used in function
def func():
def inner():
raise IndexError("This is an exception") # OK
inner()
# Test case 7: Exception statement used in variable assignment
def func():
err = KeyError("This is an exception") # OK
# Test case 8: Exception statement inside context manager
def func():
with suppress(AttributeError):
raise AttributeError("This is an exception") # OK

View File

@@ -33,7 +33,7 @@ bool(b"")
bool(1.0)
int().denominator
# These become string or byte literals
# These become literals
str()
str("foo")
str("""
@@ -53,3 +53,9 @@ bool(False)
# These become a literal but retain parentheses
int(1).denominator
# These too are literals in spirit
int(+1)
int(-1)
float(+1.0)
float(-1.0)

View File

@@ -252,3 +252,10 @@ raise ValueError(
# The dictionary should be parenthesized.
"{}".format({0: 1}())
# The string shouldn't be converted, since it would require repeating the function call.
"{x} {x}".format(x=foo())
"{0} {0}".format(foo())
# The string _should_ be converted, since the function call is repeated in the arguments.
"{0} {1}".format(foo(), foo())

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