770 Commits

Author SHA1 Message Date
Erik Johnson ea6be89b0a Increase max value of line-length setting (#24962)
Co-authored-by: Erik Johnson <source@ekriirke.net>
Co-authored-by: Micha Reiser <micha@reiser.io>
2026-05-05 14:08:51 +00:00
Dev-iL 94e61100ef [airflow] Implement task-branch-as-short-circuit (AIR004) (#23579)
## Summary

Adds a new rule `AIR004` that detects `@task.branch` decorated functions
that could be replaced with `@task.short_circuit`.

In Airflow,
[`@task.branch`](https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html#branching)
selects which downstream tasks to run by returning a list of task IDs
(or an empty list to skip all). When the function has at least two
`return` statements and exactly one of them returns a non-empty list, it
is effectively acting as a boolean short-circuit (i.e. either run one
specific set of downstream tasks or skip them all). In that case,
[`@task.short_circuit`](https://www.astronomer.io/docs/learn/airflow-branch-operator#taskshort_circuit-shortcircuitoperator)
is a simpler and more readable alternative that returns `True`/`False`
instead.

```python
# Before (AIR004)
@task.branch
def my_task():
    if condition:
        return ["my_downstream_task"]
    return []

# After
@task.short_circuit
def my_task():
    return condition
```

### Implementation details

- Resolves the `@task.branch` decorator via the semantic model
(`airflow.decorators.task` + `.branch` attribute), handling both
`@task.branch` and `@task.branch()` call forms via `map_callable`.
- Uses `ReturnStatementVisitor` to collect all `return` statements
recursively (including those inside nested `if`/`else`/`for`/`while`
blocks).
- Flags the function when: `len(returns) >= 2` and exactly one return
has a non-empty list value.

### What it does NOT flag

- Functions with multiple non-empty list returns (genuine branching
logic).
- Functions with all-empty returns (no downstream tasks selected at
all).
- Functions with only a single return statement.
- Functions not decorated with `@task.branch`.
- Functions returning non-list values (strings, `None`, etc.).

## Test Plan
<!-- How was it tested? -->

Added snapshot tests in `AIR004.py` covering both violation and
non-violation cases:
- two returns with one non-empty list
- three returns with one non-empty list
- nested returns
- multiple non-empty returns
- all-empty returns
- single return
- undecorated functions
- `@task.short_circuit` decorated functions
2026-04-23 10:21:30 -04:00
Dev-iL 0921ed25a1 [airflow] Implement airflow-xcom-pull-in-template-string (AIR201) (#23583)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2026-04-14 08:47:58 +02:00
Matthew Lloyd 6468a35e9b Add nested-string-quote-style formatting option (#24312) 2026-03-31 18:48:24 +02:00
Daniil Sivak 3a44cce48e Implement unnecessary-if (RUF050) (#24114) 2026-03-25 17:57:43 +01:00
Renzo 32f644c3bf Add RUF072: warn when using operator on an f-string (#24162)
## Summary

Fixes #24159

Adds a new rule RUF072 (`fstring-percent-format`) that flags any use of
the `%` operator on an f-string.

Mixing f-string interpolation with `%`-formatting is almost certainly a
mistake since both serve the same purpose. There's no valid use case for
using `%` on an f-string.

```python
# Flagged
f"{name}" % name
f"hello %s %s" % (1, 2)
f"value: {x}" % {"key": "value"}

# OK — plain string literals are handled by existing F50x rules
"hello %s" % name
"%s %s" % (1, 2)
```

## Test Plan

- Added test cases for f-strings with `%` on various RHS types
(variables, tuples, dicts, literals)
- Added test cases for plain string literals (not flagged — handled by
F50x)
- Verified existing ruff and pyflakes tests still pass

```shell
cargo test -p ruff_linter -- fstring_percent_format
```

Test file:
`crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF072_RUF072.py.snap`

Test cases cover:
- F-string with `%` and variable RHS: `f"{banana}" % banana`
- F-string with `%` and tuple RHS: `f"hello %s %s" % (1, 2)`
- F-string with `%` and dict RHS: `f"value: {x}" % {"key": "value"}`
- F-string with `%` and literal RHS: `f"{x}" % 42`
- Plain string literals not flagged (handled by existing F50x rules)
2026-03-25 14:09:31 +01:00
Daniil Sivak ceddca7123 Implement useless-finally (RUF-072) (#24165)
Closes #19158

Implements the `useless_finally` rule (`RUF072`), which detects useless
`finally` blocks that only contain `pass` or `...`.

It handles two cases:
- `try/except/finally: pass` - the `finally` clause is removed, leaving
a valid `try/except`
- bare `try/finally: pass`: the entire `try/finally` is unwrapped, the
try body is dedented to replace the whole statement

Fix is skipped when comments are present in or around the `finally`
block.

It complements with existing rules like `RUF047` (`needless-else`) and
`SIM105` (`suppressible-exception`). It case of `SIM105` it also
unblocks this rule, as currently `SIM105` got skipped if `finally` has
any body at all (even just `pass`).

## Test Plan

- `RUF072.py` - main rule test with error cases and non-error.
- `useless_finally_and_needless_else` - test function, which checks how
`RUF047` and `RUF072` work together on the same `try` statement.
- `useless_finally_and_suppressible_exception` - test function, which
checks how `RUF072` and `SIM105` work together.
2026-03-25 13:06:53 +01:00
Matt Van Horn a2472ff3a1 Clarify extend-ignore and extend-select settings documentation (#24064)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-03-20 08:55:31 +00:00
Dev-iL 91cc7cd0d3 [airflow] Flag Variable.get() calls outside of task execution context (AIR003) (#23584)
## Summary

Implements a new rule `AIR003` (`airflow-variable-get-outside-task`)
that flags `Variable.get()` calls outside of task execution context.

Per the [Airflow best practices
documentation](https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html#airflow-variables),
calling `Variable.get()` at module level or inside operator constructor
arguments causes a database query **every time the DAG file is parsed**
by the scheduler. This can degrade parsing performance and even cause
DAG file timeouts. The recommended alternative is to pass variables via
Jinja templates (`{{ var.value.my_var }}`), which defer the lookup until
task execution.

### What the rule flags

```python
from airflow.sdk import Variable
from airflow.operators.bash import BashOperator

# Top-level Variable.get() — runs on every DAG parse
foo = Variable.get("foo")

# Variable.get() in operator constructor args — also runs on every DAG parse
BashOperator(
    task_id="bad",
    bash_command="echo $FOO",
    env={"FOO": Variable.get("foo")},
)

# Variable.get() in a regular helper function — not a task execution context
def helper():
    return Variable.get("foo")
```

### What it allows

`Variable.get()` is fine inside code that only runs during task
execution:

- `@task`-decorated functions (including `@task()`, `@task.branch`,
`@task.short_circuit`, etc.)
- `execute()` methods on `BaseOperator` subclasses

### Implementation details

- Resolves `Variable.get` via the semantic model, matching both
`airflow.models.Variable` and `airflow.sdk.Variable` import paths.
- Determines "task execution context" by walking the statement hierarchy
(`current_statements()`) looking for a parent `FunctionDef` that is
either:
- decorated with `@task` / `@task.<variant>` (resolved via
`airflow.decorators.task`), or
  - named `execute` inside a class inheriting from `BaseOperator`.
- Handles both `@task` and `@task()` forms via `map_callable`, and
attribute-style decorators like `@task.branch` via `Expr::Attribute`
resolution.

## Test Plan

Added snapshot tests in `AIR005.py` covering:
- **Violations**: `Variable.get()` at module level, inside operator
constructor keyword arguments, inside f-string interpolation in operator
args, and in a regular helper function. Tested both
`airflow.models.Variable` and `airflow.sdk.Variable` import paths.
- **Non-violations**: `Variable.get()` inside `@task`, `@task()`,
`@task.branch` decorated functions, inside a `BaseOperator.execute()`
method, and Jinja template usage (no `Variable.get()` call).

related: https://github.com/apache/airflow/issues/43176
2026-03-11 13:45:59 -04:00
Charlie Marsh e46e458eae Allow users to ban lazy imports (#23847)
## Summary

This PR renames TID254 to `lazy-import-mismatch` and expands it into a
single rule with two complementary settings: `require-lazy` and
`ban-lazy`. Both settings accept `"all"`, `list[str]`, or `{ include =
..., exclude = [...] }`, and we validate that the two selectors don’t
overlap.

This covers the two main use-cases users asked for:

- Require lazy imports by default, except for known side-effectful
modules. Example:

  ```toml
  require-lazy = { include = "all", exclude = ["sitecustomize"] }
  ```
This enforces lazy imports everywhere Ruff can rewrite them, while
leaving sitecustomize eager.

- Forbid lazy imports for modules that must stay eager, or even forbid
lazy imports by default with a small allowlist.

  ```toml
  # Require `sitecustomize` to stay eager.
  ban-lazy = ["sitecustomize"]

  # Ban lazy imports everywhere except `typing`.
  ban-lazy = { include = "all", exclude = ["typing"] }
  ```
2026-03-10 14:27:53 -04:00
Andrew Barnes be5db3c110 [flake8-bugbear] Implement delattr-with-constant (B043) (#23737)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Amethyst Reese <amethyst@n7.gg>
2026-03-09 19:54:14 +00:00
Dev-iL b82853fb12 [airflow] Flag runtime-varying values in DAG/task constructor arguments (AIR304) (#23631)
## Summary

Add rule AIR304 that flags runtime-varying function calls (e.g.,
`datetime.now()`, `pendulum.now()`, `random.randint()`, `uuid.uuid4()`)
used as arguments in Airflow DAG/task constructors. These calls cause
the serialized DAG hash to change on every parse, creating infinite DAG
versions in the `dag_version` and `serialized_dag` tables, leading to
unbounded database growth and eventual OOM conditions.

Reference: https://github.com/apache/airflow/pull/59430

The rule checks:
- `DAG(...)` constructors and `@dag(...)` decorator calls
- Operator and sensor constructors (via
`is_airflow_builtin_or_provider`)
- `@task(...)` decorator calls

It recursively inspects keyword argument values through binary/unary
ops, dicts, lists, sets, tuples, and f-strings to find calls to known
runtime-varying functions from `datetime`, `pendulum`, `time`, `uuid`,
and `random`.

## Test Plan

`cargo test -p ruff_linter -- airflow::tests` — all 43 tests pass,
including the new AIR304 test case with 17 violation and 6 non-violation
scenarios covering direct calls, binary ops, `default_args` dicts,
f-strings, operators, sensors, decorators, and non-airflow calls.

---
Related: https://github.com/apache/airflow/issues/43176
CC: @Lee-W @sjyangkevin @wjddn279

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:59:59 -04:00
Charlie Marsh 2e227cdd5b Add a rule to enforce lazy imports (#23777)
## Summary

`TID254` enforces the use of `lazy` imports. You can specify a set of
modules, similar to `banned-module-level-imports`, or `"all"`:

```toml
# Require every module-level import to be lazy.
banned-eager-imports = "all"

# Require lazy imports for specific modules.
banned-eager-imports = [
    "boto3",
    "botocore",
]
```
2026-03-09 17:57:37 +00:00
Anish Giri e4dfddcd17 [ruff] Add os-path-commonprefix (RUF071) (#23814)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Adds a new rule os-path-commonprefix (RUF071) that detects calls to
os.path.commonprefix(), which performs character-by-character string
comparison instead of path-component comparison — a well-known footgun.
os.path.commonpath() is the correct alternative.
               
## Test Plan

cargo nextest run -p ruff_linter -- rule_ospathcommonprefix

Closes #22981

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2026-03-09 17:29:19 +00:00
Karthik 85dac631b4 [pydocstyle] Add rule D420 to enforce docstring section ordering (#23537)
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2026-02-25 11:05:50 -08:00
Amethyst Reese 32a59c234d Include configured extensions in file discovery (#23400) 2026-02-23 15:39:49 -08:00
Anish Giri beda157528 [ruff] Add unnecessary-assign-before-yield (RUF070) (#23300)
## Summary          

Closes #13141
  
Adds a new rule `unnecessary-assign-before-yield` (`RUF070`) that
detects variable assignments immediately followed by a `yield` (or
`yield from`) of that variable, where the variable is not referenced
anywhere else. This is the `yield` equivalent of `RET504`
(`unnecessary-assign`).

  ```python
  # Before
  def gen():
      x = 1
      yield x

  # After
  def gen():
      yield 1
```

 Unlike return, yield does not exit the function, so the rule only triggers when the binding has exactly one reference (the yielditself). The fix is marked as unsafe for the same reason.

## Test Plan

cargo nextest run -p ruff_linter -- RUF070

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2026-02-20 14:20:19 +00:00
Bnyro 333ad91ba3 [pylint] Implement swap-with-temporary-variable (PLR1712) (#22205)
## Summary
This PR implements the `consider-swap-variables` rule from pylint.
Basically it tries to find code parts that swap two variables with each
other using a temporary variable.

Example code:
```py
temp = x
x = y
y = temp
```

can be simplified to

```py
x, y = y, x
```

related:
-
https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/consider-swap-variables.html
- #970 

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

## Test Plan
I've added new snapshots tests.

PS: Since this is my first contribution here and I'm not too familiar
with the codebase, suggestions are very welcome! The implementation
might also not be 100% memory-optimized yet, since we use `clone` a few
times.
2026-02-19 18:32:01 -05:00
Amethyst Reese d1b544393a Add extension mapping to configuration file options (#23384)
New `extension` configuration option takes a dictionary mapping custom file extensions (keys) to languages by name (values). Eg,

```toml
[tool.ruff]
extension = {qmd="markdown"}
```

Issue #23204
2026-02-19 12:21:11 -08:00
Alexander Ley d056a9fa6d [isort] support for configurable import section heading comments (#23151)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

fixes #6371

- `settings.rs` - Added import_headings: FxHashMap<ImportSection,
String> field to Settings struct with default and display support
- `options.rs` - Added import_heading configuration option with
#[option] metadata, documentation, and TOML table syntax. Added
validation for unknown sections and mapping to Settings
- `mod.rs` - Core logic in format_import_block:
  - Collects all configured heading values as "# {heading}" strings
- Strips matching heading comments from ALL imports in each section
(handles reordering)
  - Inserts heading comments above each section after blank line logic
- `organize_imports.rs` - Extended fix range backward to include heading
comment lines above the first import, preventing duplication on fix
application

## Test Plan

Test Coverage (9 new tests, 156 total passing)

```
cargo test -p ruff_linter -- isort::tests
cargo test -p ruff_linter -- isort::tests::import_heading
```

| Test | Scenario |

|-------------------------------------------|------------------------------------------------------|
| import_heading.py | Basic unsorted imports get headings added |
| import_heading_already_present.py | Existing headings stripped and
re-added correctly |
| import_heading_already_correct.py | Properly sorted+headed imports
produce NO diagnostic |
| import_heading_unsorted.py | Completely unsorted imports get sorted
with headings |
| import_heading_with_no_lines_before.py | Interaction with
no_lines_before setting |
| import_heading_partial.py | Headings configured for only some sections
|
| import_heading_wrong_heading.py | Non-matching comments preserved as
regular comments |
| import_heading_single_section.py | Only one section present gets its
heading |
| import_heading_force_sort_within_sections.py | Works with
force_sort_within_sections |

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Amethyst Reese <amethyst@n7.gg>
2026-02-12 11:51:06 -08:00
Avasam d9439a1d17 Fixed import in runtime-evaluated-decorators example (#23187) 2026-02-09 22:29:50 +00:00
chiri d70f33c210 [ruff] New rule float-comparison (RUF069) (#20585)
## Summary

Part of https://github.com/astral-sh/ruff/issues/14220

## Test Plan

`cargo nextest run ruf069`

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2026-02-05 14:48:07 -05:00
Kevin Yang f055f39345 [airflow] Add ruff rules to catch deprecated Airflow imports for Airflow 3.1 (AIR321) (#22376)
## Summary

This PR is related to the discussion:
https://github.com/apache/airflow/issues/54714

This change creates a new code (`AIR321`) and implement ruff rules to
catch, and/or fix deprecated imports in Airflow for **Airflow 3.1**. The
rules are implemented by following the structure of `AIR301`. The rules
check whether a removed Airflow name is used, and match on `Expr::Name`
and `Expr::Attribute`.

## Test Plan

The following two test files are added:
1. crates/ruff_linter/resources/test/fixtures/airflow/AIR321_names.py
2.
crates/ruff_linter/resources/test/fixtures/airflow/AIR321_names_fix.py

`AIR321_names.py`
All the test cases in this file should raise violations and fixes should
be suggested when running the test with `--unsafe-fixes`. The test
results shown in the snapshot are expected.
```bash
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/airflow/AIR321_names.py --no-cache --preview --select AIR321 --unsafe-fixes
```
`AIR321_names_fix.py `
All the test cases in this file raise NO violation (i.e., all checks
should pass). The snapshot file is empty.

<img width="1330" height="384" alt="Screenshot from 2026-01-04 16-37-51"
src="https://github.com/user-attachments/assets/8a1d6dca-dc69-41f8-ba9f-822e0bcd04a7"
/>

## Document Update

<img width="942" height="56" alt="Screenshot from 2026-01-04 16-37-02"
src="https://github.com/user-attachments/assets/e678837d-895f-483b-94a0-58dde7c60032"
/>
2026-02-04 15:31:08 -05:00
Leandro Braga b80d8ff6ff [ruff] Detect duplicate entries in __all__ (RUF068) (#22114)
Hello,

This MR adds a new rule and its fix, `RUF069`,
`DuplicateEntryInDunderAll`. I'm using `RUF069` because we already have
[RUF068](https://github.com/astral-sh/ruff/pull/20585) and
[RUF069](https://github.com/astral-sh/ruff/pull/21079#issuecomment-3493839453)
in the works.

The rule job is to prevent users from accidentally adding duplicate
entries to `__all__`, which, for example, can result from copy-paste
mistakes.

It deals with the following syntaxes:

```python
__all__: list[str] = ["a", "a"]
__all__: typing.Any = ("a", "a")
__all__.extend(["a", "a"])
__all__ += ["a", "a"]
```

But it does not keep track of `__all__` contents, meaning the following
code snippet is a false negative:
```python
class A: ...

__all__ = ["A"]
__all__.extend(["A"])
```

## Violation Example

```console
RUF069 `__all__` contains duplicate entries
 --> RUF069.py:2:17
  |
1 | __all__ = ["A", "A", "B"]
  |                 ^^^
help: Remove duplicate entries from `__all__`
1 | __all__ = ["A", "B"]
  - __all__ = ["A", "A", "B"]
```

## Ecosystem Report

The `ruff-ecosystem` results contain seven violations in four projects,
all of them seem like true positives, with one instance appearing to be
an actual bug.

This [code
snippet](https://github.com/python/typeshed/blob/90d855985be5aae9bc76e77b0f3d4b6738c38347/stubs/reportlab/reportlab/lib/rltempfile.pyi#L4)
from `reportlab` contains the same entry twice instead of exporting both
functions.

```python
def get_rl_tempdir(*subdirs: str) -> str: ...
def get_rl_tempfile(fn: str | None = None) -> str: ...

__all__ = ("get_rl_tempdir", "get_rl_tempdir")
```

Closes [#21945](https://github.com/astral-sh/ruff/issues/21945)

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2026-01-16 14:58:06 -05:00
Jason K Hall 3b61da0da3 Allow Python 3.15 as valid target-version value in preview (#22419) 2026-01-07 09:38:36 +01:00
renovate[bot] 2395954d9a Update Rust crate schemars to v1.2.0 (#22391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2026-01-05 09:08:58 +01:00
Kevin Yang b2b9d91859 [airflow] Passing positional argument into airflow.lineage.hook.HookLineageCollector.create_asset is not allowed (AIR303) (#22046)
## Summary

This is a follow up PR to https://github.com/astral-sh/ruff/pull/21096

The new code AIR303 is added for checking function signature change in
Airflow 3.0. The new rule added to AIR303 will check if positional
argument is passed into
`airflow.lineage.hook.HookLineageCollector.create_asset`. Since this
method is updated to accept only keywords argument, passing positional
argument into it is not allowed, and will raise an error. The test is
done by checking whether positional argument with 0 index can be found.

## Test Plan

A new test file is added to the fixtures for the code AIR303. Snapshot
test is updated accordingly.

<img width="1444" height="513" alt="Screenshot from 2025-12-17 20-54-48"
src="https://github.com/user-attachments/assets/bc235195-e986-4743-9bf7-bba65805fb87"
/>

<img width="981" height="433" alt="Screenshot from 2025-12-17 21-34-29"
src="https://github.com/user-attachments/assets/492db71f-58f2-40ba-ad2f-f74852fa5a6b"
/>
2025-12-30 11:39:08 -05:00
Brent Westbrook c483b59ddd [ruff] Add non-empty-init-module (RUF067) (#22143)
Summary
--

This PR adds a new rule, `non-empty-init-module`, which restricts the
kind of
code that can be included in an `__init__.py` file. By default,
docstrings,
imports, and assignments to `__all__` are allowed. When the new
configuration
option `lint.ruff.strictly-empty-init-modules` is enabled, no code at
all is
allowed.

This closes #9848, where these two variants correspond to different
rules in the

[`flake8-empty-init-modules`](https://github.com/samueljsb/flake8-empty-init-modules/)
linter. The upstream rules are EIM001, which bans all code, and EIM002,
which
bans non-import/docstring/`__all__` code. Since we discussed folding
these into
one rule on [Discord], I just added the rule to the `RUF` group instead
of
adding a new `EIM` plugin.

I'm not really sure we need to flag docstrings even when the strict
setting is
enabled, but I just followed upstream for now. Similarly, as I noted in
a TODO
comment, we could also allow more statements involving `__all__`, such
as
`__all__.append(...)` or `__all__.extend(...)`. The current version only
allows
assignments, like upstream, as well as annotated and augmented
assignments,
unlike upstream.

I think when we discussed this previously, we considered flagging the
module
itself as containing code, but for now I followed the upstream
implementation of
flagging each statement in the module that breaks the rule (actually the
upstream linter flags each _line_, including comments). This will
obviously be a
bit noisier, emitting many diagnostics for the same module. But this
also seems
preferable because it flags every statement that needs to be fixed up
front
instead of only emitting one diagnostic for the whole file that persists
as you
keep removing more lines. It was also easy to implement in
`analyze::statement`
without a separate visitor.

The first commit adds the rule and baseline tests, the second commit
adds the
option and a diff test showing the additional diagnostics when the
setting is
enabled.

I noticed a small (~2%) performance regression on our largest benchmark,
so I also added a cached `Checker::in_init_module` field and method
instead of the `Checker::path` method. This was almost the only reason
for the `Checker::path` field at all, but there's one remaining
reference in a `warn_user!`
[call](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs#L188).

Test Plan
--

New tests adapted from the upstream linter

## Ecosystem Report

I've spot-checked the ecosystem report, and the results look "correct."
This is obviously a very noisy rule if you do include code in
`__init__.py` files. We could make it less noisy by adding more
exceptions (e.g. allowing `if TYPE_CHECKING` blocks, allowing
`__getattr__` functions, allowing imports from `importlib` assignments),
but I'm sort of inclined just to start simple and see what users need.

[Discord]:
https://discord.com/channels/1039017663004942429/1082324250112823306/1440086001035771985

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-12-30 11:32:10 -05:00
Amethyst Reese 3d334a313e Report diagnostics for invalid/unmatched range suppression comments (#21908)
## Summary

- Adds new RUF103 and RUF104 diagnostics for invalid and unmatched
suppression comments
- Reports RUF100 for any unused range suppression
- Reports RUF102 for range suppression comment with invalid rule codes
- Reports RUF103 for range suppression comment with invalid suppression syntax
- Reports RUF104 diagnostics for any unmatched range suppression comment (disable w/o enable)


## Test Plan

Updated snapshots from test cases with unmatched suppression comments

Issue #3711
Fixes #21878
Fixes #21875
2025-12-18 12:58:58 -08:00
Shantanu cf8d2e35a8 New rule to prevent implicit string concatenation in collections (#21972)
This is a common footgun, see the example in
https://github.com/astral-sh/ruff/issues/13014#issuecomment-3411496519

Fixes #13014 , fixes #13031
2025-12-17 17:37:01 -05:00
Shahar Naveh 33713a7e2a Add rule to detect unnecessary class properties (#21535)
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Amethyst Reese <amethyst@n7.gg>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-11-26 09:31:22 +01:00
Gautham Venkataraman 665f68036c analyze: Add option to skip over imports in TYPE_CHECKING blocks (#21472)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-11-16 12:30:24 +00:00
chiri 9d7da914b9 Improve extend docs (#21135)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-10-31 15:10:14 +00:00
wangxiaolei 28aed61a22 [pylint] Implement stop-iteration-return (PLR1708) (#20733)
## Summary

implement pylint rule stop-iteration-return / R1708

## Test Plan

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

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-10-23 15:02:41 -07:00
Takayuki Maeda 48b50128eb [ruff] Update schemars to v1 (#20942) 2025-10-20 08:59:52 +02:00
Auguste Lalande 03696687ea [pydoclint] Implement docstring-extraneous-parameter (DOC102) (#20376)
## Summary

Implement `docstring-extraneous-parameter` (`DOC102`). This rule checks
that all parameters present in a functions docstring are also present in
its signature.

Split from #13280, per this
[comment](https://github.com/astral-sh/ruff/pull/13280#issuecomment-3280575506).

Part of #12434.

## Test Plan

Test cases added.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-10-16 11:26:51 -04:00
Matt Norton e338d2095e Update lint.flake8-type-checking.quoted-annotations docs (#20765)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-10-14 06:43:24 +00:00
Brent Westbrook 88c0ce3e38 Update default and latest Python versions for 3.14 (#20725)
Summary
--

Closes #19467 and also removes the warning about using Python 3.14
without
preview enabled.

I also bumped `PythonVersion::default` to 3.9 because it reaches EOL
this month,
but we could also defer that for now if we wanted.

The first three commits are related to the `latest` bump to 3.14; the
fourth commit
bumps the default to 3.10.

Note that this PR also bumps the default Python version for ty to 3.10
because
there was a test asserting that it stays in sync with
`ast::PythonVersion`.

Test Plan
--

Existing tests

I spot-checked the ecosystem report, and I believe these are all
expected. Inbits doesn't specify a target Python version, so I guess
we're applying the default. UP007, UP035, and UP045 all use the new
default value to emit new diagnostics.
2025-10-07 12:23:11 -04:00
Brent Westbrook 6b7a9dc2f2 [isort] Clarify dependency between order-by-type and case-sensitive settings (#20559)
Summary
--

Fixes #20536 by linking between the isort options `case-sensitive` and
`order-by-type`. The latter takes precedence over the former, so it
seems good to clarify this somewhere.

I tweaked the wording slightly, but this is otherwise based on the patch
from @SkylerWittman in
https://github.com/astral-sh/ruff/issues/20536#issuecomment-3326097324
(thank you!)

Test Plan
--

N/a

---------

Co-authored-by: Skyler Wittman <skyler.wittman@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-25 16:25:12 +00:00
Pieter Cardillo Kwok edb920b4d5 [flake8-async] Implement blocking-path-method (ASYNC240) (#20264)
## Summary
Adds a new rule to find and report use of `os.path` or `pathlib.Path` in
async functions.

Issue: #8451

## Test Plan

Using `cargo insta test`
2025-09-23 08:30:47 -04:00
Gary Yendell 44fc87f491 [ruff] Add logging-eager-conversion (RUF065) (#19942)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Fixes #12734

I have started with simply checking if any arguments that are providing
extra values to the log message are calls to `str` or `repr`, as
suggested in the linked issue. There was a concern that this could cause
false positives and the check should be more explicit. I am happy to
look into that if I have some further examples to work with.

If this is the accepted solution then there are more cases to add to the
test and it should possibly also do test for the same behavior via the
`extra` keyword.

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

## Test Plan

I have added a new test case and python file to flake8_logging_format
with examples of this anti-pattern.

<!-- How was it tested? -->
2025-09-19 16:43:44 -04:00
Dan Parizher c94ddb590f [flake8-bugbear] Add B912: map() without an explicit strict= parameter (#20429)
## Summary

Implements new rule `B912` that requires the `strict=` argument for
`map(...)` calls with two or more iterables on Python 3.14+, following
the same pattern as `B905` for `zip()`.

Closes #20057

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-09-19 12:54:44 -05:00
Dylan 9ca632c84f Stabilize adding future import via config option (#20277)
Introduced in #19100. Removed gating, updated tests, removed warning(s),
and updated documentation.
2025-09-10 09:00:27 -04:00
हिमांशु 3dbdd2b883 [pyupgrade] Remove non-pep604-isinstance (UP038) (#19156)
## Summary
This PR Removes deprecated UP038 as per instructed in #18727 
closes #18727 
## Test Plan
I have run tests non of them failing 

One Question i have is do we have to document that UP038 is removed?

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-09-10 09:00:27 -04:00
हिमांशु d8e43bf9f7 [pandas-vet] Remove pandas-df-variable-name (PD901) (#19223)
## Summary
closes #7710 

## Test Plan

It is is removal so i don't think we have to add tests otherwise i have
followed test plan mentioned in contributing.md

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-09-10 09:00:27 -04:00
Amethyst Reese ca1f66a657 [flake8-async] Implement blocking-input rule (ASYNC250) (#20122)
## Summary

Adds new rule to catch use of builtins `input()` in async functions.

Issue #8451

## Test Plan

New snapshosts in `ASYNC250.py` with `cargo insta test`.
2025-08-28 11:04:24 -07:00
Amethyst Reese af259faed5 [flake8-async] Implement blocking-http-call-httpx (ASYNC212) (#20091)
## Summary

Adds new rule to find and report use of `httpx.Client` in synchronous
functions.

See issue #8451

## Test Plan

New snapshots for `ASYNC212.py` with `cargo insta test`.
2025-08-27 15:19:01 -07:00
Charlie Marsh d9cab4d242 Add support for specifying minimum dots in detected string imports (#19538)
## Summary

Defaults to requiring two dots, which matches the Pants default.
2025-07-24 15:48:23 -04:00
Brent Westbrook 893f5727e5 [flake8-type-checking, pyupgrade, ruff] Add from __future__ import annotations when it would allow new fixes (TC001, TC002, TC003, UP037, RUF013) (#19100)
## Summary

This is a second attempt at addressing
https://github.com/astral-sh/ruff/issues/18502 instead of reusing
`FA100` (#18919).

This PR:
- adds a new `lint.allow-importing-future-annotations` option
- uses the option to add a `__future__` import when it would trigger
`TC001`, `TC002`, or `TC003`
- uses the option to add an import when it would allow unquoting more
annotations in [quoted-annotation
(UP037)](https://docs.astral.sh/ruff/rules/quoted-annotation/#quoted-annotation-up037)
- uses the option to allow the `|` union syntax before 3.10 in
[implicit-optional
(RUF013)](https://docs.astral.sh/ruff/rules/implicit-optional/#implicit-optional-ruf013)

I started adding a fix for [runtime-string-union
(TC010)](https://docs.astral.sh/ruff/rules/runtime-string-union/#runtime-string-union-tc010)
too, as mentioned in my previous
[comment](https://github.com/astral-sh/ruff/issues/18502#issuecomment-3005238092),
but some of the existing tests already imported `from __future__ import
annotations`, so I think we intentionally flag these cases for the user
to inspect. Adding the import is _a_ fix but probably not the best one.

## Test Plan

Existing `TC` tests, new copies of them with the option enabled, and new
tests based on ideas in
https://github.com/astral-sh/ruff/pull/18919#discussion_r2166292705 and
the following thread. For UP037 and RUF013, the new tests are also
copies of the existing tests, with the new option enabled. The easiest
way to review them is probably by their diffs from the existing
snapshots:

### UP037

`UP037_0.py` and `UP037_2.pyi` have no diffs. The diff for `UP037_1.py`
is below. It correctly unquotes an annotation in module scope that would
otherwise be invalid.

<details><summary>UP037_1.py</summary>

```diff
3d2
< snapshot_kind: text
23c22,42
< 12 12 |
---
> 12 12 |
>
> UP037_1.py:14:4: UP037 [*] Remove quotes from type annotation
>    |
> 13 | # OK
> 14 | X: "Tuple[int, int]" = (0, 0)
>    |    ^^^^^^^^^^^^^^^^^ UP037
>    |
>    = help: Remove quotes
>
> ℹ Unsafe fix
>    1  |+from __future__ import annotations
> 1  2  | from typing import TYPE_CHECKING
> 2  3  |
> 3  4  | if TYPE_CHECKING:
> --------------------------------------------------------------------------------
> 11 12 |
> 12 13 |
> 13 14 | # OK
> 14    |-X: "Tuple[int, int]" = (0, 0)
>    15 |+X: Tuple[int, int] = (0, 0)
```

</details>

### RUF013

The diffs here are mostly just the imports because the original snaps
were on 3.13. So we're getting the same fixes now on 3.9.

<details><summary>RUF013_0.py</summary>

```diff
3d2
< snapshot_kind: text
14,16c13,20
< 17 17 |     pass
< 18 18 | 
< 19 19 | 
---
>    1  |+from __future__ import annotations
> 1  2  | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 17 18 |     pass
> 18 19 | 
> 19 20 | 
18,21c22,25
<    20 |+def f(arg: int | None = None):  # RUF013
< 21 21 |     pass
< 22 22 | 
< 23 23 | 
---
>    21 |+def f(arg: int | None = None):  # RUF013
> 21 22 |     pass
> 22 23 | 
> 23 24 | 
32,34c36,43
< 21 21 |     pass
< 22 22 | 
< 23 23 | 
---
>    1  |+from __future__ import annotations
> 1  2  | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 21 22 |     pass
> 22 23 | 
> 23 24 | 
36,39c45,48
<    24 |+def f(arg: str | None = None):  # RUF013
< 25 25 |     pass
< 26 26 | 
< 27 27 | 
---
>    25 |+def f(arg: str | None = None):  # RUF013
> 25 26 |     pass
> 26 27 | 
> 27 28 | 
50,52c59,66
< 25 25 |     pass
< 26 26 | 
< 27 27 | 
---
>    1  |+from __future__ import annotations
> 1  2  | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 25 26 |     pass
> 26 27 | 
> 27 28 | 
54,57c68,71
<    28 |+def f(arg: Tuple[str] | None = None):  # RUF013
< 29 29 |     pass
< 30 30 | 
< 31 31 | 
---
>    29 |+def f(arg: Tuple[str] | None = None):  # RUF013
> 29 30 |     pass
> 30 31 | 
> 31 32 | 
68,70c82,89
< 55 55 |     pass
< 56 56 | 
< 57 57 | 
---
>    1  |+from __future__ import annotations
> 1  2  | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 55 56 |     pass
> 56 57 | 
> 57 58 | 
72,75c91,94
<    58 |+def f(arg: Union | None = None):  # RUF013
< 59 59 |     pass
< 60 60 | 
< 61 61 | 
---
>    59 |+def f(arg: Union | None = None):  # RUF013
> 59 60 |     pass
> 60 61 | 
> 61 62 | 
86,88c105,112
< 59 59 |     pass
< 60 60 | 
< 61 61 | 
---
>    1  |+from __future__ import annotations
> 1  2  | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 59 60 |     pass
> 60 61 | 
> 61 62 | 
90,93c114,117
<    62 |+def f(arg: Union[int] | None = None):  # RUF013
< 63 63 |     pass
< 64 64 | 
< 65 65 | 
---
>    63 |+def f(arg: Union[int] | None = None):  # RUF013
> 63 64 |     pass
> 64 65 | 
> 65 66 | 
104,106c128,135
< 63 63 |     pass
< 64 64 | 
< 65 65 | 
---
>    1  |+from __future__ import annotations
> 1  2  | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 63 64 |     pass
> 64 65 | 
> 65 66 | 
108,111c137,140
<    66 |+def f(arg: Union[int, str] | None = None):  # RUF013
< 67 67 |     pass
< 68 68 | 
< 69 69 | 
---
>    67 |+def f(arg: Union[int, str] | None = None):  # RUF013
> 67 68 |     pass
> 68 69 | 
> 69 70 | 
122,124c151,158
< 82 82 |     pass
< 83 83 | 
< 84 84 | 
---
>    1  |+from __future__ import annotations
> 1  2  | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 82 83 |     pass
> 83 84 | 
> 84 85 | 
126,129c160,163
<    85 |+def f(arg: int | float | None = None):  # RUF013
< 86 86 |     pass
< 87 87 | 
< 88 88 | 
---
>    86 |+def f(arg: int | float | None = None):  # RUF013
> 86 87 |     pass
> 87 88 | 
> 88 89 | 
140,142c174,181
< 86 86 |     pass
< 87 87 | 
< 88 88 | 
---
>    1  |+from __future__ import annotations
> 1  2  | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 86 87 |     pass
> 87 88 | 
> 88 89 | 
144,147c183,186
<    89 |+def f(arg: int | float | str | bytes | None = None):  # RUF013
< 90 90 |     pass
< 91 91 | 
< 92 92 | 
---
>    90 |+def f(arg: int | float | str | bytes | None = None):  # RUF013
> 90 91 |     pass
> 91 92 | 
> 92 93 | 
158,160c197,204
< 105 105 |     pass
< 106 106 | 
< 107 107 | 
---
>     1   |+from __future__ import annotations
> 1   2   | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2   3   | 
> 3   4   | 
> --------------------------------------------------------------------------------
> 105 106 |     pass
> 106 107 | 
> 107 108 | 
162,165c206,209
<     108 |+def f(arg: Literal[1] | None = None):  # RUF013
< 109 109 |     pass
< 110 110 | 
< 111 111 | 
---
>     109 |+def f(arg: Literal[1] | None = None):  # RUF013
> 109 110 |     pass
> 110 111 | 
> 111 112 | 
176,178c220,227
< 109 109 |     pass
< 110 110 | 
< 111 111 | 
---
>     1   |+from __future__ import annotations
> 1   2   | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2   3   | 
> 3   4   | 
> --------------------------------------------------------------------------------
> 109 110 |     pass
> 110 111 | 
> 111 112 | 
180,183c229,232
<     112 |+def f(arg: Literal[1, "foo"] | None = None):  # RUF013
< 113 113 |     pass
< 114 114 | 
< 115 115 | 
---
>     113 |+def f(arg: Literal[1, "foo"] | None = None):  # RUF013
> 113 114 |     pass
> 114 115 | 
> 115 116 | 
194,196c243,250
< 128 128 |     pass
< 129 129 | 
< 130 130 | 
---
>     1   |+from __future__ import annotations
> 1   2   | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2   3   | 
> 3   4   | 
> --------------------------------------------------------------------------------
> 128 129 |     pass
> 129 130 | 
> 130 131 | 
198,201c252,255
<     131 |+def f(arg: Annotated[int | None, ...] = None):  # RUF013
< 132 132 |     pass
< 133 133 | 
< 134 134 | 
---
>     132 |+def f(arg: Annotated[int | None, ...] = None):  # RUF013
> 132 133 |     pass
> 133 134 | 
> 134 135 | 
212,214c266,273
< 132 132 |     pass
< 133 133 | 
< 134 134 | 
---
>     1   |+from __future__ import annotations
> 1   2   | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2   3   | 
> 3   4   | 
> --------------------------------------------------------------------------------
> 132 133 |     pass
> 133 134 | 
> 134 135 | 
216,219c275,278
<     135 |+def f(arg: Annotated[Annotated[int | str | None, ...], ...] = None):  # RUF013
< 136 136 |     pass
< 137 137 | 
< 138 138 | 
---
>     136 |+def f(arg: Annotated[Annotated[int | str | None, ...], ...] = None):  # RUF013
> 136 137 |     pass
> 137 138 | 
> 138 139 | 
232,234c291,298
< 148 148 | 
< 149 149 | 
< 150 150 | def f(
---
>     1   |+from __future__ import annotations
> 1   2   | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2   3   | 
> 3   4   | 
> --------------------------------------------------------------------------------
> 148 149 | 
> 149 150 | 
> 150 151 | def f(
236,239c300,303
<     151 |+    arg1: int | None = None,  # RUF013
< 152 152 |     arg2: Union[int, float] = None,  # RUF013
< 153 153 |     arg3: Literal[1, 2, 3] = None,  # RUF013
< 154 154 | ):
---
>     152 |+    arg1: int | None = None,  # RUF013
> 152 153 |     arg2: Union[int, float] = None,  # RUF013
> 153 154 |     arg3: Literal[1, 2, 3] = None,  # RUF013
> 154 155 | ):
253,255c317,324
< 149 149 | 
< 150 150 | def f(
< 151 151 |     arg1: int = None,  # RUF013
---
>     1   |+from __future__ import annotations
> 1   2   | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2   3   | 
> 3   4   | 
> --------------------------------------------------------------------------------
> 149 150 | 
> 150 151 | def f(
> 151 152 |     arg1: int = None,  # RUF013
257,260c326,329
<     152 |+    arg2: Union[int, float] | None = None,  # RUF013
< 153 153 |     arg3: Literal[1, 2, 3] = None,  # RUF013
< 154 154 | ):
< 155 155 |     pass
---
>     153 |+    arg2: Union[int, float] | None = None,  # RUF013
> 153 154 |     arg3: Literal[1, 2, 3] = None,  # RUF013
> 154 155 | ):
> 155 156 |     pass
274,276c343,350
< 150 150 | def f(
< 151 151 |     arg1: int = None,  # RUF013
< 152 152 |     arg2: Union[int, float] = None,  # RUF013
---
>     1   |+from __future__ import annotations
> 1   2   | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2   3   | 
> 3   4   | 
> --------------------------------------------------------------------------------
> 150 151 | def f(
> 151 152 |     arg1: int = None,  # RUF013
> 152 153 |     arg2: Union[int, float] = None,  # RUF013
278,281c352,355
<     153 |+    arg3: Literal[1, 2, 3] | None = None,  # RUF013
< 154 154 | ):
< 155 155 |     pass
< 156 156 | 
---
>     154 |+    arg3: Literal[1, 2, 3] | None = None,  # RUF013
> 154 155 | ):
> 155 156 |     pass
> 156 157 | 
292,294c366,373
< 178 178 |     pass
< 179 179 | 
< 180 180 | 
---
>     1   |+from __future__ import annotations
> 1   2   | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2   3   | 
> 3   4   | 
> --------------------------------------------------------------------------------
> 178 179 |     pass
> 179 180 | 
> 180 181 | 
296,299c375,378
<     181 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None):  # RUF013
< 182 182 |     pass
< 183 183 | 
< 184 184 | 
---
>     182 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None):  # RUF013
> 182 183 |     pass
> 183 184 | 
> 184 185 | 
307c386
<     = help: Convert to `T | None`
---
>     = help: Convert to `Optional[T]`
314c393
<     188 |+def f(arg: "int | None" = None):  # RUF013
---
>     188 |+def f(arg: "Optional[int]" = None):  # RUF013
325c404
<     = help: Convert to `T | None`
---
>     = help: Convert to `Optional[T]`
332c411
<     192 |+def f(arg: "str | None" = None):  # RUF013
---
>     192 |+def f(arg: "Optional[str]" = None):  # RUF013
343c422
<     = help: Convert to `T | None`
---
>     = help: Convert to `Optional[T]`
354,356c433,440
< 201 201 |     pass
< 202 202 | 
< 203 203 | 
---
>     1   |+from __future__ import annotations
> 1   2   | from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
> 2   3   | 
> 3   4   | 
> --------------------------------------------------------------------------------
> 201 202 |     pass
> 202 203 | 
> 203 204 | 
358,361c442,445
<     204 |+def f(arg: Union["int", "str"] | None = None):  # RUF013
< 205 205 |     pass
< 206 206 | 
< 207 207 |
---
>     205 |+def f(arg: Union["int", "str"] | None = None):  # RUF013
> 205 206 |     pass
> 206 207 | 
> 207 208 |
```

</details>

<details><summary>RUF013_1.py</summary>

```diff
3d2
< snapshot_kind: text
15,16c14,16
< 2 2 |
< 3 3 |
---
>   2 |+from __future__ import annotations
> 2 3 |
> 3 4 |
18,19c18,19
<   4 |+def f(arg: int | None = None):  # RUF013
< 5 5 |     pass
---
>   5 |+def f(arg: int | None = None):  # RUF013
> 5 6 |     pass
```

</details>

<details><summary>RUF013_3.py</summary>

```diff
3d2
< snapshot_kind: text
14,16c13,16
< 1 1 | import typing
< 2 2 | 
< 3 3 | 
---
>   1 |+from __future__ import annotations
> 1 2 | import typing
> 2 3 | 
> 3 4 | 
18,21c18,21
<   4 |+def f(arg: typing.List[str] | None = None):  # RUF013
< 5 5 |     pass
< 6 6 | 
< 7 7 | 
---
>   5 |+def f(arg: typing.List[str] | None = None):  # RUF013
> 5 6 |     pass
> 6 7 | 
> 7 8 | 
32,34c32,39
< 19 19 |     pass
< 20 20 | 
< 21 21 | 
---
>    1  |+from __future__ import annotations
> 1  2  | import typing
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 19 20 |     pass
> 20 21 | 
> 21 22 | 
36,39c41,44
<    22 |+def f(arg: typing.Union[int, str] | None = None):  # RUF013
< 23 23 |     pass
< 24 24 | 
< 25 25 | 
---
>    23 |+def f(arg: typing.Union[int, str] | None = None):  # RUF013
> 23 24 |     pass
> 24 25 | 
> 25 26 | 
50,52c55,62
< 26 26 | # Literal
< 27 27 | 
< 28 28 | 
---
>    1  |+from __future__ import annotations
> 1  2  | import typing
> 2  3  | 
> 3  4  | 
> --------------------------------------------------------------------------------
> 26 27 | # Literal
> 27 28 | 
> 28 29 | 
54,55c64,65
<    29 |+def f(arg: typing.Literal[1, "foo", True] | None = None):  # RUF013
< 30 30 |     pass
---
>    30 |+def f(arg: typing.Literal[1, "foo", True] | None = None):  # RUF013
> 30 31 |     pass
```

</details>

<details><summary>RUF013_4.py</summary>

```diff
3d2
< snapshot_kind: text
13,15c12,20
< 12 12 | def multiple_1(arg1: Optional, arg2: Optional = None): ...
< 13 13 |
< 14 14 |
---
> 1  1  | # https://github.com/astral-sh/ruff/issues/13833
>    2  |+from __future__ import annotations
> 2  3  |
> 3  4  | from typing import Optional
> 4  5  |
> --------------------------------------------------------------------------------
> 12 13 | def multiple_1(arg1: Optional, arg2: Optional = None): ...
> 13 14 |
> 14 15 |
17,20c22,25
<    15 |+def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int | None = None): ...
< 16 16 |
< 17 17 |
< 18 18 | def return_type(arg: Optional = None) -> Optional: ...
---
>    16 |+def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int | None = None): ...
> 16 17 |
> 17 18 |
> 18 19 | def return_type(arg: Optional = None) -> Optional: ...
```

</details>

## Future work

This PR does not touch UP006, UP007, or UP045, which are currently
coupled to FA100. If this new approach turns out well, we may eventually
want to deprecate FA100 and add a `__future__` import in those rules'
fixes too.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-07-16 08:50:52 -04:00
Leander Cain Slotosch 1b813cd5f1 Fix description of the format.skip-magic-trailing-comma example (#19095)
## Summary

This PR fixes a typo in the docs, where both variants of a config have
the same description.
2025-07-03 10:39:59 -04:00