Compare commits

...

37 Commits

Author SHA1 Message Date
David Peter
ea56e2415f [ty] Support field_specifiers for metaclass based transformers 2025-10-15 17:21:22 +02:00
David Peter
23543194fc Recognize custom field-specifier functions 2025-10-15 16:39:26 +02:00
David Peter
4dc88a0a5f [ty] Store field_specifiers 2025-10-15 15:07:19 +02:00
Douglas Creager
8817ea5c84 [ty] Add (unused) inferable parameter to type property methods (#20865)
A large part of the diff on #20677 just involves threading a new
`inferable` parameter through all of the type property methods. In the
interests of making that PR easier to review, I've pulled that bit out
into here, so that it can be reviewed in isolation. This should be a
pure refactoring, with no logic changes or behavioral changes.
2025-10-15 09:05:15 -04:00
Micha Reiser
85ff4f3eef Run macos tests on macos (#20889) 2025-10-15 14:41:33 +02:00
Micha Reiser
c6959381f8 Remove release CI job (#20887) 2025-10-15 12:39:31 +02:00
David Peter
270ba71ad5 [ty] CI: Faster ecosystem analysis (#20886)
## Summary

I considered making a dedicated cargo profile for these, but the
`profiling` profile basically made all the modifications to `release`
that I would have also made.

## Test Plan

CI on this PR
2025-10-15 12:38:17 +02:00
Micha Reiser
cb4d4493d7 Remove strip from release profile (#20885) 2025-10-15 09:36:05 +00:00
github-actions[bot]
cafb96aa7a [ty] Sync vendored typeshed stubs (#20876)
Close and reopen this PR to trigger CI

---------

Co-authored-by: typeshedbot <>
Co-authored-by: David Peter <mail@david-peter.de>
2025-10-15 11:13:32 +02:00
Andrew Gallant
651f7963a7 [ty] Add some completion ranking improvements (#20807)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-10-15 08:59:33 +00:00
Micha Reiser
4fc7dd300c Improved error recovery for unclosed strings (including f- and t-strings) (#20848) 2025-10-15 09:50:56 +02:00
Micha Reiser
a93618ed23 Enable lto=fat (#20863)
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-10-15 08:59:59 +02:00
Dan Parizher
9e1aafd0ce [pyupgrade] Extend UP019 to detect typing_extensions.Text (UP019) (#20825)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-10-15 06:52:14 +00:00
Dylan
abf685b030 [flake8-bugbear] Omit annotation in preview fix for B006 (#20877)
Closes #20864
2025-10-15 01:14:01 +00:00
Paillat
e1e3eb7209 fix(docs): Fix typo in RUF015 description (#20873)
## Summary
Fixed a typo. It should be "or", not "of". Both `.pop()` and `next()` on
an empty collection will raise `IndexError`, not "`[0]` of the `pop()`
function"

## Test Plan

n/a
2025-10-14 21:38:31 +00:00
Alex Waygood
43eddc566f [ty] Improve and extend tests for instance attributes redeclared in subclasses (#20866)
Part of https://github.com/astral-sh/ty/issues/1345
2025-10-14 19:31:34 +01:00
David Peter
f8e00e3cd9 [ty] Ignore slow seeds as a temporary measure (#20870)
## Summary

Basically what @AlexWaygood suggested
[here](https://github.com/astral-sh/ruff/pull/20802#issuecomment-3402218389)
(thank you).

## Test Plan

CI on this PR
2025-10-14 20:02:20 +02:00
Brent Westbrook
591e9bbccb Remove parentheses around multiple exception types on Python 3.14+ (#20768)
Summary
--

This PR implements the black preview style from
https://github.com/psf/black/pull/4720. As of Python 3.14, you're
allowed to omit the parentheses around groups of exceptions, as long as
there's no `as` binding:

**3.13**

```pycon
Python 3.13.4 (main, Jun  4 2025, 17:37:06) [Clang 20.1.4 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> try: ...
... except (Exception, BaseException): ...
...
Ellipsis
>>> try: ...
... except Exception, BaseException: ...
...
  File "<python-input-1>", line 2
    except Exception, BaseException: ...
           ^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized
```

**3.14**

```pycon
Python 3.14.0rc2 (main, Sep  2 2025, 14:20:56) [Clang 20.1.4 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> try: ...
... except Exception, BaseException: ...
...
Ellipsis
>>> try: ...
... except (Exception, BaseException): ...
...
Ellipsis
>>> try: ...
... except Exception, BaseException as e: ...
...
  File "<python-input-2>", line 2
    except Exception, BaseException as e: ...
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized when using 'as'
```

I think this ended up being pretty straightforward, at least once Micha
showed me where to start :)

Test Plan
--

New tests

At first I thought we were deviating from black in how we handle
comments within the exception type tuple, but I think this applies to
how we format all tuples, not specifically with the new preview style.
2025-10-14 11:17:45 -04:00
Brent Westbrook
1ed9b215b9 Update Black tests (#20794)
Summary
--

```shell
git clone git@github.com:psf/black.git ../other/black
crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py ../other/black
```

Then ran our tests and accepted the snapshots

I had to make a small fix to our tuple normalization logic for `del`
statements
in the second commit, otherwise the tests were panicking at a changed
AST. I
think the new implementation is closer to the intention described in the
nearby
comment anyway, though.

The first commit adds the new Python, settings, and `.expect` files, the
next three commits make some small
fixes to help get the tests running, and then the fifth commit accepts
all but one of the new snapshots. The last commit includes the new
unsupported syntax error for one f-string example, tracked in #20774.

Test Plan
--

Newly imported tests. I went through all of the new snapshots and added
review comments below. I think they're all expected, except a few cases
I wasn't 100% sure about.
2025-10-14 10:14:59 -04:00
Alex Waygood
9090aead0f [ty] Fix further issues in super() inference logic (#20843) 2025-10-14 12:48:47 +00:00
Micha Reiser
441ba20876 [ty] Document when a rule was added (#20859) 2025-10-14 14:33:48 +02:00
David Peter
6341bb7403 [ty] Treat Callable dunder members as bound method descriptors (#20860)
## Summary

Dunder methods (at least the ones defined in the standard library)
always take an instance of the class as the first parameter. So it seems
reasonable to generally treat them as bound method descriptors if they
are defined via a `Callable` type.

This removes just a few false positives from the ecosystem, but solves
three user-reported issues:

closes https://github.com/astral-sh/ty/issues/908
closes https://github.com/astral-sh/ty/issues/1143
closes https://github.com/astral-sh/ty/issues/1209

In addition to the change here, I also considered [making `ClassVar`s
bound method descriptors](https://github.com/astral-sh/ruff/pull/20861).
However, there was zero ecosystem impact. So I think we can also close
https://github.com/astral-sh/ty/issues/491 with this PR.

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

## Test Plan

Added regression test
2025-10-14 14:27:52 +02:00
David Peter
ac2c530377 [ty] Handle decorators which return unions of Callables (#20858)
## Summary

If a function is decorated with a decorator that returns a union of
`Callable`s, also treat it as a union of function-like `Callable`s.

Labeling as `internal`, since the previous change has not been released
yet.

## Test Plan

New regression test.
2025-10-14 09:47:50 +00:00
Dan Parizher
c69fa75cd5 Fix false negatives in Truthiness::from_expr for lambdas, generators, and f-strings (#20704) 2025-10-14 03:06:17 -05:00
David Peter
f73bb45be6 [ty] Rename Type unwrapping methods (#20857)
## Summary

Rename "unwrapping" methods on `Type` from e.g.
`Type::into_class_literal` to `Type::as_class_literal`. I personally
find that name more intuitive, since no transformation of any kind is
happening. We are just unwrapping from certain enum variants. An
alternative would be `try_as_class_literal`, which would follow the
[`strum` naming
scheme](https://docs.rs/strum/latest/strum/derive.EnumTryAs.html), but
is slightly longer.

Also rename `Type::into_callable` to `Type::try_upcast_to_callable`.
Note that I intentionally kept names like
`FunctionType::into_callable_type`, because those return `CallableType`,
not `Option<Type<…>>`.

## Test Plan

Pure refactoring
2025-10-14 09:53:29 +02: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
Douglas Creager
5e08e5451d [ty] Add separate type for typevar "identity" (#20813)
As part of #20598, we added `is_identical_to` methods to
`TypeVarInstance` and `BoundTypeVarInstance`, which compare when two
typevar instances refer to "the same" underlying typevar, even if we
have forced their lazy bounds/constraints as part of marking typevars as
inferable. (Doing so results in a different salsa interned struct ID,
since we've changed the contents of the `bounds_or_constraints` field.)

It turns out that marking typevars as inferable is not the only way that
we might force lazy bounds/constraints; it also happens when we
materialize a type containing a typevar. This surfaced as ecosystem
report failures on #20677.

That means that we need a more long-term fix to this problem.
(`is_identical_to`, and its underlying `original` field, were meant to
be a temporary fix until we removed the `MarkTypeVarsInferable` type
mapping.)

This PR extracts out a separate type (`TypeVarIdentity`) that only
includes the fields that actually inform whether two typevars are "the
same". All other properties of the typevar (default, bounds/constraints,
etc) still live in `TypeVarInstance`. Call sites that care about typevar
identity can now either store just `TypeVarIdentity` (if they never need
access to those other properties), or continue to store
`TypeVarInstance` but pull out its `identity` when performing those "are
they the same typevar" comparisons. (All of this also applies
respectively to `BoundTypeVar{Identity,Instance}`.) In particular,
constraint sets now work on `BoundTypeVarIdentity`, and generic contexts
still _store_ a `BoundTypeVarInstance` (since we might need access to
defaults when specializing), but are keyed on `BoundTypeVarIdentity`.
2025-10-13 20:09:27 -04:00
Douglas Creager
aba0bd568e [ty] Diagnostic for generic classes that reference typevars in enclosing scope (#20822)
Generic classes are not allowed to bind or reference a typevar from an
enclosing scope:

```py
def f[T](x: T, y: T) -> None:
    class Ok[S]: ...
    # error: [invalid-generic-class]
    class Bad1[T]: ...
    # error: [invalid-generic-class]
    class Bad2(Iterable[T]): ...

class C[T]:
    class Ok1[S]: ...
    # error: [invalid-generic-class]
    class Bad1[T]: ...
    # error: [invalid-generic-class]
    class Bad2(Iterable[T]): ...
```

It does not matter if the class uses PEP 695 or legacy syntax. It does
not matter if the enclosing scope is a generic class or function. The
generic class cannot even _reference_ an enclosing typevar in its base
class list.

This PR adds diagnostics for these cases.

In addition, the PR adds better fallback behavior for generic classes
that violate this rule: any enclosing typevars are not included in the
class's generic context. (That ensures that we don't inadvertently try
to infer specializations for those typevars in places where we
shouldn't.) The `dulwich` ecosystem project has [examples of
this](d912eaaffd/dulwich/config.py (L251))
that were causing new false positives on #20677.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-10-13 19:30:49 -04:00
Martín Gaitán
83b497ce88 Update Python compatibility from 3.13 to 3.14 in README.md (#20852)
After #20725 ruff is compatible with Python 3.14 without preview
enabled, so lets note it in the README
2025-10-13 20:49:11 +00:00
Bhuminjay Soni
2b729b4d52 [syntax-errors]: break outside loop F701 (#20556)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

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

This PR implements https://docs.astral.sh/ruff/rules/break-outside-loop/
(F701) as a semantic syntax error.

## Test Plan

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

---------

Signed-off-by: 11happy <soni5happy@gmail.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-10-13 20:00:59 +00:00
David Peter
4b8e278a88 [ty] Treat Callables as bound-method descriptors in special cases (#20802)
## Summary

Treat `Callable`s as bound-method descriptors if `Callable` is the
return type of a decorator that is applied to a function definition. See
the [rendered version of the new test
file](https://github.com/astral-sh/ruff/blob/david/callables-as-descriptors/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md)
for the full description of this new heuristic.

I could imagine that we want to treat `Callable`s as bound-method
descriptors in other cases as well, but this seems like a step in the
right direction. I am planning to add other "use cases" from
https://github.com/astral-sh/ty/issues/491 to this test suite.

partially addresses https://github.com/astral-sh/ty/issues/491
closes https://github.com/astral-sh/ty/issues/1333

## Ecosystem impact

All positive

* 2961 removed `unsupported-operator` diagnostics on `sympy`, which was
one of the main motivations for implementing this change
* 37 removed `missing-argument` diagnostics, and no added call-error
diagnostics, which is an indicator that this heuristic shouldn't cause
many false positives
* A few removed `possibly-missing-attribute` diagnostics when accessing
attributes like `__name__` on decorated functions. The two added
`unused-ignore-comment` diagnostics are also cases of this.
* One new `invalid-assignment` diagnostic on `dd-trace-py`, which looks
suspicious, but only because our `invalid-assignment` diagnostics are
not great. This is actually a "Implicit shadowing of function"
diagnostic that hides behind the `invalid-assignment` diagnostic,
because a module-global function is being patched through a
`module.func` attribute assignment.

## Test Plan

New Markdown tests.
2025-10-13 21:17:47 +02:00
David Peter
d912f13661 [ty] Do not bind self to non-positional parameters (#20850)
## Summary

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

## Test Plan

Regression test
2025-10-13 20:44:27 +02:00
Brent Westbrook
71f8389f61 Fix syntax error false positives on parenthesized context managers (#20846)
This PR resolves the issue noticed in
https://github.com/astral-sh/ruff/pull/20777#discussion_r2417233227.
Namely, cases like this were being flagged as syntax errors despite
being perfectly valid on Python 3.8:

```pycon
Python 3.8.20 (default, Oct  2 2024, 16:34:12)
[Clang 18.1.8 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> with (open("foo.txt", "w")): ...
...
Ellipsis
>>> with (open("foo.txt", "w")) as f: print(f)
...
<_io.TextIOWrapper name='foo.txt' mode='w' encoding='UTF-8'>
```

The second of these was already allowed but not the first:

```shell
> ruff check --target-version py38 --ignore ALL - <<EOF
with (open("foo.txt", "w")): ...
with (open("foo.txt", "w")) as f: print(f)
EOF
invalid-syntax: Cannot use parentheses within a `with` statement on Python 3.8 (syntax was added in Python 3.9)
 --> -:1:6
  |
1 | with (open("foo.txt", "w")): ...
  |      ^
2 | with (open("foo.txt", "w")) as f: print(f)
  |

Found 1 error.
```

There was some discussion of related cases in
https://github.com/astral-sh/ruff/pull/16523#discussion_r1984657793, but
it seems I overlooked the single-element case when flagging tuples. As
suggested in the other thread, we can just check if there's more than
one element or a trailing comma, which will cause the tuple parsing on
<=3.8 and avoid the false positives.
2025-10-13 14:13:27 -04:00
Micha Reiser
373fe8a39c [ty] Remove 'pre-release software' warning (#20817) 2025-10-13 19:50:19 +02:00
Brent Westbrook
975891fc90 Render unsupported syntax errors in formatter tests (#20777)
## Summary

Based on the suggestion in
https://github.com/astral-sh/ruff/issues/20774#issuecomment-3383153511,
I added rendering of unsupported syntax errors in our `format` test.

In support of this, I added a `DummyFileResolver` type to `ruff_db` to
pass to `DisplayDiagnostics::new` (first commit). Another option would
obviously be implementing this directly in the fixtures, but we'd have
to import a `NotebookIndex` somehow; either by depending directly on
`ruff_notebook` or re-exporting it from `ruff_db`. I thought it might be
convenient elsewhere to have a dummy resolver, for example in the
parser, where we currently have a separate rendering pipeline
[copied](https://github.com/astral-sh/ruff/blob/main/crates/ruff_python_parser/tests/fixtures.rs#L321)
from our old rendering code in `ruff_linter`. I also briefly tried
implementing a `TestDb` in the formatter since I noticed the
`ruff_python_formatter::db` module, but that was turning into a lot more
code than the dummy resolver.

We could also push this a bit further if we wanted. I didn't add the new
snapshots to the black compatibility tests or to the preview snapshots,
for example. I thought it was kind of noisy enough (and helpful enough)
already, though. We could also use a shorter diagnostic format, but the
full output seems most useful once we accept this initial large batch of
changes.

## Test Plan

I went through the baseline snapshots pretty quickly, but they all
looked reasonable to me, with one exception I noted below. I also tested
that the case from #20774 produces a new unsupported syntax error.
2025-10-13 10:00:37 -04:00
David Peter
195e8f0684 [ty] Treat functions, methods, and dynamic types as function-like Callables (#20842)
## Summary

Treat functions, methods, and dynamic types as function-like `Callable`s

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

## Ecosystem analysis

All removed diagnostics look like cases of
https://github.com/astral-sh/ty/issues/1344

## Test Plan

Added regression test
2025-10-13 15:21:55 +02:00
Alex Waygood
513d2996ec [ty] Move logic for super() inference to a new types::bound_super submodule (#20840) 2025-10-13 11:18:13 +00:00
418 changed files with 13856 additions and 3665 deletions

View File

@@ -142,7 +142,7 @@ jobs:
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- 'python/py_fuzzer/**' \
if git diff --quiet "${MERGE_BASE}...HEAD" -- 'python/py-fuzzer/**' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
@@ -361,6 +361,37 @@ jobs:
cargo nextest run --all-features --profile ci
cargo test --all-features --doc
cargo-test-macos:
name: "cargo test (macos)"
runs-on: macos-latest
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: |
cargo nextest run --all-features --profile ci
cargo test --all-features --doc
cargo-test-wasm:
name: "cargo test (wasm)"
runs-on: ubuntu-latest
@@ -391,23 +422,6 @@ jobs:
cd crates/ty_wasm
wasm-pack test --node
cargo-build-release:
name: "cargo build (release)"
runs-on: macos-latest
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Build"
run: cargo build --release --locked
cargo-build-msrv:
name: "cargo build (msrv)"
runs-on: depot-ubuntu-latest-8
@@ -721,7 +735,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Run ty completion evaluation"
run: cargo run --release --package ty_completion_eval -- all --threshold 0.1 --tasks /tmp/completion-evaluation-tasks.csv
run: cargo run --release --package ty_completion_eval -- all --threshold 0.4 --tasks /tmp/completion-evaluation-tasks.csv
- name: "Ensure there are no changes"
run: diff ./crates/ty_completion_eval/completion-evaluation-tasks.csv /tmp/completion-evaluation-tasks.csv

View File

@@ -64,12 +64,12 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@279f8a15b0e7f77213bf9096dbc2335a19ef89c5"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
ecosystem-analyzer \
--repository ruff \
diff \
--profile=release \
--profile=profiling \
--projects-old ruff/projects_old.txt \
--projects-new ruff/projects_new.txt \
--old old_commit \

View File

@@ -49,13 +49,13 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@279f8a15b0e7f77213bf9096dbc2335a19ef89c5"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
ecosystem-analyzer \
--verbose \
--repository ruff \
analyze \
--profile=release \
--profile=profiling \
--projects ruff/crates/ty_python_semantic/resources/primer/good.txt \
--output ecosystem-diagnostics.json

View File

@@ -268,12 +268,7 @@ large_stack_arrays = "allow"
[profile.release]
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times
# and runtime performance[1].
#
# [1]: https://github.com/astral-sh/ruff/pull/9031
lto = "thin"
lto = "fat"
codegen-units = 16
# Some crates don't change as much but benefit more from
@@ -283,6 +278,8 @@ codegen-units = 16
codegen-units = 1
[profile.release.package.ruff_python_ast]
codegen-units = 1
[profile.release.package.salsa]
codegen-units = 1
[profile.dev.package.insta]
opt-level = 3
@@ -298,11 +295,30 @@ opt-level = 3
[profile.dev.package.ruff_python_parser]
opt-level = 1
# This profile is meant to mimic the `release` profile as closely as
# possible, but using settings that are more beneficial for iterative
# development. That is, the `release` profile is intended for actually
# building the release, where as `profiling` is meant for building ty/ruff
# for running benchmarks.
#
# The main differences here are to avoid stripping debug information
# and disabling fat lto. This does result in a mismatch between our release
# configuration and our benchmarking configuration, which is unfortunate.
# But compile times with `lto = fat` are completely untenable.
#
# This setup does risk that we are measuring something in benchmarks
# that we aren't shipping, but in order to make those two the same, we'd
# either need to make compile times way worse for development, or take
# a hit to binary size and a slight hit to runtime performance in our
# release builds.
#
# Use the `--profile profiling` flag to show symbols in release mode.
# e.g. `cargo build --profile profiling`
[profile.profiling]
inherits = "release"
debug = 1
strip = false
debug = "full"
lto = false
# The profile that 'cargo dist' will build with.
[profile.dist]

View File

@@ -28,7 +28,7 @@ An extremely fast Python linter and code formatter, written in Rust.
- ⚡️ 10-100x faster than existing linters (like Flake8) and formatters (like Black)
- 🐍 Installable via `pip`
- 🛠️ `pyproject.toml` support
- 🤝 Python 3.13 compatibility
- 🤝 Python 3.14 compatibility
- ⚖️ Drop-in parity with [Flake8](https://docs.astral.sh/ruff/faq/#how-does-ruffs-linter-compare-to-flake8), isort, and [Black](https://docs.astral.sh/ruff/faq/#how-does-ruffs-formatter-compare-to-black)
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports)

View File

@@ -7,7 +7,8 @@ use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use self::render::{
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
DisplayDiagnostic, DisplayDiagnostics, DummyFileResolver, FileResolver, Input,
ceil_char_boundary,
github::{DisplayGithubDiagnostics, GithubRenderer},
};
use crate::{Db, files::File};

View File

@@ -1170,6 +1170,31 @@ pub fn ceil_char_boundary(text: &str, offset: TextSize) -> TextSize {
.unwrap_or_else(|| TextSize::from(upper_bound))
}
/// A stub implementation of [`FileResolver`] intended for testing.
pub struct DummyFileResolver;
impl FileResolver for DummyFileResolver {
fn path(&self, _file: File) -> &str {
unimplemented!()
}
fn input(&self, _file: File) -> Input {
unimplemented!()
}
fn notebook_index(&self, _file: &UnifiedFile) -> Option<NotebookIndex> {
None
}
fn is_notebook(&self, _file: &UnifiedFile) -> bool {
false
}
fn current_directory(&self) -> &Path {
Path::new(".")
}
}
#[cfg(test)]
mod tests {

View File

@@ -93,14 +93,39 @@ fn generate_markdown() -> String {
})
.join("\n");
let status_text = match lint.status() {
ty_python_semantic::lint::LintStatus::Stable { since } => {
format!(
r#"Added in <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>"#
)
}
ty_python_semantic::lint::LintStatus::Preview { since } => {
format!(
r#"Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
)
}
ty_python_semantic::lint::LintStatus::Deprecated { since, .. } => {
format!(
r#"Deprecated (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
)
}
ty_python_semantic::lint::LintStatus::Removed { since, .. } => {
format!(
r#"Removed (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
)
}
};
let _ = writeln!(
&mut output,
r#"<small>
Default level: [`{level}`](../rules.md#rule-levels "This lint has a default level of '{level}'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}) ·
[View source](https://github.com/astral-sh/ruff/blob/main/{file}#L{line})
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of '{level}'."><code>{level}</code></a> ·
{status_text} ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/{file}#L{line}" target="_blank">View source</a>
</small>
{documentation}
"#,
level = lint.default_level(),

View File

@@ -33,3 +33,10 @@ class ShellConfig:
def run(self, username):
Popen("true", shell={**self.shell_defaults, **self.fetch_shell_config(username)})
# Additional truthiness cases for generator, lambda, and f-strings
Popen("true", shell=(i for i in ()))
Popen("true", shell=lambda: 0)
Popen("true", shell=f"{b''}")
x = 1
Popen("true", shell=f"{x=}")

View File

@@ -6,3 +6,19 @@ foo(shell=True)
foo(shell={**{}})
foo(shell={**{**{}}})
# Truthy non-bool values for `shell`
foo(shell=(i for i in ()))
foo(shell=lambda: 0)
# f-strings guaranteed non-empty
foo(shell=f"{b''}")
x = 1
foo(shell=f"{x=}")
# Additional truthiness cases for generator, lambda, and f-strings
foo(shell=(i for i in ()))
foo(shell=lambda: 0)
foo(shell=f"{b''}")
x = 1
foo(shell=f"{x=}")

View File

@@ -9,3 +9,10 @@ os.system("tar cf foo.tar bar/*")
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{**{}}})
# Additional truthiness cases for generator, lambda, and f-strings
subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
subprocess.Popen("chmod +w foo*", shell=lambda: 0)
subprocess.Popen("chmod +w foo*", shell=f"{b''}")
x = 1
subprocess.Popen("chmod +w foo*", shell=f"{x=}")

View File

@@ -1,10 +1,10 @@
# The lexer doesn't emit a string token if it's unterminated
# The lexer emits a string token if it's unterminated
"a" "b
"a" "b" "c
"a" """b
c""" "d
# For f-strings, the `FStringRanges` won't contain the range for
# This is also true for
# unterminated f-strings.
f"a" f"b
f"a" f"b" f"c

View File

@@ -197,3 +197,10 @@ for x in {**a, **b} or [None]:
# https://github.com/astral-sh/ruff/issues/7127
def f(a: "'b' or 'c'"): ...
# https://github.com/astral-sh/ruff/issues/20703
print(f"{b''}" or "bar") # SIM222
x = 1
print(f"{x=}" or "bar") # SIM222
(lambda: 1) or True # SIM222
(i for i in range(1)) or "bar" # SIM222

View File

@@ -18,3 +18,20 @@ def print_third_word(word: Hello.Text) -> None:
def print_fourth_word(word: Goodbye) -> None:
print(word)
import typing_extensions
import typing_extensions as TypingExt
from typing_extensions import Text as TextAlias
def print_fifth_word(word: typing_extensions.Text) -> None:
print(word)
def print_sixth_word(word: TypingExt.Text) -> None:
print(word)
def print_seventh_word(word: TextAlias) -> None:
print(word)

View File

@@ -50,15 +50,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::nonlocal_and_global(checker, nonlocal);
}
}
Stmt::Break(_) => {
if checker.is_rule_enabled(Rule::BreakOutsideLoop) {
pyflakes::rules::break_outside_loop(
checker,
stmt,
&mut checker.semantic.current_statements().skip(1),
);
}
}
Stmt::Continue(_) => {
if checker.is_rule_enabled(Rule::ContinueOutsideLoop) {
pyflakes::rules::continue_outside_loop(

View File

@@ -697,6 +697,7 @@ impl SemanticSyntaxContext for Checker<'_> {
}
}
SemanticSyntaxErrorKind::FutureFeatureNotDefined(name) => {
// F407
if self.is_rule_enabled(Rule::FutureFeatureNotDefined) {
self.report_diagnostic(
pyflakes::rules::FutureFeatureNotDefined { name },
@@ -704,6 +705,12 @@ impl SemanticSyntaxContext for Checker<'_> {
);
}
}
SemanticSyntaxErrorKind::BreakOutsideLoop => {
// F701
if self.is_rule_enabled(Rule::BreakOutsideLoop) {
self.report_diagnostic(pyflakes::rules::BreakOutsideLoop, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
@@ -811,19 +818,40 @@ impl SemanticSyntaxContext for Checker<'_> {
}
)
}
fn in_loop_context(&self) -> bool {
let mut child = self.semantic.current_statement();
for parent in self.semantic.current_statements().skip(1) {
match parent {
Stmt::For(ast::StmtFor { orelse, .. })
| Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return true;
}
}
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
break;
}
_ => {}
}
child = parent;
}
false
}
}
impl<'a> Visitor<'a> for Checker<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
// Step 0: Pre-processing
self.semantic.push_node(stmt);
// For functions, defer semantic syntax error checks until the body of the function is
// visited
if !stmt.is_function_def_stmt() {
self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context));
}
// Step 0: Pre-processing
self.semantic.push_node(stmt);
// For Jupyter Notebooks, we'll reset the `IMPORT_BOUNDARY` flag when
// we encounter a cell boundary.
if self.source_type.is_ipynb()

View File

@@ -11,8 +11,7 @@ use crate::settings::types::CompiledPerFileIgnoreList;
pub fn get_cwd() -> &'static Path {
#[cfg(target_arch = "wasm32")]
{
static CWD: std::sync::LazyLock<PathBuf> = std::sync::LazyLock::new(|| PathBuf::from("."));
&CWD
Path::new(".")
}
#[cfg(not(target_arch = "wasm32"))]
path_absolutize::path_dedot::CWD.as_path()

View File

@@ -265,3 +265,7 @@ pub(crate) const fn is_fix_read_whole_file_enabled(settings: &LinterSettings) ->
pub(crate) const fn is_fix_write_whole_file_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -127,3 +127,44 @@ S602 `subprocess` call with `shell=True` identified, security issue
21 |
22 | # Check dict display with only double-starred expressions can be falsey.
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:38:1
|
37 | # Additional truthiness cases for generator, lambda, and f-strings
38 | Popen("true", shell=(i for i in ()))
| ^^^^^
39 | Popen("true", shell=lambda: 0)
40 | Popen("true", shell=f"{b''}")
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:39:1
|
37 | # Additional truthiness cases for generator, lambda, and f-strings
38 | Popen("true", shell=(i for i in ()))
39 | Popen("true", shell=lambda: 0)
| ^^^^^
40 | Popen("true", shell=f"{b''}")
41 | x = 1
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:40:1
|
38 | Popen("true", shell=(i for i in ()))
39 | Popen("true", shell=lambda: 0)
40 | Popen("true", shell=f"{b''}")
| ^^^^^
41 | x = 1
42 | Popen("true", shell=f"{x=}")
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:42:1
|
40 | Popen("true", shell=f"{b''}")
41 | x = 1
42 | Popen("true", shell=f"{x=}")
| ^^^^^
|

View File

@@ -9,3 +9,85 @@ S604 Function call with `shell=True` parameter identified, security issue
6 |
7 | foo(shell={**{}})
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:11:1
|
10 | # Truthy non-bool values for `shell`
11 | foo(shell=(i for i in ()))
| ^^^
12 | foo(shell=lambda: 0)
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:12:1
|
10 | # Truthy non-bool values for `shell`
11 | foo(shell=(i for i in ()))
12 | foo(shell=lambda: 0)
| ^^^
13 |
14 | # f-strings guaranteed non-empty
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:15:1
|
14 | # f-strings guaranteed non-empty
15 | foo(shell=f"{b''}")
| ^^^
16 | x = 1
17 | foo(shell=f"{x=}")
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:17:1
|
15 | foo(shell=f"{b''}")
16 | x = 1
17 | foo(shell=f"{x=}")
| ^^^
18 |
19 | # Additional truthiness cases for generator, lambda, and f-strings
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:20:1
|
19 | # Additional truthiness cases for generator, lambda, and f-strings
20 | foo(shell=(i for i in ()))
| ^^^
21 | foo(shell=lambda: 0)
22 | foo(shell=f"{b''}")
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:21:1
|
19 | # Additional truthiness cases for generator, lambda, and f-strings
20 | foo(shell=(i for i in ()))
21 | foo(shell=lambda: 0)
| ^^^
22 | foo(shell=f"{b''}")
23 | x = 1
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:22:1
|
20 | foo(shell=(i for i in ()))
21 | foo(shell=lambda: 0)
22 | foo(shell=f"{b''}")
| ^^^
23 | x = 1
24 | foo(shell=f"{x=}")
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:24:1
|
22 | foo(shell=f"{b''}")
23 | x = 1
24 | foo(shell=f"{x=}")
| ^^^
|

View File

@@ -43,3 +43,44 @@ S609 Possible wildcard injection in call due to `*` usage
9 |
10 | subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:14:18
|
13 | # Additional truthiness cases for generator, lambda, and f-strings
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
| ^^^^^^^^^^^^^^^
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:15:18
|
13 | # Additional truthiness cases for generator, lambda, and f-strings
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
| ^^^^^^^^^^^^^^^
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
17 | x = 1
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:16:18
|
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
| ^^^^^^^^^^^^^^^
17 | x = 1
18 | subprocess.Popen("chmod +w foo*", shell=f"{x=}")
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:18:18
|
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
17 | x = 1
18 | subprocess.Popen("chmod +w foo*", shell=f"{x=}")
| ^^^^^^^^^^^^^^^
|

View File

@@ -188,16 +188,10 @@ fn move_initialization(
content.push_str(stylist.line_ending().as_str());
content.push_str(stylist.indentation());
if is_b006_unsafe_fix_preserve_assignment_expr_enabled(checker.settings()) {
let annotation = if let Some(ann) = parameter.annotation() {
format!(": {}", locator.slice(ann))
} else {
String::new()
};
let _ = write!(
&mut content,
"{}{} = {}",
"{} = {}",
parameter.parameter.name(),
annotation,
locator.slice(
parenthesized_range(
default.into(),

View File

@@ -16,7 +16,7 @@ help: Replace with `None`; initialize within function
5 + def import_module_wrong(value: dict[str, str] = None):
6 | import os
7 + if value is None:
8 + value: dict[str, str] = {}
8 + value = {}
9 |
10 |
11 | def import_module_with_values_wrong(value: dict[str, str] = {}):
@@ -38,7 +38,7 @@ help: Replace with `None`; initialize within function
10 | import os
11 |
12 + if value is None:
13 + value: dict[str, str] = {}
13 + value = {}
14 | return 2
15 |
16 |
@@ -62,7 +62,7 @@ help: Replace with `None`; initialize within function
17 | import sys
18 | import itertools
19 + if value is None:
20 + value: dict[str, str] = {}
20 + value = {}
21 |
22 |
23 | def from_import_module_wrong(value: dict[str, str] = {}):
@@ -83,7 +83,7 @@ help: Replace with `None`; initialize within function
21 + def from_import_module_wrong(value: dict[str, str] = None):
22 | from os import path
23 + if value is None:
24 + value: dict[str, str] = {}
24 + value = {}
25 |
26 |
27 | def from_imports_module_wrong(value: dict[str, str] = {}):
@@ -106,7 +106,7 @@ help: Replace with `None`; initialize within function
26 | from os import path
27 | from sys import version_info
28 + if value is None:
29 + value: dict[str, str] = {}
29 + value = {}
30 |
31 |
32 | def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
@@ -129,7 +129,7 @@ help: Replace with `None`; initialize within function
31 | import os
32 | from sys import version_info
33 + if value is None:
34 + value: dict[str, str] = {}
34 + value = {}
35 |
36 |
37 | def import_docstring_module_wrong(value: dict[str, str] = {}):
@@ -152,7 +152,7 @@ help: Replace with `None`; initialize within function
36 | """Docstring"""
37 | import os
38 + if value is None:
39 + value: dict[str, str] = {}
39 + value = {}
40 |
41 |
42 | def import_module_wrong(value: dict[str, str] = {}):
@@ -175,7 +175,7 @@ help: Replace with `None`; initialize within function
41 | """Docstring"""
42 | import os; import sys
43 + if value is None:
44 + value: dict[str, str] = {}
44 + value = {}
45 |
46 |
47 | def import_module_wrong(value: dict[str, str] = {}):
@@ -197,7 +197,7 @@ help: Replace with `None`; initialize within function
45 + def import_module_wrong(value: dict[str, str] = None):
46 | """Docstring"""
47 + if value is None:
48 + value: dict[str, str] = {}
48 + value = {}
49 | import os; import sys; x = 1
50 |
51 |
@@ -220,7 +220,7 @@ help: Replace with `None`; initialize within function
51 | """Docstring"""
52 | import os; import sys
53 + if value is None:
54 + value: dict[str, str] = {}
54 + value = {}
55 |
56 |
57 | def import_module_wrong(value: dict[str, str] = {}):
@@ -241,7 +241,7 @@ help: Replace with `None`; initialize within function
55 + def import_module_wrong(value: dict[str, str] = None):
56 | import os; import sys
57 + if value is None:
58 + value: dict[str, str] = {}
58 + value = {}
59 |
60 |
61 | def import_module_wrong(value: dict[str, str] = {}):
@@ -261,7 +261,7 @@ help: Replace with `None`; initialize within function
- def import_module_wrong(value: dict[str, str] = {}):
59 + def import_module_wrong(value: dict[str, str] = None):
60 + if value is None:
61 + value: dict[str, str] = {}
61 + value = {}
62 | import os; import sys; x = 1
63 |
64 |
@@ -282,7 +282,7 @@ help: Replace with `None`; initialize within function
63 + def import_module_wrong(value: dict[str, str] = None):
64 | import os; import sys
65 + if value is None:
66 + value: dict[str, str] = {}
66 + value = {}
67 |
68 |
69 | def import_module_wrong(value: dict[str, str] = {}): import os

View File

@@ -51,7 +51,7 @@ help: Replace with `None`; initialize within function
10 + def baz(a: list = None):
11 | """This one raises a different exception"""
12 + if a is None:
13 + a: list = []
13 + a = []
14 | raise IndexError()
15 |
16 |

View File

@@ -1,22 +1,23 @@
---
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
---
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:2:5
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:2:1
|
1 | # The lexer doesn't emit a string token if it's unterminated
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
| ^^
| ^^^^^^
3 | "a" "b" "c
4 | "a" """b
|
help: Combine string literals
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:2:7
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:2:5
|
1 | # The lexer doesn't emit a string token if it's unterminated
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
| ^
| ^^
3 | "a" "b" "c
4 | "a" """b
|
@@ -24,7 +25,7 @@ invalid-syntax: Expected a statement
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:3:1
|
1 | # The lexer doesn't emit a string token if it's unterminated
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^^^^^^
@@ -33,24 +34,25 @@ ISC001 Implicitly concatenated string literals on one line
|
help: Combine string literals
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:3:9
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:3:5
|
1 | # The lexer doesn't emit a string token if it's unterminated
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^
| ^^^^^^
4 | "a" """b
5 | c""" "d
|
help: Combine string literals
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:3:11
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:3:9
|
1 | # The lexer doesn't emit a string token if it's unterminated
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^
| ^^
4 | "a" """b
5 | c""" "d
|
@@ -64,7 +66,21 @@ ISC001 Implicitly concatenated string literals on one line
5 | | c""" "d
| |____^
6 |
7 | # For f-strings, the `FStringRanges` won't contain the range for
7 | # This is also true for
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:4:5
|
2 | "a" "b
3 | "a" "b" "c
4 | "a" """b
| _____^
5 | | c""" "d
| |_______^
6 |
7 | # This is also true for
|
help: Combine string literals
@@ -76,24 +92,13 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
| ^^
6 |
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:5:8
|
3 | "a" "b" "c
4 | "a" """b
5 | c""" "d
| ^
6 |
7 | # For f-strings, the `FStringRanges` won't contain the range for
7 | # This is also true for
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:9:8
|
7 | # For f-strings, the `FStringRanges` won't contain the range for
7 | # This is also true for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -104,7 +109,7 @@ invalid-syntax: f-string: unterminated string
invalid-syntax: Expected FStringEnd, found newline
--> ISC_syntax_error.py:9:9
|
7 | # For f-strings, the `FStringRanges` won't contain the range for
7 | # This is also true for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -183,14 +188,6 @@ invalid-syntax: f-string: unterminated triple-quoted string
| |__^
|
invalid-syntax: unexpected EOF while parsing
--> ISC_syntax_error.py:30:1
|
28 | "i" "j"
29 | )
| ^
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:30:1
|

View File

@@ -4,27 +4,17 @@ source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:2:5
|
1 | # The lexer doesn't emit a string token if it's unterminated
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
| ^^
3 | "a" "b" "c
4 | "a" """b
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:2:7
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
| ^
3 | "a" "b" "c
4 | "a" """b
|
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:3:9
|
1 | # The lexer doesn't emit a string token if it's unterminated
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^
@@ -32,17 +22,6 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:3:11
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^
4 | "a" """b
5 | c""" "d
|
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:5:6
|
@@ -51,24 +30,13 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
| ^^
6 |
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:5:8
|
3 | "a" "b" "c
4 | "a" """b
5 | c""" "d
| ^
6 |
7 | # For f-strings, the `FStringRanges` won't contain the range for
7 | # This is also true for
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:9:8
|
7 | # For f-strings, the `FStringRanges` won't contain the range for
7 | # This is also true for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -79,7 +47,7 @@ invalid-syntax: f-string: unterminated string
invalid-syntax: Expected FStringEnd, found newline
--> ISC_syntax_error.py:9:9
|
7 | # For f-strings, the `FStringRanges` won't contain the range for
7 | # This is also true for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -133,14 +101,6 @@ invalid-syntax: f-string: unterminated triple-quoted string
| |__^
|
invalid-syntax: unexpected EOF while parsing
--> ISC_syntax_error.py:30:1
|
28 | "i" "j"
29 | )
| ^
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:30:1
|

View File

@@ -1062,3 +1062,77 @@ help: Replace with `"bar"`
170 |
171 |
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `f"{b''}"` instead of `f"{b''}" or ...`
--> SIM222.py:202:7
|
201 | # https://github.com/astral-sh/ruff/issues/20703
202 | print(f"{b''}" or "bar") # SIM222
| ^^^^^^^^^^^^^^^^^
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
|
help: Replace with `f"{b''}"`
199 | def f(a: "'b' or 'c'"): ...
200 |
201 | # https://github.com/astral-sh/ruff/issues/20703
- print(f"{b''}" or "bar") # SIM222
202 + print(f"{b''}") # SIM222
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `f"{x=}"` instead of `f"{x=}" or ...`
--> SIM222.py:204:7
|
202 | print(f"{b''}" or "bar") # SIM222
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
| ^^^^^^^^^^^^^^^^
205 | (lambda: 1) or True # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
|
help: Replace with `f"{x=}"`
201 | # https://github.com/astral-sh/ruff/issues/20703
202 | print(f"{b''}" or "bar") # SIM222
203 | x = 1
- print(f"{x=}" or "bar") # SIM222
204 + print(f"{x=}") # SIM222
205 | (lambda: 1) or True # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `lambda: 1` instead of `lambda: 1 or ...`
--> SIM222.py:205:1
|
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
| ^^^^^^^^^^^^^^^^^^^
206 | (i for i in range(1)) or "bar" # SIM222
|
help: Replace with `lambda: 1`
202 | print(f"{b''}" or "bar") # SIM222
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
- (lambda: 1) or True # SIM222
205 + lambda: 1 # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `(i for i in range(1))` instead of `(i for i in range(1)) or ...`
--> SIM222.py:206:1
|
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with `(i for i in range(1))`
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
- (i for i in range(1)) or "bar" # SIM222
206 + (i for i in range(1)) # SIM222
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,9 +1,6 @@
use ruff_python_ast::{self as ast, Stmt};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_text_size::Ranged;
use crate::{Violation, checkers::ast::Checker};
use crate::Violation;
/// ## What it does
/// Checks for `break` statements outside of loops.
@@ -29,28 +26,3 @@ impl Violation for BreakOutsideLoop {
"`break` outside loop".to_string()
}
}
/// F701
pub(crate) fn break_outside_loop<'a>(
checker: &Checker,
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) {
let mut child = stmt;
for parent in parents {
match parent {
Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return;
}
}
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
break;
}
_ => {}
}
child = parent;
}
checker.report_diagnostic(BreakOutsideLoop, stmt.range());
}

View File

@@ -23,16 +23,17 @@ invalid-syntax: missing closing quote in string literal
9 | # Unterminated f-string
|
invalid-syntax: Expected a statement
--> invalid_characters_syntax_error.py:7:7
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:7:6
|
5 | b = '␈'
6 | # Unterminated string
7 | b = '␈
| ^
| ^
8 | b = '␈'
9 | # Unterminated f-string
|
help: Replace with escape sequence
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:8:6
@@ -46,6 +47,18 @@ PLE2510 Invalid unescaped character backspace, use "\b" instead
|
help: Replace with escape sequence
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:10:7
|
8 | b = '␈'
9 | # Unterminated f-string
10 | b = f'␈
| ^
11 | b = f'␈'
12 | # Implicitly concatenated
|
help: Replace with escape sequence
invalid-syntax: f-string: unterminated string
--> invalid_characters_syntax_error.py:10:7
|
@@ -109,11 +122,12 @@ invalid-syntax: missing closing quote in string literal
| ^^
|
invalid-syntax: Expected a statement
--> invalid_characters_syntax_error.py:13:16
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:13:15
|
11 | b = f'␈'
12 | # Implicitly concatenated
13 | b = '␈' f'␈' '␈
| ^
| ^
|
help: Replace with escape sequence

View File

@@ -126,6 +126,7 @@ mod tests {
}
#[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))]
#[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))]
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}__preview", path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -6,7 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_ast::{self as ast, Expr, Keyword};
use ruff_python_ast::{self as ast, Expr, Keyword, StringFlags};
use ruff_python_literal::format::{
FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate,
};
@@ -430,7 +430,7 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma
// dot is the start of an attribute access.
break token.start();
}
TokenKind::String => {
TokenKind::String if !token.unwrap_string_flags().is_unclosed() => {
match FStringConversion::try_convert(token.range(), &mut summary, checker.locator())
{
// If the format string contains side effects that would need to be repeated,

View File

@@ -1,15 +1,19 @@
use ruff_python_ast::Expr;
use std::fmt::{Display, Formatter};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_typing_extensions_str_alias_enabled;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `typing.Text`.
///
/// In preview mode, also checks for `typing_extensions.Text`.
///
/// ## Why is this bad?
/// `typing.Text` is an alias for `str`, and only exists for Python 2
/// compatibility. As of Python 3.11, `typing.Text` is deprecated. Use `str`
@@ -30,14 +34,16 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ## References
/// - [Python documentation: `typing.Text`](https://docs.python.org/3/library/typing.html#typing.Text)
#[derive(ViolationMetadata)]
pub(crate) struct TypingTextStrAlias;
pub(crate) struct TypingTextStrAlias {
module: TypingModule,
}
impl Violation for TypingTextStrAlias {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`typing.Text` is deprecated, use `str`".to_string()
format!("`{}.Text` is deprecated, use `str`", self.module)
}
fn fix_title(&self) -> Option<String> {
@@ -47,16 +53,26 @@ impl Violation for TypingTextStrAlias {
/// UP019
pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::TYPING) {
if !checker
.semantic()
.seen_module(Modules::TYPING | Modules::TYPING_EXTENSIONS)
{
return;
}
if checker
.semantic()
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"]))
{
let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias, expr.range());
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) {
let segments = qualified_name.segments();
let module = match segments {
["typing", "Text"] => TypingModule::Typing,
["typing_extensions", "Text"]
if is_typing_extensions_str_alias_enabled(checker.settings()) =>
{
TypingModule::TypingExtensions
}
_ => return,
};
let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias { module }, expr.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
@@ -71,3 +87,18 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) {
});
}
}
#[derive(Copy, Clone, Debug)]
enum TypingModule {
Typing,
TypingExtensions,
}
impl Display for TypingModule {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TypingModule::Typing => f.write_str("typing"),
TypingModule::TypingExtensions => f.write_str("typing_extensions"),
}
}
}

View File

@@ -66,3 +66,5 @@ help: Replace with `str`
- def print_fourth_word(word: Goodbye) -> None:
19 + def print_fourth_word(word: str) -> None:
20 | print(word)
21 |
22 |

View File

@@ -0,0 +1,119 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:7:22
|
7 | def print_word(word: Text) -> None:
| ^^^^
8 | print(word)
|
help: Replace with `str`
4 | from typing import Text as Goodbye
5 |
6 |
- def print_word(word: Text) -> None:
7 + def print_word(word: str) -> None:
8 | print(word)
9 |
10 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:11:29
|
11 | def print_second_word(word: typing.Text) -> None:
| ^^^^^^^^^^^
12 | print(word)
|
help: Replace with `str`
8 | print(word)
9 |
10 |
- def print_second_word(word: typing.Text) -> None:
11 + def print_second_word(word: str) -> None:
12 | print(word)
13 |
14 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:15:28
|
15 | def print_third_word(word: Hello.Text) -> None:
| ^^^^^^^^^^
16 | print(word)
|
help: Replace with `str`
12 | print(word)
13 |
14 |
- def print_third_word(word: Hello.Text) -> None:
15 + def print_third_word(word: str) -> None:
16 | print(word)
17 |
18 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:19:29
|
19 | def print_fourth_word(word: Goodbye) -> None:
| ^^^^^^^
20 | print(word)
|
help: Replace with `str`
16 | print(word)
17 |
18 |
- def print_fourth_word(word: Goodbye) -> None:
19 + def print_fourth_word(word: str) -> None:
20 | print(word)
21 |
22 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:28:28
|
28 | def print_fifth_word(word: typing_extensions.Text) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^
29 | print(word)
|
help: Replace with `str`
25 | from typing_extensions import Text as TextAlias
26 |
27 |
- def print_fifth_word(word: typing_extensions.Text) -> None:
28 + def print_fifth_word(word: str) -> None:
29 | print(word)
30 |
31 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:32:28
|
32 | def print_sixth_word(word: TypingExt.Text) -> None:
| ^^^^^^^^^^^^^^
33 | print(word)
|
help: Replace with `str`
29 | print(word)
30 |
31 |
- def print_sixth_word(word: TypingExt.Text) -> None:
32 + def print_sixth_word(word: str) -> None:
33 | print(word)
34 |
35 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:36:30
|
36 | def print_seventh_word(word: TextAlias) -> None:
| ^^^^^^^^^
37 | print(word)
|
help: Replace with `str`
33 | print(word)
34 |
35 |
- def print_seventh_word(word: TextAlias) -> None:
36 + def print_seventh_word(word: str) -> None:
37 | print(word)

View File

@@ -48,7 +48,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
/// element. As such, any side effects that occur during iteration will be
/// delayed.
/// 2. Second, accessing members of a collection via square bracket notation
/// `[0]` of the `pop()` function will raise `IndexError` if the collection
/// `[0]` or the `pop()` function will raise `IndexError` if the collection
/// is empty, while `next(iter(...))` will raise `StopIteration`.
///
/// ## References

View File

@@ -12,7 +12,7 @@ use crate::parenthesize::parenthesized_range;
use crate::statement_visitor::StatementVisitor;
use crate::visitor::Visitor;
use crate::{
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr,
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, ExprNoneLiteral,
InterpolatedStringElement, MatchCase, Operator, Pattern, Stmt, TypeParam,
};
use crate::{AnyNodeRef, ExprContext};
@@ -1219,6 +1219,8 @@ impl Truthiness {
F: Fn(&str) -> bool,
{
match expr {
Expr::Lambda(_) => Self::Truthy,
Expr::Generator(_) => Self::Truthy,
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if value.is_empty() {
Self::Falsey
@@ -1388,7 +1390,9 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
Expr::FString(f_string) => is_non_empty_f_string(f_string),
// These literals may or may not be empty.
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(),
// Confusingly, f"{b""}" renders as the string 'b""', which is non-empty.
// Therefore, any bytes interpolation is guaranteed non-empty when stringified.
Expr::BytesLiteral(_) => true,
}
}
@@ -1397,7 +1401,9 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(),
InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression),
InterpolatedStringElement::Interpolation(f_string) => {
f_string.debug_text.is_some() || inner(&f_string.expression)
}
})
}
})
@@ -1493,7 +1499,7 @@ pub fn pep_604_optional(expr: &Expr) -> Expr {
ast::ExprBinOp {
left: Box::new(expr.clone()),
op: Operator::BitOr,
right: Box::new(Expr::NoneLiteral(ast::ExprNoneLiteral::default())),
right: Box::new(Expr::NoneLiteral(ExprNoneLiteral::default())),
range: TextRange::default(),
node_index: AtomicNodeIndex::NONE,
}

View File

@@ -735,6 +735,8 @@ pub trait StringFlags: Copy {
fn prefix(self) -> AnyStringPrefix;
fn is_unclosed(self) -> bool;
/// Is the string triple-quoted, i.e.,
/// does it begin and end with three consecutive quote characters?
fn is_triple_quoted(self) -> bool {
@@ -779,6 +781,7 @@ pub trait StringFlags: Copy {
fn as_any_string_flags(self) -> AnyStringFlags {
AnyStringFlags::new(self.prefix(), self.quote_style(), self.triple_quotes())
.with_unclosed(self.is_unclosed())
}
fn display_contents(self, contents: &str) -> DisplayFlags<'_> {
@@ -829,6 +832,10 @@ bitflags! {
/// for why we track the casing of the `r` prefix,
/// but not for any other prefix
const R_PREFIX_UPPER = 1 << 3;
/// The f-string is unclosed, meaning it is missing a closing quote.
/// For example: `f"{bar`
const UNCLOSED = 1 << 4;
}
}
@@ -887,6 +894,12 @@ impl FStringFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(InterpolatedStringFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: FStringPrefix) -> Self {
match prefix {
@@ -984,6 +997,12 @@ impl TStringFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(InterpolatedStringFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: TStringPrefix) -> Self {
match prefix {
@@ -1051,6 +1070,10 @@ impl StringFlags for FStringFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Format(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(InterpolatedStringFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for FStringFlags {
@@ -1059,6 +1082,7 @@ impl fmt::Debug for FStringFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -1090,6 +1114,10 @@ impl StringFlags for TStringFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Template(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(InterpolatedStringFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for TStringFlags {
@@ -1098,6 +1126,7 @@ impl fmt::Debug for TStringFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -1427,6 +1456,9 @@ bitflags! {
/// The string was deemed invalid by the parser.
const INVALID = 1 << 5;
/// The string literal misses the matching closing quote(s).
const UNCLOSED = 1 << 6;
}
}
@@ -1479,6 +1511,12 @@ impl StringLiteralFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(StringLiteralFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(self, prefix: StringLiteralPrefix) -> Self {
let StringLiteralFlags(flags) = self;
@@ -1560,6 +1598,10 @@ impl StringFlags for StringLiteralFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Regular(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(StringLiteralFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for StringLiteralFlags {
@@ -1568,6 +1610,7 @@ impl fmt::Debug for StringLiteralFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -1846,6 +1889,9 @@ bitflags! {
/// The bytestring was deemed invalid by the parser.
const INVALID = 1 << 4;
/// The byte string misses the matching closing quote(s).
const UNCLOSED = 1 << 5;
}
}
@@ -1897,6 +1943,12 @@ impl BytesLiteralFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(BytesLiteralFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: ByteStringPrefix) -> Self {
match prefix {
@@ -1959,6 +2011,10 @@ impl StringFlags for BytesLiteralFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Bytes(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(BytesLiteralFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for BytesLiteralFlags {
@@ -1967,6 +2023,7 @@ impl fmt::Debug for BytesLiteralFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -2028,7 +2085,7 @@ bitflags! {
/// prefix flags is by calling the `as_flags()` method on the
/// `StringPrefix` enum.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
struct AnyStringFlagsInner: u8 {
struct AnyStringFlagsInner: u16 {
/// The string uses double quotes (`"`).
/// If this flag is not set, the string uses single quotes (`'`).
const DOUBLE = 1 << 0;
@@ -2071,6 +2128,9 @@ bitflags! {
/// for why we track the casing of the `r` prefix,
/// but not for any other prefix
const R_PREFIX_UPPER = 1 << 7;
/// String without matching closing quote(s).
const UNCLOSED = 1 << 8;
}
}
@@ -2166,6 +2226,12 @@ impl AnyStringFlags {
.set(AnyStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes());
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(AnyStringFlagsInner::UNCLOSED, unclosed);
self
}
}
impl StringFlags for AnyStringFlags {
@@ -2234,6 +2300,10 @@ impl StringFlags for AnyStringFlags {
}
AnyStringPrefix::Regular(StringLiteralPrefix::Empty)
}
fn is_unclosed(self) -> bool {
self.0.intersects(AnyStringFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for AnyStringFlags {
@@ -2242,6 +2312,7 @@ impl fmt::Debug for AnyStringFlags {
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("quote_style", &self.quote_style())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -2258,6 +2329,7 @@ impl From<AnyStringFlags> for StringLiteralFlags {
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}
@@ -2279,6 +2351,7 @@ impl From<AnyStringFlags> for BytesLiteralFlags {
.with_quote_style(value.quote_style())
.with_prefix(bytestring_prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}
@@ -2300,6 +2373,7 @@ impl From<AnyStringFlags> for FStringFlags {
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}
@@ -2321,6 +2395,7 @@ impl From<AnyStringFlags> for TStringFlags {
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}

View File

@@ -0,0 +1,7 @@
# regression test for #1765
class Foo:
def foo(self):
if True:
content_ids: Mapping[
str, Optional[ContentId]
] = self.publisher_content_store.store_config_contents(files)

View File

@@ -0,0 +1,7 @@
# regression test for #1765
class Foo:
def foo(self):
if True:
content_ids: Mapping[str, Optional[ContentId]] = (
self.publisher_content_store.store_config_contents(files)
)

View File

@@ -1 +1 @@
{"target_version": "3.10"}
{"target_version": "3.10"}

View File

@@ -0,0 +1,35 @@
# long variable name
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 0
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 1 # with a comment
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = [
1, 2, 3
]
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function()
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
arg1, arg2, arg3
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
# long function name
normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying()
normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
arg1, arg2, arg3
)
normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
string_variable_name = (
"a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa
)
for key in """
hostname
port
username
""".split():
if key in self.connect_kwargs:
raise ValueError(err.format(key))
concatenated_strings = "some strings that are " "concatenated implicitly, so if you put them on separate " "lines it will fit"
del concatenated_strings, string_variable_name, normal_function_name, normal_name, need_more_to_make_the_line_long_enough
del ([], name_1, name_2), [(), [], name_4, name_3], name_1[[name_2 for name_1 in name_0]]
del (),

View File

@@ -0,0 +1,61 @@
# long variable name
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = (
0
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = (
1 # with a comment
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = [
1,
2,
3,
]
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = (
function()
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
arg1, arg2, arg3
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
# long function name
normal_name = (
but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying()
)
normal_name = (
but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
arg1, arg2, arg3
)
)
normal_name = (
but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
)
string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa
for key in """
hostname
port
username
""".split():
if key in self.connect_kwargs:
raise ValueError(err.format(key))
concatenated_strings = (
"some strings that are "
"concatenated implicitly, so if you put them on separate "
"lines it will fit"
)
del (
concatenated_strings,
string_variable_name,
normal_function_name,
normal_name,
need_more_to_make_the_line_long_enough,
)
del (
([], name_1, name_2),
[(), [], name_4, name_3],
name_1[[name_2 for name_1 in name_0]],
)
del ((),)

View File

@@ -1 +1 @@
{"target_version": "3.8"}
{"target_version": "3.8"}

View File

@@ -82,3 +82,9 @@ async def func():
argument1, argument2, argument3="some_value"
):
pass
# don't remove the brackets here, it changes the meaning of the code.
with (x, y) as z:
pass

View File

@@ -83,3 +83,8 @@ async def func():
some_other_function(argument1, argument2, argument3="some_value"),
):
pass
# don't remove the brackets here, it changes the meaning of the code.
with (x, y) as z:
pass

View File

@@ -0,0 +1,3 @@
"""
87 characters ............................................................................
"""

View File

@@ -0,0 +1,3 @@
"""
87 characters ............................................................................
"""

View File

@@ -0,0 +1,8 @@
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip

View File

@@ -0,0 +1,8 @@
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip

View File

@@ -0,0 +1,6 @@
def foo():
pass
# comment 1 # fmt: skip
# comment 2

View File

@@ -0,0 +1,6 @@
def foo():
pass
# comment 1 # fmt: skip
# comment 2

View File

@@ -0,0 +1,15 @@
x = "\x1F"
x = "\\x1B"
x = "\\\x1B"
x = "\U0001F60E"
x = "\u0001F60E"
x = r"\u0001F60E"
x = "don't format me"
x = "\xA3"
x = "\u2717"
x = "\uFaCe"
x = "\N{ox}\N{OX}"
x = "\N{lAtIn smaLL letteR x}"
x = "\N{CYRILLIC small LETTER BYELORUSSIAN-UKRAINIAN I}"
x = b"\x1Fdon't byte"
x = rb"\x1Fdon't format"

View File

@@ -0,0 +1,15 @@
x = "\x1f"
x = "\\x1B"
x = "\\\x1b"
x = "\U0001f60e"
x = "\u0001F60E"
x = r"\u0001F60E"
x = "don't format me"
x = "\xa3"
x = "\u2717"
x = "\uface"
x = "\N{OX}\N{OX}"
x = "\N{LATIN SMALL LETTER X}"
x = "\N{CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I}"
x = b"\x1fdon't byte"
x = rb"\x1Fdon't format"

View File

@@ -1 +0,0 @@
{"target_version": "3.12"}

View File

@@ -0,0 +1,34 @@
# Regression tests for long f-strings, including examples from issue #3623
a = (
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"'
)
a = (
f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
)
a = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + \
f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"'
a = f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"' + \
f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"'
a = (
f'bbbbbbb"{"b"}"'
'aaaaaaaa'
)
a = (
f'"{"b"}"'
)
a = (
f'\"{"b"}\"'
)
a = (
r'\"{"b"}\"'
)

View File

@@ -0,0 +1,29 @@
# Regression tests for long f-strings, including examples from issue #3623
a = (
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"'
)
a = (
f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"'
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
)
a = (
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"'
)
a = (
f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"'
+ f'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"{"b"}"'
)
a = f'bbbbbbb"{"b"}"' "aaaaaaaa"
a = f'"{"b"}"'
a = f'"{"b"}"'
a = r'\"{"b"}\"'

View File

@@ -1 +1 @@
{"preview": "enabled", "target_version": "3.10"}
{"target_version": "3.10"}

View File

@@ -0,0 +1 @@
{"target_version": "3.12"}

View File

@@ -0,0 +1,133 @@
def plain[T, B](a: T, b: T) -> T:
return a
def arg_magic[T, B](a: T, b: T,) -> T:
return a
def type_param_magic[T, B,](a: T, b: T) -> T:
return a
def both_magic[T, B,](a: T, b: T,) -> T:
return a
def plain_multiline[
T,
B
](
a: T,
b: T
) -> T:
return a
def arg_magic_multiline[
T,
B
](
a: T,
b: T,
) -> T:
return a
def type_param_magic_multiline[
T,
B,
](
a: T,
b: T
) -> T:
return a
def both_magic_multiline[
T,
B,
](
a: T,
b: T,
) -> T:
return a
def plain_mixed1[
T,
B
](a: T, b: T) -> T:
return a
def plain_mixed2[T, B](
a: T,
b: T
) -> T:
return a
def arg_magic_mixed1[
T,
B
](a: T, b: T,) -> T:
return a
def arg_magic_mixed2[T, B](
a: T,
b: T,
) -> T:
return a
def type_param_magic_mixed1[
T,
B,
](a: T, b: T) -> T:
return a
def type_param_magic_mixed2[T, B,](
a: T,
b: T
) -> T:
return a
def both_magic_mixed1[
T,
B,
](a: T, b: T,) -> T:
return a
def both_magic_mixed2[T, B,](
a: T,
b: T,
) -> T:
return a
def something_something_function[
T: Model
](param: list[int], other_param: type[T], *, some_other_param: bool = True) -> QuerySet[
T
]:
pass
def func[A_LOT_OF_GENERIC_TYPES: AreBeingDefinedHere, LIKE_THIS, AND_THIS, ANOTHER_ONE, AND_YET_ANOTHER_ONE: ThisOneHasTyping](a: T, b: T, c: T, d: T, e: T, f: T, g: T, h: T, i: T, j: T, k: T, l: T, m: T, n: T, o: T, p: T) -> T:
return a
def with_random_comments[
Z
# bye
]():
return a
def func[
T, # comment
U # comment
,
Z: # comment
int
](): pass
def func[
T, # comment but it's long so it doesn't just move to the end of the line
U # comment comment comm comm ent ent
,
Z: # comment ent ent comm comm comment
int
](): pass

View File

@@ -0,0 +1,170 @@
def plain[T, B](a: T, b: T) -> T:
return a
def arg_magic[T, B](
a: T,
b: T,
) -> T:
return a
def type_param_magic[
T,
B,
](
a: T, b: T
) -> T:
return a
def both_magic[
T,
B,
](
a: T,
b: T,
) -> T:
return a
def plain_multiline[T, B](a: T, b: T) -> T:
return a
def arg_magic_multiline[T, B](
a: T,
b: T,
) -> T:
return a
def type_param_magic_multiline[
T,
B,
](
a: T, b: T
) -> T:
return a
def both_magic_multiline[
T,
B,
](
a: T,
b: T,
) -> T:
return a
def plain_mixed1[T, B](a: T, b: T) -> T:
return a
def plain_mixed2[T, B](a: T, b: T) -> T:
return a
def arg_magic_mixed1[T, B](
a: T,
b: T,
) -> T:
return a
def arg_magic_mixed2[T, B](
a: T,
b: T,
) -> T:
return a
def type_param_magic_mixed1[
T,
B,
](
a: T, b: T
) -> T:
return a
def type_param_magic_mixed2[
T,
B,
](
a: T, b: T
) -> T:
return a
def both_magic_mixed1[
T,
B,
](
a: T,
b: T,
) -> T:
return a
def both_magic_mixed2[
T,
B,
](
a: T,
b: T,
) -> T:
return a
def something_something_function[T: Model](
param: list[int], other_param: type[T], *, some_other_param: bool = True
) -> QuerySet[T]:
pass
def func[
A_LOT_OF_GENERIC_TYPES: AreBeingDefinedHere,
LIKE_THIS,
AND_THIS,
ANOTHER_ONE,
AND_YET_ANOTHER_ONE: ThisOneHasTyping,
](
a: T,
b: T,
c: T,
d: T,
e: T,
f: T,
g: T,
h: T,
i: T,
j: T,
k: T,
l: T,
m: T,
n: T,
o: T,
p: T,
) -> T:
return a
def with_random_comments[
Z
# bye
]():
return a
def func[T, U, Z: int](): # comment # comment # comment
pass
def func[
T, # comment but it's long so it doesn't just move to the end of the line
U, # comment comment comm comm ent ent
Z: int, # comment ent ent comm comm comment
]():
pass

View File

@@ -6,7 +6,7 @@ def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parame
def foo3(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
# Adding some unformatted code covering a wide range of syntaxes.
# Adding some unformated code covering a wide range of syntaxes.
if True:
# Incorrectly indented prefix comments.

View File

@@ -28,7 +28,7 @@ def foo3(
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
# Adding some unformatted code covering a wide range of syntaxes.
# Adding some unformated code covering a wide range of syntaxes.
if True:
# Incorrectly indented prefix comments.

View File

@@ -0,0 +1,8 @@
# flags: --line-ranges=6-7
class Foo:
@overload
def foo(): ...
def fox(self):
print()

View File

@@ -0,0 +1,8 @@
# flags: --line-ranges=6-7
class Foo:
@overload
def foo(): ...
def fox(self):
print()

View File

@@ -0,0 +1,28 @@
def func(
arg1,
arg2,
) -> Set["this_is_a_very_long_module_name.AndAVeryLongClasName"
".WithAVeryVeryVeryVeryVeryLongSubClassName"]:
pass
def func(
argument: (
"VeryLongClassNameWithAwkwardGenericSubtype[int] |"
"VeryLongClassNameWithAwkwardGenericSubtype[str]"
),
) -> (
"VeryLongClassNameWithAwkwardGenericSubtype[int] |"
"VeryLongClassNameWithAwkwardGenericSubtype[str]"
):
pass
def func(
argument: (
"int |"
"str"
),
) -> Set["int |"
" str"]:
pass

View File

@@ -0,0 +1,26 @@
def func(
arg1,
arg2,
) -> Set[
"this_is_a_very_long_module_name.AndAVeryLongClasName"
".WithAVeryVeryVeryVeryVeryLongSubClassName"
]:
pass
def func(
argument: (
"VeryLongClassNameWithAwkwardGenericSubtype[int] |"
"VeryLongClassNameWithAwkwardGenericSubtype[str]"
),
) -> (
"VeryLongClassNameWithAwkwardGenericSubtype[int] |"
"VeryLongClassNameWithAwkwardGenericSubtype[str]"
):
pass
def func(
argument: "int |" "str",
) -> Set["int |" " str"]:
pass

View File

@@ -1,6 +1,6 @@
"""I am a very helpful module docstring.
With trailing spaces (only removed with unify_docstring_detection on):
With trailing spaces:
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam,

View File

@@ -1,6 +1,6 @@
"""I am a very helpful module docstring.
With trailing spaces (only removed with unify_docstring_detection on):
With trailing spaces:
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam,

View File

@@ -0,0 +1,9 @@
#!/python
# regression test for #4762
"""
docstring
"""
from __future__ import annotations
import os

View File

@@ -0,0 +1,10 @@
#!/python
# regression test for #4762
"""
docstring
"""
from __future__ import annotations
import os

View File

@@ -25,5 +25,4 @@ class MultilineDocstringsAsWell:
class SingleQuotedDocstring:
"I'm a docstring but I don't even get triple quotes."

View File

@@ -1 +1 @@
{"preview": "enabled", "target_version": "3.10"}
{"target_version": "3.10"}

View File

@@ -19,7 +19,7 @@ z: (Short
z: (int) = 2.3
z: ((int)) = foo()
# In case I go for not enforcing parentheses, this might get improved at the same time
# In case I go for not enforcing parantheses, this might get improved at the same time
x = (
z
== 9999999999999999999999999999999999999999

View File

@@ -28,7 +28,7 @@ z: Short | Short2 | Short3 | Short4 = 8
z: int = 2.3
z: int = foo()
# In case I go for not enforcing parentheses, this might get improved at the same time
# In case I go for not enforcing parantheses, this might get improved at the same time
x = (
z
== 9999999999999999999999999999999999999999

View File

@@ -0,0 +1 @@
{"target_version": "3.11"}

View File

@@ -0,0 +1,5 @@
def fn(*args: *tuple[*A, B]) -> None:
pass
fn.__annotations__

View File

@@ -0,0 +1,5 @@
def fn(*args: *tuple[*A, B]) -> None:
pass
fn.__annotations__

View File

@@ -13,3 +13,8 @@ f(a := b + c for c in range(10))
f((a := b + c for c in range(10)), x)
f(y=(a := b + c for c in range(10)))
f(x, (a := b + c for c in range(10)), y=z, **q)
# Don't remove parens when assignment expr is one of the exprs in a with statement
with x, (a := b):
pass

View File

@@ -13,3 +13,8 @@ f(a := b + c for c in range(10))
f((a := b + c for c in range(10)), x)
f(y=(a := b + c for c in range(10)))
f(x, (a := b + c for c in range(10)), y=z, **q)
# Don't remove parens when assignment expr is one of the exprs in a with statement
with x, (a := b):
pass

View File

@@ -73,8 +73,9 @@ x = f"a{2+2:=^{foo(x+y**2):something else}}b"
x = f"a{2+2:=^{foo(x+y**2):something else}one more}b"
f'{(abc:=10)}'
f"This is a really long string, but just make sure that you reflow fstrings {
2+2:d}"
f"""This is a really long string, but just make sure that you reflow fstrings {
2+2:d
}"""
f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}"
f"{2+2=}"

View File

@@ -73,9 +73,9 @@ x = f"a{2+2:=^{foo(x+y**2):something else}}b"
x = f"a{2+2:=^{foo(x+y**2):something else}one more}b"
f"{(abc:=10)}"
f"This is a really long string, but just make sure that you reflow fstrings {
f"""This is a really long string, but just make sure that you reflow fstrings {
2+2:d
}"
}"""
f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}"
f"{2+2=}"

View File

@@ -10,3 +10,12 @@ first_value, (m1, m2,), third_value = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvv
# Make when when the left side of assignment plus the opening paren "... = (" is
# exactly line length limit + 1, it won't be split like that.
xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1
# Regression test for #1187
print(
dict(
a=1,
b=2 if some_kind_of_data is not None else some_other_kind_of_data, # some explanation of why this is actually necessary
c=3,
)
)

View File

@@ -19,3 +19,14 @@
xxxxxxxxx_yyy_zzzzzzzz[
xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)
] = 1
# Regression test for #1187
print(
dict(
a=1,
b=(
2 if some_kind_of_data is not None else some_other_kind_of_data
), # some explanation of why this is actually necessary
c=3,
)
)

View File

@@ -29,7 +29,6 @@ from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component
MyLovelyCompanyTeamProjectComponent as component, # DRY
)
result = 1 # look ma, no comment migration xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
result = 1 # look ma, no comment migration xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

View File

@@ -0,0 +1 @@
f"{''=}" f'{""=}'

View File

@@ -0,0 +1 @@
f"{''=}" f'{""=}'

View File

@@ -0,0 +1,90 @@
from middleman.authentication import validate_oauth_token
logger = logging.getLogger(__name__)
# case 2 comment after import
from middleman.authentication import validate_oauth_token
#comment
logger = logging.getLogger(__name__)
# case 3 comment after import
from middleman.authentication import validate_oauth_token
# comment
logger = logging.getLogger(__name__)
from middleman.authentication import validate_oauth_token
logger = logging.getLogger(__name__)
# case 4 try catch with import after import
import os
import os
try:
import os
except Exception:
pass
try:
import os
def func():
a = 1
except Exception:
pass
# case 5 multiple imports
import os
import os
import os
import os
for i in range(10):
print(i)
# case 6 import in function
def func():
print()
import os
def func():
pass
print()
def func():
import os
a = 1
print()
def func():
import os
a = 1
print()
def func():
import os
a = 1
print()

View File

@@ -0,0 +1,85 @@
from middleman.authentication import validate_oauth_token
logger = logging.getLogger(__name__)
# case 2 comment after import
from middleman.authentication import validate_oauth_token
# comment
logger = logging.getLogger(__name__)
# case 3 comment after import
from middleman.authentication import validate_oauth_token
# comment
logger = logging.getLogger(__name__)
from middleman.authentication import validate_oauth_token
logger = logging.getLogger(__name__)
# case 4 try catch with import after import
import os
import os
try:
import os
except Exception:
pass
try:
import os
def func():
a = 1
except Exception:
pass
# case 5 multiple imports
import os
import os
import os
import os
for i in range(10):
print(i)
# case 6 import in function
def func():
print()
import os
def func():
pass
print()
def func():
import os
a = 1
print()
def func():
import os
a = 1
print()
def func():
import os
a = 1
print()

View File

@@ -1,3 +1,24 @@
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx"
)
}
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx"
),
}
x = {
"foo": bar,
"foo": bar,
"foo": (
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx
),
}
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxx"
}
my_dict = {
"something_something":
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
@@ -5,23 +26,90 @@ my_dict = {
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t",
}
# Function calls as keys
tasks = {
get_key_name(
foo,
bar,
baz,
): src,
loop.run_in_executor(): src,
loop.run_in_executor(xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx): src,
loop.run_in_executor(
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxx
): src,
loop.run_in_executor(): (
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx
),
}
# Dictionary comprehensions
tasks = {
key_name: (
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx
)
for src in sources
}
tasks = {key_name: foobar for src in sources}
tasks = {
get_key_name(
src,
): "foo"
for src in sources
}
tasks = {
get_key_name(
foo,
bar,
baz,
): src
for src in sources
}
tasks = {
get_key_name(): (
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx
)
for src in sources
}
tasks = {get_key_name(): foobar for src in sources}
# Delimiters inside the value
def foo():
def bar():
x = {
common.models.DateTimeField: datetime(2020, 1, 31, tzinfo=utc) + timedelta(
days=i
),
}
x = {
common.models.DateTimeField: (
datetime(2020, 1, 31, tzinfo=utc) + timedelta(days=i)
),
}
x = {
"foobar": (123 + 456),
}
x = {
"foobar": (123) + 456,
}
my_dict = {
"a key in my dict": a_very_long_variable * and_a_very_long_function_call() / 100000.0
}
my_dict = {
"a key in my dict": a_very_long_variable * and_a_very_long_function_call() * and_another_long_func() / 100000.0
}
my_dict = {
"a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value")
}
{
'xxxxxx':
"xxxxxx":
xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxx(
xxxxxxxxxxxxxx={
'x':
"x":
xxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
@@ -29,8 +117,8 @@ my_dict = {
xxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx={
'x': x.xx,
'x': x.x,
"x": x.xx,
"x": x.x,
}))))
}),
}

View File

@@ -1,3 +1,24 @@
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx"
)
}
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx"
),
}
x = {
"foo": bar,
"foo": bar,
"foo": (
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx
),
}
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxx"
}
my_dict = {
"something_something": (
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
@@ -6,12 +27,80 @@ my_dict = {
),
}
# Function calls as keys
tasks = {
get_key_name(
foo,
bar,
baz,
): src,
loop.run_in_executor(): src,
loop.run_in_executor(xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx): src,
loop.run_in_executor(
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxx
): src,
loop.run_in_executor(): (
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx
),
}
# Dictionary comprehensions
tasks = {
key_name: (
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx
)
for src in sources
}
tasks = {key_name: foobar for src in sources}
tasks = {
get_key_name(
src,
): "foo"
for src in sources
}
tasks = {
get_key_name(
foo,
bar,
baz,
): src
for src in sources
}
tasks = {
get_key_name(): (
xx_xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxxxxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx
)
for src in sources
}
tasks = {get_key_name(): foobar for src in sources}
# Delimiters inside the value
def foo():
def bar():
x = {
common.models.DateTimeField: (
datetime(2020, 1, 31, tzinfo=utc) + timedelta(days=i)
),
}
x = {
common.models.DateTimeField: (
datetime(2020, 1, 31, tzinfo=utc) + timedelta(days=i)
),
}
x = {
"foobar": 123 + 456,
}
x = {
"foobar": (123) + 456,
}
my_dict = {
"a key in my dict": (
a_very_long_variable * and_a_very_long_function_call() / 100000.0
)
}
my_dict = {
"a key in my dict": (
a_very_long_variable
@@ -20,7 +109,6 @@ my_dict = {
/ 100000.0
)
}
my_dict = {
"a key in my dict": (
MyClass.some_attribute.first_call()
@@ -51,8 +139,8 @@ my_dict = {
class Random:
def func():
random_service.status.active_states.inactive = (
make_new_top_level_state_from_dict({
random_service.status.active_states.inactive = make_new_top_level_state_from_dict(
{
"topLevelBase": {
"secondaryBase": {
"timestamp": 1234,
@@ -63,5 +151,5 @@ class Random:
),
}
},
})
}
)

View File

@@ -278,7 +278,7 @@ string_with_escaped_nameescape = (
"........................................................................... \\N{LAO KO LA}"
)
msg = lambda x: f"this is a very very very long lambda value {x} that doesn't fit on a single line"
msg = lambda x: f"this is a very very very very long lambda value {x} that doesn't fit on a single line"
dict_with_lambda_values = {
"join": lambda j: (
@@ -327,3 +327,17 @@ log.info(f'''Skipping: {"a" == 'b'} {desc["ms_name"]} {money=} {dte=} {pos_share
log.info(f'''Skipping: {'a' == "b"=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}''')
log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}""")
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx"
)
}
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx",
}
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxx"
)
}

View File

@@ -508,11 +508,9 @@ string_with_escaped_nameescape = (
" \\N{LAO KO LA}"
)
msg = (
lambda x: (
f"this is a very very very long lambda value {x} that doesn't fit on a single"
" line"
)
msg = lambda x: (
f"this is a very very very very long lambda value {x} that doesn't fit on a"
" single line"
)
dict_with_lambda_values = {
@@ -537,43 +535,43 @@ code = (
call(body="%s %s" % (",".join(items), suffix))
log.info(
"Skipping:"
f' {desc["db_id"]=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]=} {desc["exposure_max"]=}'
f'Skipping: {desc["db_id"]=} {desc["ms_name"]} {money=} {dte=} {pos_share=}'
f' {desc["status"]=} {desc["exposure_max"]=}'
)
log.info(
"Skipping:"
f" {desc['db_id']=} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']=} {desc['exposure_max']=}"
f"Skipping: {desc['db_id']=} {desc['ms_name']} {money=} {dte=} {pos_share=}"
f" {desc['status']=} {desc['exposure_max']=}"
)
log.info(
"Skipping:"
f" {desc['db_id']} {foo('bar',x=123)} {'foo' != 'bar'} {(x := 'abc=')} {pos_share=} {desc['status']} {desc['exposure_max']}"
f'Skipping: {desc["db_id"]} {foo("bar",x=123)} {"foo" != "bar"} {(x := "abc=")}'
f' {pos_share=} {desc["status"]} {desc["exposure_max"]}'
)
log.info(
"Skipping:"
f' {desc["db_id"]} {desc["ms_name"]} {money=} {(x := "abc=")=} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
f'Skipping: {desc["db_id"]} {desc["ms_name"]} {money=} {(x := "abc=")=}'
f' {pos_share=} {desc["status"]} {desc["exposure_max"]}'
)
log.info(
"Skipping:"
f' {desc["db_id"]} {foo("bar",x=123)=} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
f'Skipping: {desc["db_id"]} {foo("bar",x=123)=} {money=} {dte=} {pos_share=}'
f' {desc["status"]} {desc["exposure_max"]}'
)
log.info(
"Skipping:"
f' {foo("asdf")=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
f'Skipping: {foo("asdf")=} {desc["ms_name"]} {money=} {dte=} {pos_share=}'
f' {desc["status"]} {desc["exposure_max"]}'
)
log.info(
"Skipping:"
f" {'a' == 'b' == 'c' == 'd'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"
f'Skipping: {"a" == "b" == "c" == "d"} {desc["ms_name"]} {money=} {dte=}'
f' {pos_share=} {desc["status"]} {desc["exposure_max"]}'
)
log.info(
"Skipping:"
f' {"a" == "b" == "c" == "d"=} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
f'Skipping: {"a" == "b" == "c" == "d"=} {desc["ms_name"]} {money=} {dte=}'
f' {pos_share=} {desc["status"]} {desc["exposure_max"]}'
)
log.info(
@@ -592,3 +590,17 @@ log.info(
log.info(
f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"""
)
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx"
)
}
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxxx{xx}xxx_xxxxx_xxxxxxxxx_xxxxxxxxxxxx_xxxx"
),
}
x = {
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": "xx:xxxxxxxxxxxxxxxxx_xxxxx_xxxxxxx_xxxxxxxxxx"
}

View File

@@ -551,6 +551,7 @@ a_dict = {
}
# Regression test for https://github.com/psf/black/issues/3506.
# Regressed again by https://github.com/psf/black/pull/4498
s = (
"With single quote: ' "
f" {my_dict['foo']}"

View File

@@ -672,9 +672,15 @@ a_dict = {
}
# Regression test for https://github.com/psf/black/issues/3506.
s = f"With single quote: ' {my_dict['foo']} With double quote: \" {my_dict['bar']}"
# Regressed again by https://github.com/psf/black/pull/4498
s = (
"With single quote: ' "
f" {my_dict['foo']}"
' With double quote: " '
f' {my_dict["bar"]}'
)
s = (
"Lorem Ipsum is simply dummy text of the printing and typesetting"
f" industry:'{my_dict['foo']}'"
f' industry:\'{my_dict["foo"]}\''
)

View File

@@ -180,3 +180,70 @@ test
assert some_var == expected_result, f"""
expected: {expected_result}
actual: {some_var}"""
def foo():
a = {
xxxx_xxxxxxx.xxxxxx_xxxxxxxxxxxx_xxxxxx_xx_xxx_xxxxxx: {
"xxxxx": """Sxxxxx xxxxxxxxxxxx xxx xxxxx (xxxxxx xxx xxxxxxx)""",
"xxxxxxxx": (
"""Sxxxxxxx xxxxxxxx, xxxxxxx xx xxxxxxxxx
xxxxxxxxxxxxx xxxxxxx xxxxxxxxx xxx-xxxxxxxxxx xxxxxx xx xxx-xxxxxx"""
),
"xxxxxxxx": """Sxxxxxxx xxxxxxxx, xxxxxxx xx xxxxxxxxx
xxxxxxxxxxxxx xxxxxxx xxxxxxxxx xxx-xxxxxxxxxx xxxxxx xx xxx-xxxxxx"""
},
}
xxxx_xxxxxxx.xxxxxx_xxxxxxxxxxxx_xxxxxx_xx_xxx_xxxxxx = {
"xxxxx": """Sxxxxx xxxxxxxxxxxx xxx xxxxx (xxxxxx xxx xxxxxxx)""",
"xxxxxxxx": (
"""Sxxxxxxx xxxxxxxx, xxxxxxx xx xxxxxxxxx
xxxxxxxxxxxxx xxxxxxx xxxxxxxxx xxx-xxxxxxxxxx xxxxxx xx xxx-xxxxxx"""
),
"xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"""
a
a
a
a
a"""
),
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": """
a
a
a
a
a""",
}
a = """
""" if """
""" == """
""" else """
"""
a = """
""" if b else """
"""
a = """
""" if """
""" == """
""" else b
a = b if """
""" == """
""" else """
"""
a = """
""" if b else c
a = c if b else """
"""
a = b if """
""" == """
""" else c

View File

@@ -214,3 +214,106 @@ test
assert some_var == expected_result, f"""
expected: {expected_result}
actual: {some_var}"""
def foo():
a = {
xxxx_xxxxxxx.xxxxxx_xxxxxxxxxxxx_xxxxxx_xx_xxx_xxxxxx: {
"xxxxx": """Sxxxxx xxxxxxxxxxxx xxx xxxxx (xxxxxx xxx xxxxxxx)""",
"xxxxxxxx": (
"""Sxxxxxxx xxxxxxxx, xxxxxxx xx xxxxxxxxx
xxxxxxxxxxxxx xxxxxxx xxxxxxxxx xxx-xxxxxxxxxx xxxxxx xx xxx-xxxxxx"""
),
"xxxxxxxx": (
"""Sxxxxxxx xxxxxxxx, xxxxxxx xx xxxxxxxxx
xxxxxxxxxxxxx xxxxxxx xxxxxxxxx xxx-xxxxxxxxxx xxxxxx xx xxx-xxxxxx"""
),
},
}
xxxx_xxxxxxx.xxxxxx_xxxxxxxxxxxx_xxxxxx_xx_xxx_xxxxxx = {
"xxxxx": """Sxxxxx xxxxxxxxxxxx xxx xxxxx (xxxxxx xxx xxxxxxx)""",
"xxxxxxxx": (
"""Sxxxxxxx xxxxxxxx, xxxxxxx xx xxxxxxxxx
xxxxxxxxxxxxx xxxxxxx xxxxxxxxx xxx-xxxxxxxxxx xxxxxx xx xxx-xxxxxx"""
),
"xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"""
a
a
a
a
a"""
),
"xx_xxxxx_xxxxxxxxxx_xxxxxxxxx_xx": (
"""
a
a
a
a
a"""
),
}
a = (
"""
"""
if """
"""
== """
"""
else """
"""
)
a = (
"""
"""
if b
else """
"""
)
a = (
"""
"""
if """
"""
== """
"""
else b
)
a = (
b
if """
"""
== """
"""
else """
"""
)
a = (
"""
"""
if b
else c
)
a = (
c
if b
else """
"""
)
a = (
b
if """
"""
== """
"""
else c
)

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