Compare commits

..

45 Commits

Author SHA1 Message Date
David Peter
7d3ad59970 Merge remote-tracking branch 'origin/main' into david/dataclass-final-fields 2025-07-08 14:35:54 +02:00
David Peter
9a4b85d845 [ty] Add tests for dataclass fields annotated with Final (#19202)
## Summary

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

## Test Plan

New Markdown tests
2025-07-08 12:33:46 +00:00
David Peter
d133d7ab03 Add comment 2025-07-08 14:30:30 +02:00
David Peter
7d642e6416 Add test for 'Final'-qualified field without a default 2025-07-08 14:25:19 +02:00
David Peter
6d8c84bde9 [ty] Clarify diagnostic message (#19203)
This diagnostic message was missing the word "type"
2025-07-08 14:21:20 +02:00
David Peter
b92a283f35 [ty] Add tests for dataclass fields annotated with Final 2025-07-08 13:23:19 +02:00
Alex Waygood
e16473d260 [ty] Add a new property test: all types assignable to Iterable[object] should be considered iterable (#19186) 2025-07-08 10:54:06 +01:00
Alex Waygood
220a584c11 [ty] Add an instance of an Any subclass to the property tests (#19180) 2025-07-08 10:53:50 +01:00
Dhruv Manilawala
1ddda241f6 [ty] Add an empty line to separate bullet points (#19195)
Without the newline, the rendering would just combine all the bullet
points in a single line like in
https://docs.astral.sh/ty/reference/configuration/#exclude_1. With the
empty line, it would be similar to
https://docs.astral.sh/ty/reference/configuration/#include_1.
2025-07-08 05:10:31 +00:00
UnboundVariable
278f93022a [ty] First cut at semantic token provider (#19108)
This PR implements a basic semantic token provider for ty's language
server. This allows for more accurate semantic highlighting / coloring
within editors that support this LSP functionality.

Here are screen shots that show how code appears in VS Code using the
"rainbow" theme both before and after this change.


![461737617-15630625-d4a9-4ec5-9886-77b00eb7a41a](https://github.com/user-attachments/assets/f963b55b-3195-41d1-ba38-ac2e7508d5f5)


![461737624-d6dcf5f0-7b9b-47de-a410-e202c63e2058](https://github.com/user-attachments/assets/111ca2c5-bb4f-4c8a-a0b5-6c1b2b6f246b)

The token types and modifier tags in this implementation largely mirror
those used in Microsoft's default language server for Python.

The implementation supports two LSP interfaces. The first provides
semantic tokens for an entire document, and the second returns semantic
tokens for a requested range within a document.

The PR includes unit tests. It also includes comments that document
known limitations and areas for future improvements.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-07 15:34:47 -07:00
GiGaGon
4dd2c03144 [flake8-simplify] Make example error out-of-the-box (SIM116) (#19111)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

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

Part of #18972

This PR makes [if-else-block-instead-of-dict-lookup
(SIM116)](https://docs.astral.sh/ruff/rules/if-else-block-instead-of-dict-lookup/#if-else-block-instead-of-dict-lookup-sim116)'s
example error out-of-the-box

[Old example](https://play.ruff.rs/718f17ee-fbe2-4520-97c6-153bc0f4502d)
```py
if x == 1:
    return "Hello"
elif x == 2:
    return "Goodbye"
else:
    return "Goodnight"
```

[New example](https://play.ruff.rs/8a9b47b4-da46-4a50-8576-362cdd707cee)
```py
def find_phrase(x):
    if x == 1:
        return "Hello"
    elif x == 2:
        return "Goodbye"
    elif x == 3:
        return "Good morning"
    else:
        return "Goodnight"
```

The "Use instead" section was also updated to reflect the new case. I
also changed it to use an intermediary variable since I find the `return
<long dict>.get` very ugly and hard to read.

## Test Plan

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

N/A, no functionality/tests affected
2025-07-07 17:17:55 -04:00
GiGaGon
de5264fe13 [flake8-use-pathlib] Make example error out-of-the-box (PTH210) (#19189)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

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

Part of #18972

This PR makes [invalid-pathlib-with-suffix
(PTH210)](https://docs.astral.sh/ruff/rules/invalid-pathlib-with-suffix/#invalid-pathlib-with-suffix-pth210)'s
example error out-of-the-box.

[Old example](https://play.ruff.rs/d45720cc-fd08-4443-820f-b3bc9756ac59)
```py
path.with_suffix("py")
```

[New example](https://play.ruff.rs/4103669e-19c5-464a-a3fb-6e7d190ce5fd)
```py
from pathlib import Path

path = Path()

path.with_suffix("py")
```

The "Use instead" section was also modified similarly.

## Test Plan

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

N/A, no functionality/tests affected
2025-07-07 17:04:35 -04:00
chiri
e23780c2e1 [flake8-use-pathlib] Add autofixes for PTH203, PTH204, PTH205 (#18922)
<!--
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
Part of #2331 |
[#18763](https://github.com/astral-sh/ruff/pull/18763#issuecomment-2988340436)
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
update snapshots
<!-- How was it tested? -->
2025-07-07 16:56:21 -04:00
GiGaGon
47f88b3008 [flake8-type-checking] Fix syntax error introduced by fix (TC008) (#19150)
<!--
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? -->

I noticed this while working on #18972. If the string targeted by
[quoted-type-alias
(TC008)](https://docs.astral.sh/ruff/rules/quoted-type-alias/#quoted-type-alias-tc008)
is a multiline string, the fix would introduce a syntax error. This PR
fixes that by adding parenthesis around the resulting replacement if the
string contained any newline characters (`\n`, `\r`) if it doesn't
already have parenthesis outside `("""...""")` or inside `"""(...)"""`
the annotation.

Failing examples:
https://play.ruff.rs/8793eb95-860a-4bb3-9cbc-6a042fee2946
```
PS D:\rust_projects\ruff> Get-Content issue.py
```
```py
from typing import TypeAlias

OptInt: TypeAlias = """int
| None"""

type OptInt = """int
| None"""
```
```
PS D:\rust_projects\ruff> uvx ruff check issue.py --isolated --select TC008 --fix --diff --preview
```
```

error: Fix introduced a syntax error. Reverting all changes.

This indicates a bug in Ruff. If you could open an issue at:

    https://github.com/astral-sh/ruff/issues/new?title=%5BFix%20error%5D

...quoting the contents of `issue.py`, the rule codes TC008, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
```

This PR also makes the example error out-of-the-box for #18972

Old example: https://play.ruff.rs/f6cd5adb-7f9b-444d-bb3e-8c045241d93e
```py
OptInt: TypeAlias = "int | None"
```

New example: https://play.ruff.rs/906c1056-72c0-4777-b70b-2114eb9e6eaf
```py
from typing import TypeAlias

OptInt: TypeAlias = "int | None"
```

The import was also added to the "Use instead" section.

## Test Plan

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

Added multiple test cases
2025-07-07 15:34:14 -05:00
GiGaGon
6e77e1b760 [flake8-pyi] Make example error out-of-the-box (PYI007, PYI008) (#19103)
## Summary

Part of #18972

Both in one PR since they are in the same file

No playground links since the playground does not support rules that
only apply to PYI files

PYI007
---

This PR makes [unrecognized-platform-check
(PYI007)](https://docs.astral.sh/ruff/rules/unrecognized-platform-check/#unrecognized-platform-check-pyi007)'s
example error out-of-the-box

Old example:
```
PS ~\Desktop\New_folder\ruff>echo @"
```
```py
if sys.platform.startswith("linux"):
    # Linux specific definitions
    ...
else:
    # Posix specific definitions
    ...
```
```
"@ | uvx ruff check --isolated --preview --select PYI007 --stdin-filename "test.pyi" -
```
```
All checks passed!
```

New example:
```
PS ~\Desktop\New_folder\ruff>echo @"
```
```py
import sys

if sys.platform is "linux":
    # Linux specific definitions
    ...
else:
    # Posix specific definitions
    ...
```
```
"@ | uvx ruff check --isolated --preview --select PYI007 --stdin-filename "test.pyi" -
```
```snap
test.pyi:3:4: PYI007 Unrecognized `sys.platform` check
  |
1 | import sys
2 |
3 | if sys.platform is "linux":
  |    ^^^^^^^^^^^^^^^^^^^^^^^ PYI007
4 |     # Linux specific definitions
5 |     ...
  |

Found 1 error.
```

Imports were also added to the "use instead" section

> [!NOTE]
> `PYI007` is really hard to trigger, it's only specifically in the case
of a comparison where the operator is not `!=` or `==`. The original
example raises [complex-if-statement-in-stub
(PYI002)](https://docs.astral.sh/ruff/rules/complex-if-statement-in-stub/#complex-if-statement-in-stub-pyi002)
with or without the `import sys`

PYI008
---

This PR makes [unrecognized-platform-name
(PYI008)](https://docs.astral.sh/ruff/rules/unrecognized-platform-name/#unrecognized-platform-name-pyi008)'s
example error out-of-the-box

Old example:
```
PS ~\Desktop\New_folder\ruff>echo @"
```
```py
if sys.platform == "linus": ...
```
```
"@ | uvx ruff check --isolated --preview --select PYI008 --stdin-filename "test.pyi" -
```
```
All checks passed!
```

New example:
```
PS ~\Desktop\New_folder\ruff>echo @"
```
```py
import sys

if sys.platform == "linus": ...
```
```
"@ | uvx ruff check --isolated --preview --select PYI008 --stdin-filename "test.pyi" -
```
```snap
test.pyi:3:20: PYI008 Unrecognized platform `linus`
  |
1 | import sys
2 |
3 | if sys.platform == "linus": ...
  |                    ^^^^^^^ PYI008
  |

Found 1 error.
```

Imports were also added to the "use instead" section

> [!NOTE]
> The original example raises `PYI002` instead

## Test Plan

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

N/A, no functionality/tests affected

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-07-07 21:11:43 +01:00
renovate[bot]
845a1eeba6 Update Rust crate indicatif to 0.18.0 (#19165)
## Summary

Updates `indicatif` and `tracing-indicatif`.
2025-07-07 13:19:23 -04:00
Ibraheem Ahmed
cd848986d7 [ty] Add separate CI job for memory usage stats (#19134)
## Summary

As discussed in https://github.com/astral-sh/ruff/pull/19059.
2025-07-07 12:17:02 -04:00
Dhruv Manilawala
56258bb3b7 [ty] Add documentation for server traits (#19137)
This PR adds some basic documentation for the traits in the server
implementation.
2025-07-07 14:26:09 +00:00
Dhruv Manilawala
8cf1b876ee Rename to SessionSnapshot, move unwind assertion closer (#19177)
This PR addresses the post-merge review comments from
https://github.com/astral-sh/ruff/pull/19041, specifically it:
- Rename `WorkspaceSnapshot` to `SessionSnapshot`
- Rename `take_workspace_snapshot` to `take_session_snapshot`
- Rename `take_snapshot` to `take_document_snapshot`
- Move `AssertUnwindSafe` closer to the `catch_unwind` call which
requires the assertion
2025-07-07 19:44:23 +05:30
GiGaGon
1fd48120ba [flake8-type-checking] Make example error out-of-the-box (TC001) (#19151)
## Summary

Part of #18972

This PR makes [typing-only-first-party-import
(TC001)](https://docs.astral.sh/ruff/rules/typing-only-first-party-import/#typing-only-first-party-import-tc001)'s
example error out-of-the-box. The old example raised `TC002` instead of
`TC001`, so this makes it a `from .` import to fix that.

[Old example](https://play.ruff.rs/1fdbb293-86fc-4ed2-b2ff-b4836cea0c59)
```py
from __future__ import annotations

import local_module


def func(sized: local_module.Container) -> int:
    return len(sized)
```

[New example](https://play.ruff.rs/b886535c-9203-48bb-812b-1aa306f2c287)
```py
from __future__ import annotations

from . import local_module


def func(sized: local_module.Container) -> int:
    return len(sized)
```

The "Use instead" section was also modified similarly.

## Test Plan

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

N/A, no functionality/tests affected
2025-07-07 08:53:37 -05:00
David Peter
e7fb3684e8 [ty] Bare ClassVar annotations (#15768)
## Summary

It was recently clarified in the [typing
spec](https://typing.python.org/en/latest/spec/class-compat.html#classvar)
that bare `ClassVar` annotations are allowed. For annotated assignments
with a right hand side value, the spec requires type checkers to infer
the type as something "to which [the] value is assignable". For a value
of `2`, the spec suggests `int`, `Literal[2]`, or `Any` as examples.
Here, we choose `Unknown | Literal[2]` instead, conforming with out
usual treatment of attribute types.

closes https://github.com/astral-sh/ty/issues/211
2025-07-07 15:04:27 +02:00
David Peter
4aaf32476a [ty] Re-enable multithreaded pydantic benchmark (#19176)
## Summary

I played with those numbers a bit locally and `sample_size=3,
sample_count=8` seemed like a rather stable setup. This means a single
sample consistents of 3 iterations of checking pydantic multithreaded.
And this is repeated 8 times for statistics. A single check took ~300 ms
previously on the runners, so this should only take 7 s.
2025-07-07 14:28:15 +02:00
Alex Waygood
a6637964d2 [ty] Implement equivalence for protocols with method members (#18659)
## Summary

This PR implements the following pieces of `Protocol` semantics:
1. A protocol with a method member that does not have a fully static
signature should not be considered fully static. I.e., this protocol is
not fully static because `Foo.x` has no return type; we previously
incorrectly considered that it was:
  ```py
  class Foo(Protocol):
      def f(self): ...
  ```
2. Two protocols `P1` and `P2`, both with method members `x`, should be
considered equivalent if the signature of `P1.x` is equivalent to the
signature of `P2.x`. Currently we do not recognize this.

Implementing these semantics requires distinguishing between method
members and non-method members. The stored type of a method member must
be eagerly upcast to a `Callable` type when collecting the protocol's
interface: doing otherwise would mean that it would be hard to implement
equivalence of protocols even in the face of differently ordered unions,
since the two equivalent protocols would have different Salsa IDs even
when normalized.

The semantics implemented by this PR are that we consider something a
method member if:
1. It is accessible on the class itself; and
2. It is a function-like callable: a callable type that also has a
`__get__` method, meaning it can be used as a method when accessed on
instances.

Note that the spec has complicated things to say about classmethod
members and staticmethod members. These semantics are not implemented by
this PR; they are all deferred for now.

The infrastructure added in this PR fixes bugs in its own right, but
also lays the groundwork for implementing subtyping and assignability
rules for method members of protocols. A (currently failing) test is
added to verify this.

## Test Plan

mdtests
2025-07-07 12:28:32 +01:00
David Peter
c15aa572ff [ty] Use RHS inferred type for bare Final symbols (#19142)
## Summary

Infer the type of symbols with a `Final` qualifier as their
right-hand-side inferred type:
```py
x: Final = 1
y: Final[int] = 1

def _():
    reveal_type(x)  # previously: Unknown, now: Literal[1]
    reveal_type(y)  # int, same as before
```
Part of https://github.com/astral-sh/ty/issues/158

## Ecosystem analysis

### aiohttp

```diff
aiohttp (https://github.com/aio-libs/aiohttp)
+ error[invalid-argument-type] aiohttp/compression_utils.py:131:54: Argument to bound method `__init__` is incorrect: Expected `ZLibBackendProtocol`, found `<module 'zlib'>`
```

This code [creates a
protocol](a83597fa88/aiohttp/compression_utils.py (L52-L77))
that looks like
```pyi
class ZLibBackendProtocol(Protocol):
    Z_FULL_FLUSH: int
    Z_SYNC_FLUSH: int
    # more fields…
```

It then [tries to
assign](a83597fa88/aiohttp/compression_utils.py (L131))
the module literal `zlib` to that protocol. Howefer, in typeshed, these
`zlib` members are annotated like this:
```pyi
Z_FULL_FLUSH: Final = 3
Z_SYNC_FLUSH: Final = 2
```
With the proposed change here, we now infer these as `Literal[3]` /
`Literal[2]`. Since protocol members have to be assignable both ways
(invariance), we do not consider `zlib` assignable to this protocol
anymore.

That seems rather unfortunate. Not sure who is to blame here? That
`ZLibBackendProtocol` protocol should probably not annotate the members
with `int`, given that `typeshed` doesn't use an explicit annotation
here either? But what should they do instead? Annotate those fields with
`Any`?

Or is it another case where we should consider literal-widening?

FYI @AlexWaygood 

### cloud-init

```diff
cloud-init (https://github.com/canonical/cloud-init)
+ error[invalid-argument-type] tests/unittests/sources/test_smartos.py:575:32: Argument to function `oct` is incorrect: Expected `SupportsIndex`, found `int | float`
+ error[invalid-argument-type] tests/unittests/sources/test_smartos.py:593:32: Argument to function `oct` is incorrect: Expected `SupportsIndex`, found `int | float`
+ error[invalid-argument-type] tests/unittests/sources/test_smartos.py:647:35: Argument to function `oct` is incorrect: Expected `SupportsIndex`, found `int | float`
```

New false positives on expressions like
`oct(os.stat(legacy_script_f)[stat.ST_MODE])`. We now correctly infer
`stat.ST_MODE` as `Literal[1]`, because in typeshed, it is annotated as
`ST_MODE: Final = 0`. `os.stat` returns a `stat_result` which is a tuple
subclass. Accessing it at index 0 should return an `int`, but we
currently return `int | float`, presumably due to missing support for
tuple subclasses (FYI @AlexWaygood):
```pyi
class stat_result(structseq[float], tuple[int, int, int, int, int, int, int, float, float, float]):
```
In terms of `typing.Final`, things are working as expected here.


### pywin-32

Many new false positives similar to:

```diff
pywin32 (https://github.com/mhammond/pywin32)
+ error[invalid-argument-type] Pythonwin/pywin/docking/DockingBar.py:288:55: Argument to function `LoadCursor` is incorrect: Expected `PyResourceId`, found `Literal[32645]`
```

The line in question calls `win32api.LoadCursor(0, win32con.IDC_ARROW)`.
The `win32con.IDC_ARROW` symbol is annotated as [`IDC_ARROW: Final =
32512` in
typeshed](2408c028f4/stubs/pywin32/win32/lib/win32con.pyi (L594)),
but
[`LoadCursor`](2408c028f4/stubs/pywin32/win32/win32api.pyi (L197))
expects a
[`PyResourceId`](2408c028f4/stubs/pywin32/_win32typing.pyi (L1252)),
which is an empty class. So.. this seems like a true positive to me,
unless that typeshed annotation of `IDC_ARROW` is meant to imply that
the type should be `Unknown`/`Any`?

### streamlit

```diff
streamlit (https://github.com/streamlit/streamlit)
+ error[invalid-argument-type] lib/streamlit/string_util.py:163:37: Argument to bound method `translate` is incorrect: Expected `bytes`, found `bytearray`
```

This looks like a true positive? The code calls `inp.translate(None,
TEXTCHARS)`. `inp` is `bytes`, and `TEXTCHARS` is:
```py
TEXTCHARS: Final = bytearray(
    {7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7F}
)
```
~~We now infer this as `bytearray`, but `bytes.translate` [expects
`bytes` for its `delete`
parameter](2408c028f4/stdlib/builtins.pyi (L710)).
This seems to work at runtime, so maybe the typeshed annotation is
wrong?~~ (Edit: this is now fixed in typeshed)
```pycon
>>> b"abc".translate(None, bytearray(b"b"))
b'ac'
```

## rotki

```diff
+ error[invalid-return-type] rotkehlchen/chain/ethereum/modules/yearn/decoder.py:412:13: Return type does not match returned value: expected `dict[Unknown, str]`, found `dict[Unknown, Literal["yearn-v1", "yearn-v2"]]`
```

The code in question looks like
```py
    def addresses_to_counterparties(self) -> dict[ChecksumEvmAddress, str]:
        return dict.fromkeys(self.vaults, CPT_BEEFY_FINANCE)
```
where `CPT_BEEFY_FINANCE: Final = 'beefy_finance'. We previously
inferred the value type of the returned `dict` as `Unknown`, and now we
infer it as `Literal["beefy_finance"]`, which does not match the
annotated return type because `dict` is invariant in the value type.

```diff
+ error[invalid-argument-type] rotkehlchen/tests/unit/decoders/test_curve.py:249:9: Argument is incorrect: Expected `int`, found `FVal`
```
There are true positives that were previously silenced through the
`Unknown`.

## Test Plan

New Markdown tests
2025-07-07 13:16:40 +02:00
Ivan Yakushev
e0b7f496f2 [ty] Support declaration-only attributes (#19048)
## Summary

Following ty issue [#698](https://github.com/astral-sh/ty/issues/698)
this PR adds support for declarations.

closes #698

## Test Plan

Tested against mdtest (specifically attributes).

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-07 12:55:32 +02:00
github-actions[bot]
b6edfbc70f [ty] Sync vendored typeshed stubs (#19174)
Close and reopen this PR to trigger CI

---------

Co-authored-by: typeshedbot <>
Co-authored-by: David Peter <mail@david-peter.de>
2025-07-07 12:00:09 +02:00
renovate[bot]
9be8276616 Update dependency pyodide to ^0.28.0 (#19164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 10:38:29 +02:00
renovate[bot]
d0099fd012 Update NPM Development dependencies (#19170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 10:33:59 +02:00
renovate[bot]
0d3802998b Update taiki-e/install-action action to v2.56.7 (#19169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 10:25:43 +02:00
renovate[bot]
1a03b5841b Update pre-commit dependencies (#19162)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit)
| repository | patch | `v0.12.1` -> `v0.12.2` |
| [crate-ci/typos](https://redirect.github.com/crate-ci/typos) |
repository | minor | `v1.33.1` -> `v1.34.0` |
|
[python-jsonschema/check-jsonschema](https://redirect.github.com/python-jsonschema/check-jsonschema)
| repository | patch | `0.33.1` -> `0.33.2` |
|
[woodruffw/zizmor-pre-commit](https://redirect.github.com/woodruffw/zizmor-pre-commit)
| repository | minor | `v1.10.0` -> `v1.11.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

Note: The `pre-commit` manager in Renovate is not supported by the
`pre-commit` maintainers or community. Please do not report any problems
there, instead [create a Discussion in the Renovate
repository](https://redirect.github.com/renovatebot/renovate/discussions/new)
if you have any questions.

---

### Release Notes

<details>
<summary>astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit)</summary>

###
[`v0.12.2`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.12.2)

[Compare
Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.12.1...v0.12.2)

See: https://github.com/astral-sh/ruff/releases/tag/0.12.2

</details>

<details>
<summary>crate-ci/typos (crate-ci/typos)</summary>

###
[`v1.34.0`](https://redirect.github.com/crate-ci/typos/releases/tag/v1.34.0)

[Compare
Source](https://redirect.github.com/crate-ci/typos/compare/v1.33.1...v1.34.0)

#### \[1.34.0] - 2025-06-30

##### Features

- Updated the dictionary with the [June
2025](https://redirect.github.com/crate-ci/typos/issues/1309) changes

</details>

<details>
<summary>python-jsonschema/check-jsonschema
(python-jsonschema/check-jsonschema)</summary>

###
[`v0.33.2`](https://redirect.github.com/python-jsonschema/check-jsonschema/blob/HEAD/CHANGELOG.rst#0332)

[Compare
Source](https://redirect.github.com/python-jsonschema/check-jsonschema/compare/0.33.1...0.33.2)

- Update vendored schemas: bitbucket-pipelines, mergify, renovate
(2025-06-29)
- Fix a bug in the evaluation of the `date-time` format on non-string
data,
which incorrectly rejected values for which `string` was one of several
  valid types. Thanks :user:`katylava`! (:issue:`571`)

</details>

<details>
<summary>woodruffw/zizmor-pre-commit
(woodruffw/zizmor-pre-commit)</summary>

###
[`v1.11.0`](https://redirect.github.com/zizmorcore/zizmor-pre-commit/releases/tag/v1.11.0)

[Compare
Source](https://redirect.github.com/woodruffw/zizmor-pre-commit/compare/v1.10.0...v1.11.0)

See: https://github.com/zizmorcore/zizmor/releases/tag/v1.11.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMTcuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-07-07 04:07:44 +00:00
renovate[bot]
a7fafb96be Update dependency smol-toml to v1.4.1 (#19161)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [smol-toml](https://redirect.github.com/squirrelchat/smol-toml) |
[`1.4.0` ->
`1.4.1`](https://renovatebot.com/diffs/npm/smol-toml/1.4.0/1.4.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/smol-toml/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/smol-toml/1.4.0/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>squirrelchat/smol-toml (smol-toml)</summary>

###
[`v1.4.1`](https://redirect.github.com/squirrelchat/smol-toml/releases/tag/v1.4.1)

[Compare
Source](https://redirect.github.com/squirrelchat/smol-toml/compare/v1.4.0...v1.4.1)

A little fix for `asNeeded` not being implemented correctly.

#### What's Changed

fix: properly implement asNeeded by
[@&#8203;cyyynthia](https://redirect.github.com/cyyynthia)

**Full Changelog**:
https://github.com/squirrelchat/smol-toml/compare/v1.4.0...v1.4.1

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMTcuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 08:54:52 +05:30
renovate[bot]
22d809b8ce Update dependency ruff to v0.12.2 (#19160)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [ruff](https://docs.astral.sh/ruff)
([source](https://redirect.github.com/astral-sh/ruff),
[changelog](https://redirect.github.com/astral-sh/ruff/blob/main/CHANGELOG.md))
| `==0.12.1` -> `==0.12.2` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.12.1/0.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>astral-sh/ruff (ruff)</summary>

###
[`v0.12.2`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0122)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.12.1...0.12.2)

##### Preview features

- \[`flake8-pyi`] Expand `Optional[A]` to `A | None` (`PYI016`)
([#&#8203;18572](https://redirect.github.com/astral-sh/ruff/pull/18572))
- \[`pyupgrade`] Mark `UP008` fix safe if no comments are in range
([#&#8203;18683](https://redirect.github.com/astral-sh/ruff/pull/18683))

##### Bug fixes

- \[`flake8-comprehensions`] Fix `C420` to prepend whitespace when
needed
([#&#8203;18616](https://redirect.github.com/astral-sh/ruff/pull/18616))
- \[`perflint`] Fix `PERF403` panic on attribute or subscription loop
variable
([#&#8203;19042](https://redirect.github.com/astral-sh/ruff/pull/19042))
- \[`pydocstyle`] Fix `D413` infinite loop for parenthesized docstring
([#&#8203;18930](https://redirect.github.com/astral-sh/ruff/pull/18930))
- \[`pylint`] Fix `PLW0108` autofix introducing a syntax error when the
lambda's body contains an assignment expression
([#&#8203;18678](https://redirect.github.com/astral-sh/ruff/pull/18678))
- \[`refurb`] Fix false positive on empty tuples (`FURB168`)
([#&#8203;19058](https://redirect.github.com/astral-sh/ruff/pull/19058))
- \[`ruff`] Allow more `field` calls from `attrs` (`RUF009`)
([#&#8203;19021](https://redirect.github.com/astral-sh/ruff/pull/19021))
- \[`ruff`] Fix syntax error introduced for an empty string followed by
a u-prefixed string (`UP025`)
([#&#8203;18899](https://redirect.github.com/astral-sh/ruff/pull/18899))

##### Rule changes

- \[`flake8-executable`] Allow `uvx` in shebang line (`EXE003`)
([#&#8203;18967](https://redirect.github.com/astral-sh/ruff/pull/18967))
- \[`pandas`] Avoid flagging `PD002` if `pandas` is not imported
([#&#8203;18963](https://redirect.github.com/astral-sh/ruff/pull/18963))
- \[`pyupgrade`] Avoid PEP-604 unions with `typing.NamedTuple` (`UP007`,
`UP045`)
([#&#8203;18682](https://redirect.github.com/astral-sh/ruff/pull/18682))

##### Documentation

- Document link between `import-outside-top-level (PLC0415)` and
`lint.flake8-tidy-imports.banned-module-level-imports`
([#&#8203;18733](https://redirect.github.com/astral-sh/ruff/pull/18733))
- Fix description of the `format.skip-magic-trailing-comma` example
([#&#8203;19095](https://redirect.github.com/astral-sh/ruff/pull/19095))
- \[`airflow`] Make `AIR302` example error out-of-the-box
([#&#8203;18988](https://redirect.github.com/astral-sh/ruff/pull/18988))
- \[`airflow`] Make `AIR312` example error out-of-the-box
([#&#8203;18989](https://redirect.github.com/astral-sh/ruff/pull/18989))
- \[`flake8-annotations`] Make `ANN401` example error out-of-the-box
([#&#8203;18974](https://redirect.github.com/astral-sh/ruff/pull/18974))
- \[`flake8-async`] Make `ASYNC100` example error out-of-the-box
([#&#8203;18993](https://redirect.github.com/astral-sh/ruff/pull/18993))
- \[`flake8-async`] Make `ASYNC105` example error out-of-the-box
([#&#8203;19002](https://redirect.github.com/astral-sh/ruff/pull/19002))
- \[`flake8-async`] Make `ASYNC110` example error out-of-the-box
([#&#8203;18975](https://redirect.github.com/astral-sh/ruff/pull/18975))
- \[`flake8-async`] Make `ASYNC210` example error out-of-the-box
([#&#8203;18977](https://redirect.github.com/astral-sh/ruff/pull/18977))
- \[`flake8-async`] Make `ASYNC220`, `ASYNC221`, and `ASYNC222` examples
error out-of-the-box
([#&#8203;18978](https://redirect.github.com/astral-sh/ruff/pull/18978))
- \[`flake8-async`] Make `ASYNC251` example error out-of-the-box
([#&#8203;18990](https://redirect.github.com/astral-sh/ruff/pull/18990))
- \[`flake8-bandit`] Make `S201` example error out-of-the-box
([#&#8203;19017](https://redirect.github.com/astral-sh/ruff/pull/19017))
- \[`flake8-bandit`] Make `S604` and `S609` examples error
out-of-the-box
([#&#8203;19049](https://redirect.github.com/astral-sh/ruff/pull/19049))
- \[`flake8-bugbear`] Make `B028` example error out-of-the-box
([#&#8203;19054](https://redirect.github.com/astral-sh/ruff/pull/19054))
- \[`flake8-bugbear`] Make `B911` example error out-of-the-box
([#&#8203;19051](https://redirect.github.com/astral-sh/ruff/pull/19051))
- \[`flake8-datetimez`] Make `DTZ011` example error out-of-the-box
([#&#8203;19055](https://redirect.github.com/astral-sh/ruff/pull/19055))
- \[`flake8-datetimez`] Make `DTZ901` example error out-of-the-box
([#&#8203;19056](https://redirect.github.com/astral-sh/ruff/pull/19056))
- \[`flake8-pyi`] Make `PYI032` example error out-of-the-box
([#&#8203;19061](https://redirect.github.com/astral-sh/ruff/pull/19061))
- \[`flake8-pyi`] Make example error out-of-the-box (`PYI014`, `PYI015`)
([#&#8203;19097](https://redirect.github.com/astral-sh/ruff/pull/19097))
- \[`flake8-pyi`] Make example error out-of-the-box (`PYI042`)
([#&#8203;19101](https://redirect.github.com/astral-sh/ruff/pull/19101))
- \[`flake8-pyi`] Make example error out-of-the-box (`PYI059`)
([#&#8203;19080](https://redirect.github.com/astral-sh/ruff/pull/19080))
- \[`flake8-pyi`] Make example error out-of-the-box (`PYI062`)
([#&#8203;19079](https://redirect.github.com/astral-sh/ruff/pull/19079))
- \[`flake8-pytest-style`] Make example error out-of-the-box (`PT023`)
([#&#8203;19104](https://redirect.github.com/astral-sh/ruff/pull/19104))
- \[`flake8-pytest-style`] Make example error out-of-the-box (`PT030`)
([#&#8203;19105](https://redirect.github.com/astral-sh/ruff/pull/19105))
- \[`flake8-quotes`] Make example error out-of-the-box (`Q003`)
([#&#8203;19106](https://redirect.github.com/astral-sh/ruff/pull/19106))
- \[`flake8-simplify`] Make example error out-of-the-box (`SIM110`)
([#&#8203;19113](https://redirect.github.com/astral-sh/ruff/pull/19113))
- \[`flake8-simplify`] Make example error out-of-the-box (`SIM113`)
([#&#8203;19109](https://redirect.github.com/astral-sh/ruff/pull/19109))
- \[`flake8-simplify`] Make example error out-of-the-box (`SIM401`)
([#&#8203;19110](https://redirect.github.com/astral-sh/ruff/pull/19110))
- \[`pyflakes`] Fix backslash in docs (`F621`)
([#&#8203;19098](https://redirect.github.com/astral-sh/ruff/pull/19098))
- \[`pylint`] Fix `PLC0415` example
([#&#8203;18970](https://redirect.github.com/astral-sh/ruff/pull/18970))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMTcuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 08:54:35 +05:30
renovate[bot]
cc190a550c Update Rust crate serde_with to v3.14.0 (#19167)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_with](https://redirect.github.com/jonasbb/serde_with) |
workspace.dependencies | minor | `3.12.0` -> `3.14.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>jonasbb/serde_with (serde_with)</summary>

###
[`v3.14.0`](https://redirect.github.com/jonasbb/serde_with/releases/tag/v3.14.0):
serde_with v3.14.0

[Compare
Source](https://redirect.github.com/jonasbb/serde_with/compare/v3.13.0...v3.14.0)

##### Added

- Add support for `Range`, `RangeFrom`, `RangeTo`, `RangeInclusive`
([#&#8203;851](https://redirect.github.com/jonasbb/serde_with/issues/851))
  `RangeToInclusive` is currently unsupported by serde.
- Add `schemars` implementations for `Bound`, `Range`, `RangeFrom`,
`RangeTo`, `RangeInclusive`.
- Added support for `schemars` v1 under the `schemars_1` feature flag

###
[`v3.13.0`](https://redirect.github.com/jonasbb/serde_with/releases/tag/v3.13.0):
serde_with v3.13.0

[Compare
Source](https://redirect.github.com/jonasbb/serde_with/compare/v3.12.0...v3.13.0)

##### Added

- Added support for `schemars` v0.9.0 under the `schemars_0_9` feature
flag by [@&#8203;swlynch99](https://redirect.github.com/swlynch99)
([#&#8203;849](https://redirect.github.com/jonasbb/serde_with/issues/849))
- Introduce `SerializeDisplayAlt` derive macro
([#&#8203;833](https://redirect.github.com/jonasbb/serde_with/issues/833))
An alternative to the `SerializeDisplay` macro except instead of using
the
  plain formatting like `format!("{}", ...)`, it serializes with the
  `Formatter::alternate` flag set to true, like `format!("{:#}", ...)`

##### Changed

- Generalize `serde_with::rust::unwrap_or_skip` to support deserializing
references by [@&#8203;beroal](https://redirect.github.com/beroal)
([#&#8203;832](https://redirect.github.com/jonasbb/serde_with/issues/832))
- Bump MSRV to 1.71, since that is required for the `jsonschema`
dev-dependency.
- Make `serde_conv` available without the `std` feature by
[@&#8203;arilou](https://redirect.github.com/arilou)
([#&#8203;839](https://redirect.github.com/jonasbb/serde_with/issues/839))
- Bump MSRV to 1.74, since that is required for `schemars` v0.9.0 by
[@&#8203;swlynch99](https://redirect.github.com/swlynch99)
([#&#8203;849](https://redirect.github.com/jonasbb/serde_with/issues/849))

##### Fixed

- Make the `DurationSeconds` types and other variants more accessible
even without `std`
([#&#8203;845](https://redirect.github.com/jonasbb/serde_with/issues/845))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMTcuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 08:52:38 +05:30
renovate[bot]
ab024f9de1 Update Rust crate notify to v8.1.0 (#19166)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [notify](https://redirect.github.com/notify-rs/notify) |
workspace.dependencies | minor | `8.0.0` -> `8.1.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>notify-rs/notify (notify)</summary>

###
[`v8.1.0`](https://redirect.github.com/notify-rs/notify/blob/HEAD/CHANGELOG.md#notify-810-2025-07-03)

[Compare
Source](https://redirect.github.com/notify-rs/notify/compare/notify-8.0.0...notify-8.1.0)

- FEATURE: added support for the [`flume`](https://docs.rs/flume) crate
- FIX: kqueue-backend: do not double unwatch top-level directory when
recursively unwatching
\[[#&#8203;683](https://redirect.github.com/notify-rs/notify/issues/683)]
- FIX: Return the crate error `PathNotFound` instead bubbling up the
std::io error
\[[#&#8203;685](https://redirect.github.com/notify-rs/notify/issues/685)]
- FIX: fix server hangs when trashing folders on Windows
\[[#&#8203;674](https://redirect.github.com/notify-rs/notify/issues/674)]

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMTcuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 08:51:09 +05:30
renovate[bot]
3d2a0c3cd6 Update Swatinem/rust-cache action to v2.8.0 (#19168)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [Swatinem/rust-cache](https://redirect.github.com/Swatinem/rust-cache)
| action | minor | `v2.7.8` -> `v2.8.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>Swatinem/rust-cache (Swatinem/rust-cache)</summary>

###
[`v2.8.0`](https://redirect.github.com/Swatinem/rust-cache/releases/tag/v2.8.0)

[Compare
Source](https://redirect.github.com/Swatinem/rust-cache/compare/v2.7.8...v2.8.0)

##### What's Changed

- Add cache-workspace-crates feature by
[@&#8203;jbransen](https://redirect.github.com/jbransen) in
[https://github.com/Swatinem/rust-cache/pull/246](https://redirect.github.com/Swatinem/rust-cache/pull/246)
- Feat: support warpbuild cache provider by
[@&#8203;stegaBOB](https://redirect.github.com/stegaBOB) in
[https://github.com/Swatinem/rust-cache/pull/247](https://redirect.github.com/Swatinem/rust-cache/pull/247)

##### New Contributors

- [@&#8203;jbransen](https://redirect.github.com/jbransen) made their
first contribution in
[https://github.com/Swatinem/rust-cache/pull/246](https://redirect.github.com/Swatinem/rust-cache/pull/246)
- [@&#8203;stegaBOB](https://redirect.github.com/stegaBOB) made their
first contribution in
[https://github.com/Swatinem/rust-cache/pull/247](https://redirect.github.com/Swatinem/rust-cache/pull/247)

**Full Changelog**:
https://github.com/Swatinem/rust-cache/compare/v2.7.8...v2.8.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMTcuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 08:50:29 +05:30
Alex Waygood
08d8819c8a [ty] Fix descriptor lookups for most types that overlap with None (#19120) 2025-07-05 19:34:23 +01:00
Alex Waygood
44f2f77748 [ty] Add a DateType benchmark (#19148)
## Summary

The [`DateType`](https://github.com/glyph/DateType) library has some
very large protocols in it. Currently we type-check it quite quickly,
but the current version of https://github.com/astral-sh/ruff/pull/18659
makes our execution time on this library pathologically slow. That PR
doesn't seem to have a big impact on any of our current benchmarks,
however, so it seems we have some missing coverage in this area; I
therefore propose that we add `DateType` as a benchmark.

Currently the benchmark runs pretty quickly (about half the runtime of
attrs, which is our fastest real-world benchmark currently), and the
library has 0 third-party dependencies, so the benchmark is quick to
setup.

## Test Plan

`cargo bench -p ruff_benchmark --bench=ty`
2025-07-04 21:11:47 +01:00
NamelessGO
1c710c2840 Add Weblate to Who's Using Ruff (#19124)
https://github.com/WeblateOrg/weblate/issues/8867
2025-07-04 15:24:44 -04:00
Abhijeet Prasad Bodas
f4bd74ab6a [ty] Correctly handle calls to functions marked as returning Never / NoReturn (#18333)
## Summary

`ty` does not understand that calls to functions which have been
annotated as having a return type of `Never` / `NoReturn` are terminal.

This PR fixes that, by adding new reachability constraints when call
expressions are seen. If the call expression evaluates to `Never`, the
code following it will be considered to be unreachable. Note that, for
adding these constraints, we only consider call expressions at the
statement level, and that too only inside function scopes. This is
because otherwise, the number of such constraints becomes too high, and
evaluating them later on during type inference results in a major
performance degradation.

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

## Test Plan

New mdtests.

## Ecosystem changes

This PR removes the following false-positives:
- "Function can implicitly return `None`, which is not assignable to
...".
- "Name `foo` used when possibly not defind" - because the branch in
which it is not defined has a `NoReturn` call, or when `foo` was
imported in a `try`, and the except had a `NoReturn` call.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-04 11:52:52 -07:00
GiGaGon
a33cff2b12 Fix F701 to F707 errors in tests (#19125)
## Summary

Per @ntBre in https://github.com/astral-sh/ruff/pull/19111, it would be
a good idea to make the tests no longer have these syntax errors, so
this PR updates the tests and snapshots.

`B031` gave me a lot of trouble since the ending test of declaring a
function named `groupby` makes it so that inside other functions, it's
unclear which `groupby` is referred to since it depends on when the
function is called. To fix it I made each function have it's own `from
itertools import groupby` so there's no more ambiguity.
2025-07-04 13:43:18 -05:00
GiGaGon
f48a34fbab [pylint, pyupgrade] Fix syntax errors in examples (PLW1501, UP028) (#19127)
## Summary

From me and @ntBre's discussion in #19111.

This PR makes these two examples into valid code, since they previously
had `F701`-`F707` syntax errors. `SIM110` was already fixed in a
different PR, I just forgot to pull.
2025-07-04 13:38:37 -05:00
Carl Meyer
411cccb35e [ty] detect cycles in Type::is_disjoint_from (#19139) 2025-07-04 06:31:44 -07:00
Carl Meyer
7712c2fd15 [ty] don't allow first-party code to shadow stdlib types module (#19128) 2025-07-04 10:36:26 +00:00
David Peter
25bdb67d9a [ty] Remove TODOs regarding legacy generics (#19141) 2025-07-04 10:45:06 +02:00
Matthew Mckee
3be83d36a5 [ty] Add into_callable method for Type (#19130)
## Summary

Was just playing around with this, there's definitely more to do with
this function, but it seems like maybe a better option than having so
many arms in has_relation_to for (_, Callable).

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-07-03 19:04:03 -07:00
135 changed files with 6138 additions and 2431 deletions

View File

@@ -214,7 +214,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: |
rustup component add clippy
@@ -234,17 +234,17 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
with:
tool: cargo-insta
- name: ty mdtests (GitHub annotations)
@@ -292,17 +292,17 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
with:
tool: cargo-insta
- name: "Run tests"
@@ -321,11 +321,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
with:
tool: cargo-nextest
- name: "Run tests"
@@ -348,7 +348,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
@@ -377,7 +377,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -400,7 +400,7 @@ jobs:
with:
file: "Cargo.toml"
field: "workspace.package.rust-version"
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
env:
MSRV: ${{ steps.msrv.outputs.value }}
@@ -408,11 +408,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
with:
tool: cargo-insta
- name: "Run tests"
@@ -432,7 +432,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "fuzz -> target"
- name: "Install Rust toolchain"
@@ -494,7 +494,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -708,7 +708,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
@@ -732,7 +732,7 @@ jobs:
with:
persist-credentials: false
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
@@ -765,7 +765,7 @@ jobs:
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
@@ -804,7 +804,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Run checks"
@@ -874,7 +874,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
@@ -905,14 +905,14 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
with:
tool: cargo-codspeed
@@ -938,14 +938,14 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
with:
tool: cargo-codspeed

View File

@@ -39,7 +39,7 @@ jobs:
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: Build ruff
# A debug build means the script runs slower once it gets started,
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI

View File

@@ -39,7 +39,7 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "ruff"
@@ -49,46 +49,12 @@ jobs:
- name: Run mypy_primer
shell: bash
env:
TY_MEMORY_REPORT: mypy_primer
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
DIFF_FILE: mypy_primer.diff
run: |
cd ruff
echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)"
mkdir -p ~/.config/ty
cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml
PRIMER_SELECTOR="$(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt)"
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b base_commit "$MERGE_BASE"
echo "base commit"
git rev-list --format=%s --max-count=1 base_commit
cd ..
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx \
--from="git+https://github.com/hauntsaninja/mypy_primer@e5f55447969d33ae3c7ccdb183e2a37101867270" \
mypy_primer \
--repo ruff \
--type-checker ty \
--old base_commit \
--new "$GITHUB_SHA" \
--project-selector "/($PRIMER_SELECTOR)\$" \
--output concise \
--debug > mypy_primer.diff || [ $? -eq 1 ]
# Output diff with ANSI color codes
cat mypy_primer.diff
# Remove ANSI color codes before uploading
sed -ie 's/\x1b\[[0-9;]*m//g' mypy_primer.diff
echo ${{ github.event.number }} > pr-number
scripts/mypy_primer.sh
echo ${{ github.event.number }} > ../pr-number
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
@@ -101,3 +67,41 @@ jobs:
with:
name: pr-number
path: pr-number
memory_usage:
name: Run memory statistics
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Run mypy_primer
shell: bash
env:
TY_MAX_PARALLELISM: 1 # for deterministic memory numbers
TY_MEMORY_REPORT: mypy_primer
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/memory.txt
DIFF_FILE: mypy_primer_memory.diff
run: |
cd ruff
scripts/mypy_primer.sh
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: mypy_primer_memory_diff
path: mypy_primer_memory.diff

View File

@@ -45,15 +45,28 @@ jobs:
if_no_artifact_found: ignore
allow_forks: true
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: "Download mypy_primer memory results"
id: download-mypy_primer_memory_diff
if: steps.pr-number.outputs.pr-number
with:
name: mypy_primer_memory_diff
workflow: mypy_primer.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/mypy_primer_memory_diff
workflow_conclusion: completed
if_no_artifact_found: ignore
allow_forks: true
- name: Generate comment content
id: generate-comment
if: steps.download-mypy_primer_diff.outputs.found_artifact == 'true'
if: ${{ steps.download-mypy_primer_diff.outputs.found_artifact == 'true' && steps.download-mypy_primer_memory_diff.outputs.found_artifact == 'true' }}
run: |
# Guard against malicious mypy_primer results that symlink to a secret
# file on this runner
if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]]
if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]] || [[ -L pr/mypy_primer_memory_diff/mypy_primer_memory.diff ]]
then
echo "Error: mypy_primer.diff cannot be a symlink"
echo "Error: mypy_primer.diff and mypy_primer_memory.diff cannot be a symlink"
exit 1
fi
@@ -74,6 +87,18 @@ jobs:
echo 'No ecosystem changes detected ✅' >> comment.txt
fi
if [ -s "pr/mypy_primer_memory_diff/mypy_primer_memory.diff" ]; then
echo '<details>' >> comment.txt
echo '<summary>Memory usage changes were detected when running on open source projects</summary>' >> comment.txt
echo '' >> comment.txt
echo '```diff' >> comment.txt
cat pr/mypy_primer_memory_diff/mypy_primer_memory.diff >> comment.txt
echo '```' >> comment.txt
echo '</details>' >> comment.txt
else
echo 'No memory usage changes detected ✅' >> comment.txt
fi
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"

View File

@@ -68,7 +68,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}

View File

@@ -34,7 +34,7 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "ruff"

View File

@@ -67,7 +67,7 @@ repos:
- black==25.1.0
- repo: https://github.com/crate-ci/typos
rev: v1.33.1
rev: v1.34.0
hooks:
- id: typos
@@ -81,7 +81,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.1
rev: v0.12.2
hooks:
- id: ruff-format
- id: ruff
@@ -99,12 +99,12 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.10.0
rev: v1.11.0
hooks:
- id: zizmor
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.1
rev: 0.33.2
hooks:
- id: check-github-workflows

171
Cargo.lock generated
View File

@@ -591,7 +591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -600,7 +600,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -633,10 +633,22 @@ dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.1",
"windows-sys 0.59.0",
]
[[package]]
name = "console"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.1",
"windows-sys 0.60.2",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
@@ -1019,7 +1031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1481,14 +1493,14 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.17.11"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
dependencies = [
"console",
"number_prefix",
"console 0.16.0",
"portable-atomic",
"unicode-width 0.2.1",
"unit-prefix",
"vt100",
"web-time",
]
@@ -1525,7 +1537,7 @@ version = "1.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
dependencies = [
"console",
"console 0.15.11",
"globset",
"once_cell",
"pest",
@@ -1592,7 +1604,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.1",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1656,7 +1668,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2025,12 +2037,11 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "notify"
version = "8.0.0"
version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
dependencies = [
"bitflags 2.9.1",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
@@ -2039,7 +2050,7 @@ dependencies = [
"mio",
"notify-types",
"walkdir",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -2077,12 +2088,6 @@ dependencies = [
"libc",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -2166,7 +2171,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@@ -3407,7 +3412,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3603,9 +3608,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.12.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
dependencies = [
"serde",
"serde_derive",
@@ -3614,9 +3619,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.12.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [
"darling",
"proc-macro2",
@@ -3797,7 +3802,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4056,9 +4061,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
"valuable",
@@ -4077,9 +4082,9 @@ dependencies = [
[[package]]
name = "tracing-indicatif"
version = "0.3.9"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8201ca430e0cd893ef978226fd3516c06d9c494181c8bf4e5b32e30ed4b40aa1"
checksum = "8c714cc8fc46db04fcfddbd274c6ef59bebb1b435155984e7c6e89c3ce66f200"
dependencies = [
"indicatif",
"tracing",
@@ -4167,6 +4172,7 @@ dependencies = [
name = "ty_ide"
version = "0.0.0"
dependencies = [
"bitflags 2.9.1",
"insta",
"ruff_db",
"ruff_python_ast",
@@ -4473,6 +4479,12 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "unit-prefix"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
[[package]]
name = "unscanny"
version = "0.1.0"
@@ -4805,7 +4817,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4879,7 +4891,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@@ -4888,7 +4900,16 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.2",
]
[[package]]
@@ -4897,14 +4918,30 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
@@ -4913,48 +4950,96 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.10"

View File

@@ -98,7 +98,7 @@ ignore = { version = "0.4.22" }
imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" }
indexmap = { version = "2.6.0" }
indicatif = { version = "0.17.8" }
indicatif = { version = "0.18.0" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1" }
insta-cmd = { version = "0.6.0" }
@@ -167,7 +167,7 @@ tikv-jemallocator = { version = "0.6.0" }
toml = { version = "0.8.11" }
tracing = { version = "0.1.40" }
tracing-flame = { version = "0.2.0" }
tracing-indicatif = { version = "0.3.6" }
tracing-indicatif = { version = "0.3.11" }
tracing-log = { version = "0.2.0" }
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
"env-filter",

View File

@@ -506,6 +506,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Streamlit](https://github.com/streamlit/streamlit)
- [The Algorithms](https://github.com/TheAlgorithms/Python)
- [Vega-Altair](https://github.com/altair-viz/altair)
- [Weblate](https://weblate.org/)
- WordPress ([Openverse](https://github.com/WordPress/openverse))
- [ZenML](https://github.com/zenml-io/zenml)
- [Zulip](https://github.com/zulip/zulip)

View File

@@ -498,11 +498,8 @@ fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) {
let diagnostics = result.len();
assert!(
diagnostics > 1 && diagnostics <= max_diagnostics,
"Expected between {} and {} diagnostics but got {}",
1,
max_diagnostics,
diagnostics
diagnostics <= max_diagnostics,
"Expected <={max_diagnostics} diagnostics but got {diagnostics}"
);
}
@@ -570,6 +567,23 @@ fn anyio(criterion: &mut Criterion) {
bench_project(&benchmark, criterion);
}
fn datetype(criterion: &mut Criterion) {
let benchmark = ProjectBenchmark::new(
RealWorldProject {
name: "DateType",
repository: "https://github.com/glyph/DateType",
commit: "57c9c93cf2468069f72945fc04bf27b64100dad8",
paths: vec![SystemPath::new("src")],
dependencies: vec![],
max_dep_date: "2025-07-04",
python_version: PythonVersion::PY313,
},
0,
);
bench_project(&benchmark, criterion);
}
criterion_group!(check_file, benchmark_cold, benchmark_incremental);
criterion_group!(
micro,
@@ -578,5 +592,5 @@ criterion_group!(
benchmark_complex_constrained_attributes_1,
benchmark_complex_constrained_attributes_2,
);
criterion_group!(project, anyio, attrs, hydra);
criterion_group!(project, anyio, attrs, hydra, datetype);
criterion_main!(check_file, micro, project);

View File

@@ -242,20 +242,19 @@ fn large(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
// Currently disabled because the benchmark is too noisy (± 10%) to give useful feedback.
// #[bench(args=[&*PYDANTIC], sample_size=3, sample_count=3)]
// fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
// let thread_pool = ThreadPoolBuilder::new().build().unwrap();
#[bench(args=[&*PYDANTIC], sample_size=3, sample_count=8)]
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
// bencher
// .with_inputs(|| benchmark.setup_iteration())
// .bench_local_values(|db| {
// thread_pool.install(|| {
// check_project(&db, benchmark.max_diagnostics);
// db
// })
// });
// }
bencher
.with_inputs(|| benchmark.setup_iteration())
.bench_local_values(|db| {
thread_pool.install(|| {
check_project(&db, benchmark.max_diagnostics);
db
})
});
}
fn main() {
ThreadPoolBuilder::new()

View File

@@ -1,29 +1,30 @@
try:
pass
except Exception:
continue
for _ in []:
try:
pass
except Exception:
continue
try:
pass
except:
continue
try:
pass
except:
continue
try:
pass
except (Exception,):
continue
try:
pass
except (Exception,):
continue
try:
pass
except (Exception, ValueError):
continue
try:
pass
except (Exception, ValueError):
continue
try:
pass
except ValueError:
continue
try:
pass
except ValueError:
continue
try:
pass
except (ValueError,):
continue
try:
pass
except (ValueError,):
continue

View File

@@ -185,38 +185,45 @@ for _section, section_items in groupby(items, key=lambda p: p[1]):
collect_shop_items(shopper, section_items)
# Shouldn't trigger the warning when there is a return statement.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
def foo():
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
return section_items
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
return section_items
collect_shop_items(shopper, section_items)
# Should trigger the warning for duplicate access, even if is a return statement after.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
return
def foo():
from itertools import groupby
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
return
# Should trigger the warning for duplicate access, even if is a return in another branch.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
def foo():
from itertools import groupby
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
# Should trigger, since only one branch has a return statement.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items) # B031
def foo():
from itertools import groupby
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items) # B031
# Let's redefine the `groupby` function to make sure we pick up the correct one.
# NOTE: This should always be at the end of the file.

View File

@@ -26,8 +26,9 @@ abc(**{'a': b}, **{'a': c}) # PIE804
abc(a=1, **{'a': c}, **{'b': c}) # PIE804
# Some values need to be parenthesized.
abc(foo=1, **{'bar': (bar := 1)}) # PIE804
abc(foo=1, **{'bar': (yield 1)}) # PIE804
def foo():
abc(foo=1, **{'bar': (bar := 1)}) # PIE804
abc(foo=1, **{'bar': (yield 1)}) # PIE804
# https://github.com/astral-sh/ruff/issues/18036
# The autofix for this is unsafe due to the comments inside the dictionary.

View File

@@ -27,8 +27,9 @@ with contextlib.ExitStack() as stack:
close_files = stack.pop_all().close
# OK
with contextlib.AsyncExitStack() as exit_stack:
f = await exit_stack.enter_async_context(open("filename"))
async def foo():
with contextlib.AsyncExitStack() as exit_stack:
f = await exit_stack.enter_async_context(open("filename"))
# OK (false negative)
with contextlib.ExitStack():
@@ -275,9 +276,10 @@ class ExampleClassTests(TestCase):
cls.enterClassContext(open("filename"))
# OK
class ExampleAsyncTests(IsolatedAsyncioTestCase):
async def test_something(self):
await self.enterAsyncContext(open("filename"))
async def foo():
class ExampleAsyncTests(IsolatedAsyncioTestCase):
async def test_something(self):
await self.enterAsyncContext(open("filename"))
# OK
class ExampleTests(TestCase):

View File

@@ -1,98 +1,99 @@
# Errors
a = "hello"
def foo():
# Errors
a = "hello"
# SIM116
if a == "foo":
return "bar"
elif a == "bar":
return "baz"
elif a == "boo":
return "ooh"
else:
return 42
# SIM116
if a == 1:
return (1, 2, 3)
elif a == 2:
return (4, 5, 6)
elif a == 3:
return (7, 8, 9)
else:
return (10, 11, 12)
# SIM116
if a == 1:
return (1, 2, 3)
elif a == 2:
return (4, 5, 6)
elif a == 3:
return (7, 8, 9)
# SIM116
if a == "hello 'sir'":
return (1, 2, 3)
elif a == 'goodbye "mam"':
return (4, 5, 6)
elif a == """Fairwell 'mister'""":
return (7, 8, 9)
else:
return (10, 11, 12)
# SIM116
if a == b"one":
return 1
elif a == b"two":
return 2
elif a == b"three":
return 3
# SIM116
if a == "hello 'sir'":
return ("hello'", 'hi"', 3)
elif a == 'goodbye "mam"':
return (4, 5, 6)
elif a == """Fairwell 'mister'""":
return (7, 8, 9)
else:
return (10, 11, 12)
# OK
if a == "foo":
return "bar"
elif a == "bar":
return baz()
elif a == "boo":
return "ooh"
else:
return 42
# OK
if a == b"one":
return 1
elif b == b"two":
return 2
elif a == b"three":
return 3
# SIM116
if func_name == "create":
return "A"
elif func_name == "modify":
return "M"
elif func_name == "remove":
return "D"
elif func_name == "move":
return "MV"
# OK
def no_return_in_else(platform):
if platform == "linux":
return "auditwheel repair -w {dest_dir} {wheel}"
elif platform == "macos":
return "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
elif platform == "windows":
return ""
# SIM116
if a == "foo":
return "bar"
elif a == "bar":
return "baz"
elif a == "boo":
return "ooh"
else:
msg = f"Unknown platform: {platform!r}"
raise ValueError(msg)
return 42
# SIM116
if a == 1:
return (1, 2, 3)
elif a == 2:
return (4, 5, 6)
elif a == 3:
return (7, 8, 9)
else:
return (10, 11, 12)
# SIM116
if a == 1:
return (1, 2, 3)
elif a == 2:
return (4, 5, 6)
elif a == 3:
return (7, 8, 9)
# SIM116
if a == "hello 'sir'":
return (1, 2, 3)
elif a == 'goodbye "mam"':
return (4, 5, 6)
elif a == """Fairwell 'mister'""":
return (7, 8, 9)
else:
return (10, 11, 12)
# SIM116
if a == b"one":
return 1
elif a == b"two":
return 2
elif a == b"three":
return 3
# SIM116
if a == "hello 'sir'":
return ("hello'", 'hi"', 3)
elif a == 'goodbye "mam"':
return (4, 5, 6)
elif a == """Fairwell 'mister'""":
return (7, 8, 9)
else:
return (10, 11, 12)
# OK
if a == "foo":
return "bar"
elif a == "bar":
return baz()
elif a == "boo":
return "ooh"
else:
return 42
# OK
if a == b"one":
return 1
elif b == b"two":
return 2
elif a == b"three":
return 3
# SIM116
if func_name == "create":
return "A"
elif func_name == "modify":
return "M"
elif func_name == "remove":
return "D"
elif func_name == "move":
return "MV"
# OK
def no_return_in_else(platform):
if platform == "linux":
return "auditwheel repair -w {dest_dir} {wheel}"
elif platform == "macos":
return "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
elif platform == "windows":
return ""
else:
msg = f"Unknown platform: {platform!r}"
raise ValueError(msg)

View File

@@ -50,3 +50,23 @@ class Baz:
class Nested:
a: TypeAlias = 'Baz' # OK
type A = 'Baz' # TC008
# O should have parenthesis added
o: TypeAlias = """int
| None"""
type O = """int
| None"""
# P, Q, and R should not have parenthesis added
p: TypeAlias = ("""int
| None""")
type P = ("""int
| None""")
q: TypeAlias = """(int
| None)"""
type Q = """(int
| None)"""
r: TypeAlias = """int | None"""
type R = """int | None"""

View File

@@ -1,4 +1,4 @@
import os.path
import os.path, pathlib
from pathlib import Path
from os.path import getatime
@@ -10,3 +10,26 @@ os.path.getatime(Path("filename"))
getatime("filename")
getatime(b"filename")
getatime(Path("filename"))
file = __file__
os.path.getatime(file)
os.path.getatime(filename="filename")
os.path.getatime(filename=Path("filename"))
os.path.getatime( # comment 1
# comment 2
"filename" # comment 3
# comment 4
, # comment 5
# comment 6
) # comment 7
os.path.getatime("file" + "name")
getatime(Path("filename").resolve())
os.path.getatime(pathlib.Path("filename"))
getatime(Path("dir") / "file.txt")

View File

@@ -81,4 +81,5 @@ match(foo):
# https://github.com/astral-sh/ruff/issues/12094
pass;
yield, x
def foo():
yield, x

View File

@@ -1062,9 +1062,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::OsPathSplitext,
Rule::BuiltinOpen,
Rule::PyPath,
Rule::OsPathGetatime,
Rule::OsPathGetmtime,
Rule::OsPathGetctime,
Rule::Glob,
Rule::OsListdir,
Rule::OsSymlink,
@@ -1074,6 +1071,15 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::OsPathGetsize) {
flake8_use_pathlib::rules::os_path_getsize(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetatime) {
flake8_use_pathlib::rules::os_path_getatime(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetctime) {
flake8_use_pathlib::rules::os_path_getctime(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetmtime) {
flake8_use_pathlib::rules::os_path_getmtime(checker, call);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, call);
}

View File

@@ -54,6 +54,20 @@ pub(crate) const fn is_fix_manual_list_comprehension_enabled(settings: &LinterSe
pub(crate) const fn is_fix_os_path_getsize_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18922
pub(crate) const fn is_fix_os_path_getmtime_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18922
pub(crate) const fn is_fix_os_path_getatime_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18922
pub(crate) const fn is_fix_os_path_getctime_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/11436
// https://github.com/astral-sh/ruff/pull/11168

View File

@@ -1,46 +1,46 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S112.py:3:1: S112 `try`-`except`-`continue` detected, consider logging the exception
S112.py:4:5: S112 `try`-`except`-`continue` detected, consider logging the exception
|
1 | try:
2 | pass
3 | / except Exception:
4 | | continue
| |____________^ S112
5 |
6 | try:
2 | try:
3 | pass
4 | / except Exception:
5 | | continue
| |________________^ S112
6 |
7 | try:
|
S112.py:8:1: S112 `try`-`except`-`continue` detected, consider logging the exception
S112.py:9:5: S112 `try`-`except`-`continue` detected, consider logging the exception
|
6 | try:
7 | pass
8 | / except:
9 | | continue
| |____________^ S112
10 |
11 | try:
7 | try:
8 | pass
9 | / except:
10 | | continue
| |________________^ S112
11 |
12 | try:
|
S112.py:13:1: S112 `try`-`except`-`continue` detected, consider logging the exception
S112.py:14:5: S112 `try`-`except`-`continue` detected, consider logging the exception
|
11 | try:
12 | pass
13 | / except (Exception,):
14 | | continue
| |____________^ S112
15 |
16 | try:
12 | try:
13 | pass
14 | / except (Exception,):
15 | | continue
| |________________^ S112
16 |
17 | try:
|
S112.py:18:1: S112 `try`-`except`-`continue` detected, consider logging the exception
S112.py:19:5: S112 `try`-`except`-`continue` detected, consider logging the exception
|
16 | try:
17 | pass
18 | / except (Exception, ValueError):
19 | | continue
| |____________^ S112
20 |
21 | try:
17 | try:
18 | pass
19 | / except (Exception, ValueError):
20 | | continue
| |________________^ S112
21 |
22 | try:
|

View File

@@ -195,31 +195,31 @@ B031.py:144:33: B031 Using the generator returned from `itertools.groupby()` mor
146 | for group in groupby(items, key=lambda p: p[1]):
|
B031.py:200:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
B031.py:203:41: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
198 | if _section == "greens":
199 | collect_shop_items(shopper, section_items)
200 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
201 | return
201 | if _section == "greens":
202 | collect_shop_items(shopper, section_items)
203 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
204 | return
|
B031.py:210:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
B031.py:215:41: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
208 | elif _section == "frozen items":
209 | collect_shop_items(shopper, section_items)
210 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
211 |
212 | # Should trigger, since only one branch has a return statement.
213 | elif _section == "frozen items":
214 | collect_shop_items(shopper, section_items)
215 | collect_shop_items(shopper, section_items)
| ^^^^^^^^^^^^^ B031
216 |
217 | # Should trigger, since only one branch has a return statement.
|
B031.py:219:33: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
B031.py:226:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage
|
217 | elif _section == "frozen items":
218 | collect_shop_items(shopper, section_items)
219 | collect_shop_items(shopper, section_items) # B031
| ^^^^^^^^^^^^^ B031
220 |
221 | # Let's redefine the `groupby` function to make sure we pick up the correct one.
224 | elif _section == "frozen items":
225 | collect_shop_items(shopper, section_items)
226 | collect_shop_items(shopper, section_items) # B031
| ^^^^^^^^^^^^^ B031
227 |
228 | # Let's redefine the `groupby` function to make sure we pick up the correct one.
|

View File

@@ -190,72 +190,73 @@ PIE804.py:26:22: PIE804 [*] Unnecessary `dict` kwargs
26 |+abc(a=1, **{'a': c}, b=c) # PIE804
27 27 |
28 28 | # Some values need to be parenthesized.
29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
29 29 | def foo():
PIE804.py:29:12: PIE804 [*] Unnecessary `dict` kwargs
PIE804.py:30:16: PIE804 [*] Unnecessary `dict` kwargs
|
28 | # Some values need to be parenthesized.
29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
| ^^^^^^^^^^^^^^^^^^^^^ PIE804
30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
= help: Remove unnecessary kwargs
Safe fix
26 26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
27 27 |
28 28 | # Some values need to be parenthesized.
29 |-abc(foo=1, **{'bar': (bar := 1)}) # PIE804
29 |+abc(foo=1, bar=(bar := 1)) # PIE804
30 30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
31 31 |
32 32 | # https://github.com/astral-sh/ruff/issues/18036
PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs
|
28 | # Some values need to be parenthesized.
29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
| ^^^^^^^^^^^^^^^^^^^^ PIE804
31 |
32 | # https://github.com/astral-sh/ruff/issues/18036
29 | def foo():
30 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
| ^^^^^^^^^^^^^^^^^^^^^ PIE804
31 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
= help: Remove unnecessary kwargs
Safe fix
27 27 |
28 28 | # Some values need to be parenthesized.
29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
30 |-abc(foo=1, **{'bar': (yield 1)}) # PIE804
30 |+abc(foo=1, bar=(yield 1)) # PIE804
31 31 |
32 32 | # https://github.com/astral-sh/ruff/issues/18036
33 33 | # The autofix for this is unsafe due to the comments inside the dictionary.
29 29 | def foo():
30 |- abc(foo=1, **{'bar': (bar := 1)}) # PIE804
30 |+ abc(foo=1, bar=(bar := 1)) # PIE804
31 31 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
32 32 |
33 33 | # https://github.com/astral-sh/ruff/issues/18036
PIE804.py:35:5: PIE804 [*] Unnecessary `dict` kwargs
PIE804.py:31:16: PIE804 [*] Unnecessary `dict` kwargs
|
33 | # The autofix for this is unsafe due to the comments inside the dictionary.
34 | foo(
35 | / **{
36 | | # Comment 1
37 | | "x": 1.0,
38 | | # Comment 2
39 | | "y": 2.0,
40 | | }
29 | def foo():
30 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
31 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
| ^^^^^^^^^^^^^^^^^^^^ PIE804
32 |
33 | # https://github.com/astral-sh/ruff/issues/18036
|
= help: Remove unnecessary kwargs
Safe fix
28 28 | # Some values need to be parenthesized.
29 29 | def foo():
30 30 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
31 |- abc(foo=1, **{'bar': (yield 1)}) # PIE804
31 |+ abc(foo=1, bar=(yield 1)) # PIE804
32 32 |
33 33 | # https://github.com/astral-sh/ruff/issues/18036
34 34 | # The autofix for this is unsafe due to the comments inside the dictionary.
PIE804.py:36:5: PIE804 [*] Unnecessary `dict` kwargs
|
34 | # The autofix for this is unsafe due to the comments inside the dictionary.
35 | foo(
36 | / **{
37 | | # Comment 1
38 | | "x": 1.0,
39 | | # Comment 2
40 | | "y": 2.0,
41 | | }
| |_____^ PIE804
41 | )
42 | )
|
= help: Remove unnecessary kwargs
Unsafe fix
32 32 | # https://github.com/astral-sh/ruff/issues/18036
33 33 | # The autofix for this is unsafe due to the comments inside the dictionary.
34 34 | foo(
35 |- **{
36 |- # Comment 1
37 |- "x": 1.0,
38 |- # Comment 2
39 |- "y": 2.0,
40 |- }
35 |+ x=1.0, y=2.0
41 36 | )
33 33 | # https://github.com/astral-sh/ruff/issues/18036
34 34 | # The autofix for this is unsafe due to the comments inside the dictionary.
35 35 | foo(
36 |- **{
37 |- # Comment 1
38 |- "x": 1.0,
39 |- # Comment 2
40 |- "y": 2.0,
41 |- }
36 |+ x=1.0, y=2.0
42 37 | )

View File

@@ -21,7 +21,9 @@ use crate::registry::Rule;
///
/// ## Example
/// ```pyi
/// if sys.platform.startswith("linux"):
/// import sys
///
/// if sys.platform == "xunil"[::-1]:
/// # Linux specific definitions
/// ...
/// else:
@@ -31,6 +33,8 @@ use crate::registry::Rule;
///
/// Instead, use a simple string comparison, such as `==` or `!=`:
/// ```pyi
/// import sys
///
/// if sys.platform == "linux":
/// # Linux specific definitions
/// ...
@@ -65,11 +69,15 @@ impl Violation for UnrecognizedPlatformCheck {
///
/// ## Example
/// ```pyi
/// import sys
///
/// if sys.platform == "linus": ...
/// ```
///
/// Use instead:
/// ```pyi
/// import sys
///
/// if sys.platform == "linux": ...
/// ```
///

View File

@@ -18,17 +18,22 @@ use crate::checkers::ast::Checker;
///
/// ## Example
/// ```python
/// if x == 1:
/// return "Hello"
/// elif x == 2:
/// return "Goodbye"
/// else:
/// return "Goodnight"
/// def find_phrase(x):
/// if x == 1:
/// return "Hello"
/// elif x == 2:
/// return "Goodbye"
/// elif x == 3:
/// return "Good morning"
/// else:
/// return "Goodnight"
/// ```
///
/// Use instead:
/// ```python
/// return {1: "Hello", 2: "Goodbye"}.get(x, "Goodnight")
/// def find_phrase(x):
/// phrases = {1: "Hello", 2: "Goodye", 3: "Good morning"}
/// return phrases.get(x, "Goodnight")
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct IfElseBlockInsteadOfDictLookup;

View File

@@ -50,285 +50,285 @@ SIM115.py:12:5: SIM115 Use a context manager for opening files
14 | f.close()
|
SIM115.py:39:9: SIM115 Use a context manager for opening files
SIM115.py:40:9: SIM115 Use a context manager for opening files
|
37 | # SIM115
38 | with contextlib.ExitStack():
39 | f = open("filename")
38 | # SIM115
39 | with contextlib.ExitStack():
40 | f = open("filename")
| ^^^^ SIM115
40 |
41 | # OK
|
SIM115.py:80:5: SIM115 Use a context manager for opening files
|
78 | import fileinput
79 |
80 | f = tempfile.NamedTemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
81 | f = tempfile.TemporaryFile()
82 | f = tempfile.SpooledTemporaryFile()
41 |
42 | # OK
|
SIM115.py:81:5: SIM115 Use a context manager for opening files
|
80 | f = tempfile.NamedTemporaryFile()
81 | f = tempfile.TemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
82 | f = tempfile.SpooledTemporaryFile()
83 | f = tarfile.open("foo.tar")
79 | import fileinput
80 |
81 | f = tempfile.NamedTemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
82 | f = tempfile.TemporaryFile()
83 | f = tempfile.SpooledTemporaryFile()
|
SIM115.py:82:5: SIM115 Use a context manager for opening files
|
80 | f = tempfile.NamedTemporaryFile()
81 | f = tempfile.TemporaryFile()
82 | f = tempfile.SpooledTemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
83 | f = tarfile.open("foo.tar")
84 | f = TarFile("foo.tar").open()
81 | f = tempfile.NamedTemporaryFile()
82 | f = tempfile.TemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
83 | f = tempfile.SpooledTemporaryFile()
84 | f = tarfile.open("foo.tar")
|
SIM115.py:83:5: SIM115 Use a context manager for opening files
|
81 | f = tempfile.TemporaryFile()
82 | f = tempfile.SpooledTemporaryFile()
83 | f = tarfile.open("foo.tar")
| ^^^^^^^^^^^^ SIM115
84 | f = TarFile("foo.tar").open()
85 | f = tarfile.TarFile("foo.tar").open()
81 | f = tempfile.NamedTemporaryFile()
82 | f = tempfile.TemporaryFile()
83 | f = tempfile.SpooledTemporaryFile()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
84 | f = tarfile.open("foo.tar")
85 | f = TarFile("foo.tar").open()
|
SIM115.py:84:5: SIM115 Use a context manager for opening files
|
82 | f = tempfile.SpooledTemporaryFile()
83 | f = tarfile.open("foo.tar")
84 | f = TarFile("foo.tar").open()
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
85 | f = tarfile.TarFile("foo.tar").open()
86 | f = tarfile.TarFile().open()
82 | f = tempfile.TemporaryFile()
83 | f = tempfile.SpooledTemporaryFile()
84 | f = tarfile.open("foo.tar")
| ^^^^^^^^^^^^ SIM115
85 | f = TarFile("foo.tar").open()
86 | f = tarfile.TarFile("foo.tar").open()
|
SIM115.py:85:5: SIM115 Use a context manager for opening files
|
83 | f = tarfile.open("foo.tar")
84 | f = TarFile("foo.tar").open()
85 | f = tarfile.TarFile("foo.tar").open()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
86 | f = tarfile.TarFile().open()
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
83 | f = tempfile.SpooledTemporaryFile()
84 | f = tarfile.open("foo.tar")
85 | f = TarFile("foo.tar").open()
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
86 | f = tarfile.TarFile("foo.tar").open()
87 | f = tarfile.TarFile().open()
|
SIM115.py:86:5: SIM115 Use a context manager for opening files
|
84 | f = TarFile("foo.tar").open()
85 | f = tarfile.TarFile("foo.tar").open()
86 | f = tarfile.TarFile().open()
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
88 | f = io.open("foo.txt")
84 | f = tarfile.open("foo.tar")
85 | f = TarFile("foo.tar").open()
86 | f = tarfile.TarFile("foo.tar").open()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
87 | f = tarfile.TarFile().open()
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
|
SIM115.py:87:5: SIM115 Use a context manager for opening files
|
85 | f = tarfile.TarFile("foo.tar").open()
86 | f = tarfile.TarFile().open()
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
88 | f = io.open("foo.txt")
89 | f = io.open_code("foo.txt")
85 | f = TarFile("foo.tar").open()
86 | f = tarfile.TarFile("foo.tar").open()
87 | f = tarfile.TarFile().open()
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
89 | f = io.open("foo.txt")
|
SIM115.py:88:5: SIM115 Use a context manager for opening files
|
86 | f = tarfile.TarFile().open()
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
88 | f = io.open("foo.txt")
| ^^^^^^^ SIM115
89 | f = io.open_code("foo.txt")
90 | f = codecs.open("foo.txt")
86 | f = tarfile.TarFile("foo.tar").open()
87 | f = tarfile.TarFile().open()
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
89 | f = io.open("foo.txt")
90 | f = io.open_code("foo.txt")
|
SIM115.py:89:5: SIM115 Use a context manager for opening files
|
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
88 | f = io.open("foo.txt")
89 | f = io.open_code("foo.txt")
| ^^^^^^^^^^^^ SIM115
90 | f = codecs.open("foo.txt")
91 | f = bz2.open("foo.txt")
87 | f = tarfile.TarFile().open()
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
89 | f = io.open("foo.txt")
| ^^^^^^^ SIM115
90 | f = io.open_code("foo.txt")
91 | f = codecs.open("foo.txt")
|
SIM115.py:90:5: SIM115 Use a context manager for opening files
|
88 | f = io.open("foo.txt")
89 | f = io.open_code("foo.txt")
90 | f = codecs.open("foo.txt")
| ^^^^^^^^^^^ SIM115
91 | f = bz2.open("foo.txt")
92 | f = gzip.open("foo.txt")
88 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
89 | f = io.open("foo.txt")
90 | f = io.open_code("foo.txt")
| ^^^^^^^^^^^^ SIM115
91 | f = codecs.open("foo.txt")
92 | f = bz2.open("foo.txt")
|
SIM115.py:91:5: SIM115 Use a context manager for opening files
|
89 | f = io.open_code("foo.txt")
90 | f = codecs.open("foo.txt")
91 | f = bz2.open("foo.txt")
| ^^^^^^^^ SIM115
92 | f = gzip.open("foo.txt")
93 | f = dbm.open("foo.db")
89 | f = io.open("foo.txt")
90 | f = io.open_code("foo.txt")
91 | f = codecs.open("foo.txt")
| ^^^^^^^^^^^ SIM115
92 | f = bz2.open("foo.txt")
93 | f = gzip.open("foo.txt")
|
SIM115.py:92:5: SIM115 Use a context manager for opening files
|
90 | f = codecs.open("foo.txt")
91 | f = bz2.open("foo.txt")
92 | f = gzip.open("foo.txt")
| ^^^^^^^^^ SIM115
93 | f = dbm.open("foo.db")
94 | f = dbm.gnu.open("foo.db")
90 | f = io.open_code("foo.txt")
91 | f = codecs.open("foo.txt")
92 | f = bz2.open("foo.txt")
| ^^^^^^^^ SIM115
93 | f = gzip.open("foo.txt")
94 | f = dbm.open("foo.db")
|
SIM115.py:93:5: SIM115 Use a context manager for opening files
|
91 | f = bz2.open("foo.txt")
92 | f = gzip.open("foo.txt")
93 | f = dbm.open("foo.db")
| ^^^^^^^^ SIM115
94 | f = dbm.gnu.open("foo.db")
95 | f = dbm.ndbm.open("foo.db")
91 | f = codecs.open("foo.txt")
92 | f = bz2.open("foo.txt")
93 | f = gzip.open("foo.txt")
| ^^^^^^^^^ SIM115
94 | f = dbm.open("foo.db")
95 | f = dbm.gnu.open("foo.db")
|
SIM115.py:94:5: SIM115 Use a context manager for opening files
|
92 | f = gzip.open("foo.txt")
93 | f = dbm.open("foo.db")
94 | f = dbm.gnu.open("foo.db")
| ^^^^^^^^^^^^ SIM115
95 | f = dbm.ndbm.open("foo.db")
96 | f = dbm.dumb.open("foo.db")
92 | f = bz2.open("foo.txt")
93 | f = gzip.open("foo.txt")
94 | f = dbm.open("foo.db")
| ^^^^^^^^ SIM115
95 | f = dbm.gnu.open("foo.db")
96 | f = dbm.ndbm.open("foo.db")
|
SIM115.py:95:5: SIM115 Use a context manager for opening files
|
93 | f = dbm.open("foo.db")
94 | f = dbm.gnu.open("foo.db")
95 | f = dbm.ndbm.open("foo.db")
| ^^^^^^^^^^^^^ SIM115
96 | f = dbm.dumb.open("foo.db")
97 | f = lzma.open("foo.xz")
93 | f = gzip.open("foo.txt")
94 | f = dbm.open("foo.db")
95 | f = dbm.gnu.open("foo.db")
| ^^^^^^^^^^^^ SIM115
96 | f = dbm.ndbm.open("foo.db")
97 | f = dbm.dumb.open("foo.db")
|
SIM115.py:96:5: SIM115 Use a context manager for opening files
|
94 | f = dbm.gnu.open("foo.db")
95 | f = dbm.ndbm.open("foo.db")
96 | f = dbm.dumb.open("foo.db")
94 | f = dbm.open("foo.db")
95 | f = dbm.gnu.open("foo.db")
96 | f = dbm.ndbm.open("foo.db")
| ^^^^^^^^^^^^^ SIM115
97 | f = lzma.open("foo.xz")
98 | f = lzma.LZMAFile("foo.xz")
97 | f = dbm.dumb.open("foo.db")
98 | f = lzma.open("foo.xz")
|
SIM115.py:97:5: SIM115 Use a context manager for opening files
|
95 | f = dbm.ndbm.open("foo.db")
96 | f = dbm.dumb.open("foo.db")
97 | f = lzma.open("foo.xz")
| ^^^^^^^^^ SIM115
98 | f = lzma.LZMAFile("foo.xz")
99 | f = shelve.open("foo.db")
95 | f = dbm.gnu.open("foo.db")
96 | f = dbm.ndbm.open("foo.db")
97 | f = dbm.dumb.open("foo.db")
| ^^^^^^^^^^^^^ SIM115
98 | f = lzma.open("foo.xz")
99 | f = lzma.LZMAFile("foo.xz")
|
SIM115.py:98:5: SIM115 Use a context manager for opening files
|
96 | f = dbm.dumb.open("foo.db")
97 | f = lzma.open("foo.xz")
98 | f = lzma.LZMAFile("foo.xz")
| ^^^^^^^^^^^^^ SIM115
99 | f = shelve.open("foo.db")
100 | f = tokenize.open("foo.py")
96 | f = dbm.ndbm.open("foo.db")
97 | f = dbm.dumb.open("foo.db")
98 | f = lzma.open("foo.xz")
| ^^^^^^^^^ SIM115
99 | f = lzma.LZMAFile("foo.xz")
100 | f = shelve.open("foo.db")
|
SIM115.py:99:5: SIM115 Use a context manager for opening files
|
97 | f = lzma.open("foo.xz")
98 | f = lzma.LZMAFile("foo.xz")
99 | f = shelve.open("foo.db")
| ^^^^^^^^^^^ SIM115
100 | f = tokenize.open("foo.py")
101 | f = wave.open("foo.wav")
97 | f = dbm.dumb.open("foo.db")
98 | f = lzma.open("foo.xz")
99 | f = lzma.LZMAFile("foo.xz")
| ^^^^^^^^^^^^^ SIM115
100 | f = shelve.open("foo.db")
101 | f = tokenize.open("foo.py")
|
SIM115.py:100:5: SIM115 Use a context manager for opening files
|
98 | f = lzma.LZMAFile("foo.xz")
99 | f = shelve.open("foo.db")
100 | f = tokenize.open("foo.py")
| ^^^^^^^^^^^^^ SIM115
101 | f = wave.open("foo.wav")
102 | f = tarfile.TarFile.taropen("foo.tar")
98 | f = lzma.open("foo.xz")
99 | f = lzma.LZMAFile("foo.xz")
100 | f = shelve.open("foo.db")
| ^^^^^^^^^^^ SIM115
101 | f = tokenize.open("foo.py")
102 | f = wave.open("foo.wav")
|
SIM115.py:101:5: SIM115 Use a context manager for opening files
|
99 | f = shelve.open("foo.db")
100 | f = tokenize.open("foo.py")
101 | f = wave.open("foo.wav")
| ^^^^^^^^^ SIM115
102 | f = tarfile.TarFile.taropen("foo.tar")
103 | f = fileinput.input("foo.txt")
99 | f = lzma.LZMAFile("foo.xz")
100 | f = shelve.open("foo.db")
101 | f = tokenize.open("foo.py")
| ^^^^^^^^^^^^^ SIM115
102 | f = wave.open("foo.wav")
103 | f = tarfile.TarFile.taropen("foo.tar")
|
SIM115.py:102:5: SIM115 Use a context manager for opening files
|
100 | f = tokenize.open("foo.py")
101 | f = wave.open("foo.wav")
102 | f = tarfile.TarFile.taropen("foo.tar")
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
103 | f = fileinput.input("foo.txt")
104 | f = fileinput.FileInput("foo.txt")
100 | f = shelve.open("foo.db")
101 | f = tokenize.open("foo.py")
102 | f = wave.open("foo.wav")
| ^^^^^^^^^ SIM115
103 | f = tarfile.TarFile.taropen("foo.tar")
104 | f = fileinput.input("foo.txt")
|
SIM115.py:103:5: SIM115 Use a context manager for opening files
|
101 | f = wave.open("foo.wav")
102 | f = tarfile.TarFile.taropen("foo.tar")
103 | f = fileinput.input("foo.txt")
| ^^^^^^^^^^^^^^^ SIM115
104 | f = fileinput.FileInput("foo.txt")
101 | f = tokenize.open("foo.py")
102 | f = wave.open("foo.wav")
103 | f = tarfile.TarFile.taropen("foo.tar")
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
104 | f = fileinput.input("foo.txt")
105 | f = fileinput.FileInput("foo.txt")
|
SIM115.py:104:5: SIM115 Use a context manager for opening files
|
102 | f = tarfile.TarFile.taropen("foo.tar")
103 | f = fileinput.input("foo.txt")
104 | f = fileinput.FileInput("foo.txt")
102 | f = wave.open("foo.wav")
103 | f = tarfile.TarFile.taropen("foo.tar")
104 | f = fileinput.input("foo.txt")
| ^^^^^^^^^^^^^^^ SIM115
105 | f = fileinput.FileInput("foo.txt")
|
SIM115.py:105:5: SIM115 Use a context manager for opening files
|
103 | f = tarfile.TarFile.taropen("foo.tar")
104 | f = fileinput.input("foo.txt")
105 | f = fileinput.FileInput("foo.txt")
| ^^^^^^^^^^^^^^^^^^^ SIM115
105 |
106 | with contextlib.suppress(Exception):
106 |
107 | with contextlib.suppress(Exception):
|
SIM115.py:240:9: SIM115 Use a context manager for opening files
SIM115.py:241:9: SIM115 Use a context manager for opening files
|
238 | def aliased():
239 | from shelve import open as open_shelf
240 | x = open_shelf("foo.dbm")
239 | def aliased():
240 | from shelve import open as open_shelf
241 | x = open_shelf("foo.dbm")
| ^^^^^^^^^^ SIM115
241 | x.close()
242 | x.close()
|
SIM115.py:244:9: SIM115 Use a context manager for opening files
SIM115.py:245:9: SIM115 Use a context manager for opening files
|
243 | from tarfile import TarFile as TF
244 | f = TF("foo").open()
244 | from tarfile import TarFile as TF
245 | f = TF("foo").open()
| ^^^^^^^^^^^^^^ SIM115
245 | f.close()
246 | f.close()
|
SIM115.py:257:5: SIM115 Use a context manager for opening files
SIM115.py:258:5: SIM115 Use a context manager for opening files
|
256 | # SIM115
257 | f = dbm.sqlite3.open("foo.db")
257 | # SIM115
258 | f = dbm.sqlite3.open("foo.db")
| ^^^^^^^^^^^^^^^^ SIM115
258 | f.close()
259 | f.close()
|

View File

@@ -1,110 +1,110 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM116.py:5:1: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:6:5: SIM116 Use a dictionary instead of consecutive `if` statements
|
4 | # SIM116
5 | / if a == "foo":
6 | | return "bar"
7 | | elif a == "bar":
8 | | return "baz"
9 | | elif a == "boo":
10 | | return "ooh"
11 | | else:
12 | | return 42
| |_____________^ SIM116
13 |
14 | # SIM116
5 | # SIM116
6 | / if a == "foo":
7 | | return "bar"
8 | | elif a == "bar":
9 | | return "baz"
10 | | elif a == "boo":
11 | | return "ooh"
12 | | else:
13 | | return 42
| |_________________^ SIM116
14 |
15 | # SIM116
|
SIM116.py:15:1: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:16:5: SIM116 Use a dictionary instead of consecutive `if` statements
|
14 | # SIM116
15 | / if a == 1:
16 | | return (1, 2, 3)
17 | | elif a == 2:
18 | | return (4, 5, 6)
19 | | elif a == 3:
20 | | return (7, 8, 9)
21 | | else:
22 | | return (10, 11, 12)
| |_______________________^ SIM116
23 |
24 | # SIM116
15 | # SIM116
16 | / if a == 1:
17 | | return (1, 2, 3)
18 | | elif a == 2:
19 | | return (4, 5, 6)
20 | | elif a == 3:
21 | | return (7, 8, 9)
22 | | else:
23 | | return (10, 11, 12)
| |___________________________^ SIM116
24 |
25 | # SIM116
|
SIM116.py:25:1: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:26:5: SIM116 Use a dictionary instead of consecutive `if` statements
|
24 | # SIM116
25 | / if a == 1:
26 | | return (1, 2, 3)
27 | | elif a == 2:
28 | | return (4, 5, 6)
29 | | elif a == 3:
30 | | return (7, 8, 9)
| |____________________^ SIM116
31 |
32 | # SIM116
25 | # SIM116
26 | / if a == 1:
27 | | return (1, 2, 3)
28 | | elif a == 2:
29 | | return (4, 5, 6)
30 | | elif a == 3:
31 | | return (7, 8, 9)
| |________________________^ SIM116
32 |
33 | # SIM116
|
SIM116.py:33:1: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:34:5: SIM116 Use a dictionary instead of consecutive `if` statements
|
32 | # SIM116
33 | / if a == "hello 'sir'":
34 | | return (1, 2, 3)
35 | | elif a == 'goodbye "mam"':
36 | | return (4, 5, 6)
37 | | elif a == """Fairwell 'mister'""":
38 | | return (7, 8, 9)
39 | | else:
40 | | return (10, 11, 12)
| |_______________________^ SIM116
41 |
42 | # SIM116
33 | # SIM116
34 | / if a == "hello 'sir'":
35 | | return (1, 2, 3)
36 | | elif a == 'goodbye "mam"':
37 | | return (4, 5, 6)
38 | | elif a == """Fairwell 'mister'""":
39 | | return (7, 8, 9)
40 | | else:
41 | | return (10, 11, 12)
| |___________________________^ SIM116
42 |
43 | # SIM116
|
SIM116.py:43:1: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:44:5: SIM116 Use a dictionary instead of consecutive `if` statements
|
42 | # SIM116
43 | / if a == b"one":
44 | | return 1
45 | | elif a == b"two":
46 | | return 2
47 | | elif a == b"three":
48 | | return 3
| |____________^ SIM116
49 |
50 | # SIM116
43 | # SIM116
44 | / if a == b"one":
45 | | return 1
46 | | elif a == b"two":
47 | | return 2
48 | | elif a == b"three":
49 | | return 3
| |________________^ SIM116
50 |
51 | # SIM116
|
SIM116.py:51:1: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:52:5: SIM116 Use a dictionary instead of consecutive `if` statements
|
50 | # SIM116
51 | / if a == "hello 'sir'":
52 | | return ("hello'", 'hi"', 3)
53 | | elif a == 'goodbye "mam"':
54 | | return (4, 5, 6)
55 | | elif a == """Fairwell 'mister'""":
56 | | return (7, 8, 9)
57 | | else:
58 | | return (10, 11, 12)
| |_______________________^ SIM116
59 |
60 | # OK
51 | # SIM116
52 | / if a == "hello 'sir'":
53 | | return ("hello'", 'hi"', 3)
54 | | elif a == 'goodbye "mam"':
55 | | return (4, 5, 6)
56 | | elif a == """Fairwell 'mister'""":
57 | | return (7, 8, 9)
58 | | else:
59 | | return (10, 11, 12)
| |___________________________^ SIM116
60 |
61 | # OK
|
SIM116.py:79:1: SIM116 Use a dictionary instead of consecutive `if` statements
SIM116.py:80:5: SIM116 Use a dictionary instead of consecutive `if` statements
|
78 | # SIM116
79 | / if func_name == "create":
80 | | return "A"
81 | | elif func_name == "modify":
82 | | return "M"
83 | | elif func_name == "remove":
84 | | return "D"
85 | | elif func_name == "move":
86 | | return "MV"
| |_______________^ SIM116
87 |
88 | # OK
79 | # SIM116
80 | / if func_name == "create":
81 | | return "A"
82 | | elif func_name == "modify":
83 | | return "M"
84 | | elif func_name == "remove":
85 | | return "D"
86 | | elif func_name == "move":
87 | | return "MV"
| |___________________^ SIM116
88 |
89 | # OK
|

View File

@@ -11,6 +11,7 @@ use crate::registry::Rule;
use crate::rules::flake8_type_checking::helpers::quote_type_expression;
use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion;
use ruff_python_ast::parenthesize::parenthesized_range;
/// ## What it does
/// Checks if [PEP 613] explicit type aliases contain references to
@@ -87,11 +88,15 @@ impl Violation for UnquotedTypeAlias {
/// ## Example
/// Given:
/// ```python
/// from typing import TypeAlias
///
/// OptInt: TypeAlias = "int | None"
/// ```
///
/// Use instead:
/// ```python
/// from typing import TypeAlias
///
/// OptInt: TypeAlias = int | None
/// ```
///
@@ -287,7 +292,30 @@ pub(crate) fn quoted_type_alias(
let range = annotation_expr.range();
let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range);
let edit = Edit::range_replacement(annotation_expr.value.to_string(), range);
let fix_string = annotation_expr.value.to_string();
let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r'))
&& parenthesized_range(
// Check for parenthesis outside string ("""...""")
annotation_expr.into(),
checker.semantic().current_statement().into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.is_none()
&& parenthesized_range(
// Check for parenthesis inside string """(...)"""
expr.into(),
annotation_expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.is_none()
{
format!("({fix_string})")
} else {
fix_string
};
let edit = Edit::range_replacement(fix_string, range);
if checker.comment_ranges().intersects(range) {
diagnostic.set_fix(Fix::unsafe_edit(edit));
} else {

View File

@@ -44,7 +44,7 @@ use crate::{Fix, FixAvailability, Violation};
/// ```python
/// from __future__ import annotations
///
/// import local_module
/// from . import local_module
///
///
/// def func(sized: local_module.Container) -> int:
@@ -58,7 +58,7 @@ use crate::{Fix, FixAvailability, Violation};
/// from typing import TYPE_CHECKING
///
/// if TYPE_CHECKING:
/// import local_module
/// from . import local_module
///
///
/// def func(sized: local_module.Container) -> int:

View File

@@ -409,6 +409,8 @@ TC008.py:52:18: TC008 [*] Remove quotes from type alias
51 | a: TypeAlias = 'Baz' # OK
52 | type A = 'Baz' # TC008
| ^^^^^ TC008
53 |
54 | # O should have parenthesis added
|
= help: Remove quotes
@@ -418,3 +420,187 @@ TC008.py:52:18: TC008 [*] Remove quotes from type alias
51 51 | a: TypeAlias = 'Baz' # OK
52 |- type A = 'Baz' # TC008
52 |+ type A = Baz # TC008
53 53 |
54 54 | # O should have parenthesis added
55 55 | o: TypeAlias = """int
TC008.py:55:16: TC008 [*] Remove quotes from type alias
|
54 | # O should have parenthesis added
55 | o: TypeAlias = """int
| ________________^
56 | | | None"""
| |_________^ TC008
57 | type O = """int
58 | | None"""
|
= help: Remove quotes
Safe fix
52 52 | type A = 'Baz' # TC008
53 53 |
54 54 | # O should have parenthesis added
55 |-o: TypeAlias = """int
56 |-| None"""
55 |+o: TypeAlias = (int
56 |+| None)
57 57 | type O = """int
58 58 | | None"""
59 59 |
TC008.py:57:10: TC008 [*] Remove quotes from type alias
|
55 | o: TypeAlias = """int
56 | | None"""
57 | type O = """int
| __________^
58 | | | None"""
| |_________^ TC008
59 |
60 | # P, Q, and R should not have parenthesis added
|
= help: Remove quotes
Safe fix
54 54 | # O should have parenthesis added
55 55 | o: TypeAlias = """int
56 56 | | None"""
57 |-type O = """int
58 |-| None"""
57 |+type O = (int
58 |+| None)
59 59 |
60 60 | # P, Q, and R should not have parenthesis added
61 61 | p: TypeAlias = ("""int
TC008.py:61:17: TC008 [*] Remove quotes from type alias
|
60 | # P, Q, and R should not have parenthesis added
61 | p: TypeAlias = ("""int
| _________________^
62 | | | None""")
| |_________^ TC008
63 | type P = ("""int
64 | | None""")
|
= help: Remove quotes
Safe fix
58 58 | | None"""
59 59 |
60 60 | # P, Q, and R should not have parenthesis added
61 |-p: TypeAlias = ("""int
62 |-| None""")
61 |+p: TypeAlias = (int
62 |+| None)
63 63 | type P = ("""int
64 64 | | None""")
65 65 |
TC008.py:63:11: TC008 [*] Remove quotes from type alias
|
61 | p: TypeAlias = ("""int
62 | | None""")
63 | type P = ("""int
| ___________^
64 | | | None""")
| |_________^ TC008
65 |
66 | q: TypeAlias = """(int
|
= help: Remove quotes
Safe fix
60 60 | # P, Q, and R should not have parenthesis added
61 61 | p: TypeAlias = ("""int
62 62 | | None""")
63 |-type P = ("""int
64 |-| None""")
63 |+type P = (int
64 |+| None)
65 65 |
66 66 | q: TypeAlias = """(int
67 67 | | None)"""
TC008.py:66:16: TC008 [*] Remove quotes from type alias
|
64 | | None""")
65 |
66 | q: TypeAlias = """(int
| ________________^
67 | | | None)"""
| |__________^ TC008
68 | type Q = """(int
69 | | None)"""
|
= help: Remove quotes
Safe fix
63 63 | type P = ("""int
64 64 | | None""")
65 65 |
66 |-q: TypeAlias = """(int
67 |-| None)"""
66 |+q: TypeAlias = (int
67 |+| None)
68 68 | type Q = """(int
69 69 | | None)"""
70 70 |
TC008.py:68:10: TC008 [*] Remove quotes from type alias
|
66 | q: TypeAlias = """(int
67 | | None)"""
68 | type Q = """(int
| __________^
69 | | | None)"""
| |__________^ TC008
70 |
71 | r: TypeAlias = """int | None"""
|
= help: Remove quotes
Safe fix
65 65 |
66 66 | q: TypeAlias = """(int
67 67 | | None)"""
68 |-type Q = """(int
69 |-| None)"""
68 |+type Q = (int
69 |+| None)
70 70 |
71 71 | r: TypeAlias = """int | None"""
72 72 | type R = """int | None"""
TC008.py:71:16: TC008 [*] Remove quotes from type alias
|
69 | | None)"""
70 |
71 | r: TypeAlias = """int | None"""
| ^^^^^^^^^^^^^^^^ TC008
72 | type R = """int | None"""
|
= help: Remove quotes
Safe fix
68 68 | type Q = """(int
69 69 | | None)"""
70 70 |
71 |-r: TypeAlias = """int | None"""
71 |+r: TypeAlias = int | None
72 72 | type R = """int | None"""
TC008.py:72:10: TC008 [*] Remove quotes from type alias
|
71 | r: TypeAlias = """int | None"""
72 | type R = """int | None"""
| ^^^^^^^^^^^^^^^^ TC008
|
= help: Remove quotes
Safe fix
69 69 | | None)"""
70 70 |
71 71 | r: TypeAlias = """int | None"""
72 |-type R = """int | None"""
72 |+type R = int | None

View File

@@ -0,0 +1,72 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::Ranged;
pub(crate) fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
expr.as_call_expr().is_some_and(|expr_call| {
checker
.semantic()
.resolve_qualified_name(&expr_call.func)
.is_some_and(|name| matches!(name.segments(), ["pathlib", "Path"]))
})
}
pub(crate) fn check_os_path_get_calls(
checker: &Checker,
call: &ExprCall,
fn_name: &str,
attr: &str,
fix_enabled: bool,
violation: impl Violation,
) {
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_none_or(|qualified_name| qualified_name.segments() != ["os", "path", fn_name])
{
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("filename", 0) else {
return;
};
let arg_code = checker.locator().slice(arg.range());
let range = call.range();
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
if fix_enabled {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let replacement = if is_path_call(checker, arg) {
format!("{arg_code}.stat().{attr}")
} else {
format!("{binding}({arg_code}).stat().{attr}")
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}
}

View File

@@ -1,4 +1,5 @@
//! Rules from [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/).
mod helpers;
pub(crate) mod rules;
pub(crate) mod violations;
@@ -81,6 +82,9 @@ mod tests {
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
#[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))]
#[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))]
fn preview_flake8_use_pathlib(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -19,12 +19,20 @@ use ruff_text_size::Ranged;
/// ## Example
///
/// ```python
/// from pathlib import Path
///
/// path = Path()
///
/// path.with_suffix("py")
/// ```
///
/// Use instead:
///
/// ```python
/// from pathlib import Path
///
/// path = Path()
///
/// path.with_suffix(".py")
/// ```
///

View File

@@ -1,6 +1,9 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getatime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getatime`.
@@ -32,6 +35,9 @@ use crate::Violation;
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getatime`](https://docs.python.org/3/library/os.path.html#os.path.getatime)
@@ -43,8 +49,25 @@ use crate::Violation;
pub(crate) struct OsPathGetatime;
impl Violation for OsPathGetatime {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.getatime` should be replaced by `Path.stat().st_atime`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path.stat(...).st_atime`".to_string())
}
}
/// PTH203
pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
checker,
call,
"getatime",
"st_atime",
is_fix_os_path_getatime_enabled(checker.settings()),
OsPathGetatime,
);
}

View File

@@ -1,6 +1,9 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getctime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getctime`.
@@ -32,6 +35,9 @@ use crate::Violation;
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getctime`](https://docs.python.org/3/library/os.path.html#os.path.getctime)
@@ -43,8 +49,26 @@ use crate::Violation;
pub(crate) struct OsPathGetctime;
impl Violation for OsPathGetctime {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.getctime` should be replaced by `Path.stat().st_ctime`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path.stat(...).st_ctime`".to_string())
}
}
/// PTH205
pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
checker,
call,
"getctime",
"st_ctime",
is_fix_os_path_getctime_enabled(checker.settings()),
OsPathGetctime,
);
}

View File

@@ -1,6 +1,9 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_getmtime_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getmtime`.
@@ -32,6 +35,9 @@ use crate::Violation;
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getmtime`](https://docs.python.org/3/library/os.path.html#os.path.getmtime)
@@ -43,8 +49,26 @@ use crate::Violation;
pub(crate) struct OsPathGetmtime;
impl Violation for OsPathGetmtime {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.getmtime` should be replaced by `Path.stat().st_mtime`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path.stat(...).st_mtime`".to_string())
}
}
/// PTH204
pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall) {
check_os_path_get_calls(
checker,
call,
"getmtime",
"st_mtime",
is_fix_os_path_getmtime_enabled(checker.settings()),
OsPathGetmtime,
);
}

View File

@@ -1,11 +1,9 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_path_getsize_enabled;
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
use crate::rules::flake8_use_pathlib::helpers::check_os_path_get_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::Ranged;
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.getsize`.
@@ -65,63 +63,12 @@ impl Violation for OsPathGetsize {
/// PTH202
pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall) {
if !matches!(
checker
.semantic()
.resolve_qualified_name(&call.func)
.as_ref()
.map(QualifiedName::segments),
Some(["os", "path", "getsize"])
) {
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("filename", 0) else {
return;
};
let arg_code = checker.locator().slice(arg.range());
let range = call.range();
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let mut diagnostic = checker.report_diagnostic(OsPathGetsize, range);
if is_fix_os_path_getsize_enabled(checker.settings()) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let replacement = if is_path_call(checker, arg) {
format!("{arg_code}.stat().st_size")
} else {
format!("{binding}({arg_code}).stat().st_size")
};
Ok(
Fix::safe_edits(Edit::range_replacement(replacement, range), [import_edit])
.with_applicability(applicability),
)
});
}
}
fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
expr.as_call_expr().is_some_and(|expr_call| {
checker
.semantic()
.resolve_qualified_name(&expr_call.func)
.is_some_and(|name| matches!(name.segments(), ["pathlib", "Path"]))
})
check_os_path_get_calls(
checker,
call,
"getsize",
"st_size",
is_fix_os_path_getsize_enabled(checker.settings()),
OsPathGetsize,
);
}

View File

@@ -4,9 +4,7 @@ use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_use_pathlib::rules::{
Glob, OsPathGetatime, OsPathGetctime, OsPathGetmtime,
};
use crate::rules::flake8_use_pathlib::rules::Glob;
use crate::rules::flake8_use_pathlib::violations::{
BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathAbspath,
OsPathBasename, OsPathDirname, OsPathExists, OsPathExpanduser, OsPathIsabs, OsPathIsdir,
@@ -194,12 +192,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
["os", "path", "samefile"] => checker.report_diagnostic_if_enabled(OsPathSamefile, range),
// PTH122
["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range),
// PTH203
["os", "path", "getatime"] => checker.report_diagnostic_if_enabled(OsPathGetatime, range),
// PTH204
["os", "path", "getmtime"] => checker.report_diagnostic_if_enabled(OsPathGetmtime, range),
// PTH205
["os", "path", "getctime"] => checker.report_diagnostic_if_enabled(OsPathGetctime, range),
// PTH211
["os", "symlink"] => {
// `dir_fd` is not supported by pathlib, so check if there are non-default values.

View File

@@ -4,7 +4,7 @@ source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
PTH202.py:10:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
10 | os.path.getsize("filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
|
@@ -14,7 +14,7 @@ PTH202.py:11:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
|
@@ -25,7 +25,7 @@ PTH202.py:12:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
13 | os.path.getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -35,7 +35,7 @@ PTH202.py:13:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
14 |
15 | os.path.getsize(filename)
|
@@ -46,7 +46,7 @@ PTH202.py:15:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
13 | os.path.getsize(__file__)
14 |
15 | os.path.getsize(filename)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
|
@@ -56,7 +56,7 @@ PTH202.py:16:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
17 | os.path.getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
@@ -66,7 +66,7 @@ PTH202.py:17:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
18 |
19 | os.path.getsize(filename="filename")
|
@@ -77,7 +77,7 @@ PTH202.py:19:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
17 | os.path.getsize(filename2)
18 |
19 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
|
@@ -87,7 +87,7 @@ PTH202.py:20:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
|
@@ -98,7 +98,7 @@ PTH202.py:21:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
22 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -108,7 +108,7 @@ PTH202.py:22:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
23 |
24 | getsize("filename")
|
@@ -119,7 +119,7 @@ PTH202.py:24:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
22 | os.path.getsize(filename=__file__)
23 |
24 | getsize("filename")
| ^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
25 | getsize(b"filename")
26 | getsize(Path("filename"))
|
@@ -129,7 +129,7 @@ PTH202.py:25:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
24 | getsize("filename")
25 | getsize(b"filename")
| ^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
26 | getsize(Path("filename"))
27 | getsize(__file__)
|
@@ -140,7 +140,7 @@ PTH202.py:26:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
24 | getsize("filename")
25 | getsize(b"filename")
26 | getsize(Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
27 | getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -150,7 +150,7 @@ PTH202.py:27:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
25 | getsize(b"filename")
26 | getsize(Path("filename"))
27 | getsize(__file__)
| ^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
28 |
29 | getsize(filename="filename")
|
@@ -161,7 +161,7 @@ PTH202.py:29:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
27 | getsize(__file__)
28 |
29 | getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
|
@@ -171,7 +171,7 @@ PTH202.py:30:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
|
@@ -182,7 +182,7 @@ PTH202.py:31:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
32 | getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -192,7 +192,7 @@ PTH202.py:32:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
33 |
34 | getsize(filename)
|
@@ -203,7 +203,7 @@ PTH202.py:34:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
32 | getsize(filename=__file__)
33 |
34 | getsize(filename)
| ^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
35 | getsize(filename1)
36 | getsize(filename2)
|
@@ -213,7 +213,7 @@ PTH202.py:35:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
|
34 | getsize(filename)
35 | getsize(filename1)
| ^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
36 | getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
@@ -223,89 +223,70 @@ PTH202.py:36:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
34 | getsize(filename)
35 | getsize(filename1)
36 | getsize(filename2)
| ^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:39:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
39 | / os.path.getsize(
40 | | "filename", # comment
41 | | )
| |_^ PTH202
42 |
43 | os.path.getsize(
39 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
40 | "filename", # comment
41 | )
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:43:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
41 | )
41 | )
42 |
43 | / os.path.getsize(
44 | | # comment
45 | | "filename"
46 | | ,
47 | | # comment
48 | | )
| |_^ PTH202
49 |
50 | os.path.getsize(
43 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
44 | # comment
45 | "filename"
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:50:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
48 | )
48 | )
49 |
50 | / os.path.getsize(
51 | | # comment
52 | | b"filename"
53 | | # comment
54 | | )
| |_^ PTH202
55 |
56 | os.path.getsize( # comment
50 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
51 | # comment
52 | b"filename"
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:56:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
54 | )
54 | )
55 |
56 | / os.path.getsize( # comment
57 | | Path(__file__)
58 | | # comment
59 | | ) # comment
| |_^ PTH202
60 |
61 | getsize( # comment
56 | os.path.getsize( # comment
| ^^^^^^^^^^^^^^^ PTH202
57 | Path(__file__)
58 | # comment
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:61:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
59 | ) # comment
59 | ) # comment
60 |
61 | / getsize( # comment
62 | | "filename")
| |_______________^ PTH202
63 |
64 | getsize( # comment
61 | getsize( # comment
| ^^^^^^^ PTH202
62 | "filename")
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:64:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
62 | "filename")
62 | "filename")
63 |
64 | / getsize( # comment
65 | | b"filename",
66 | | #comment
67 | | )
| |_^ PTH202
68 |
69 | os.path.getsize("file" + "name")
64 | getsize( # comment
| ^^^^^^^ PTH202
65 | b"filename",
66 | #comment
|
= help: Replace with `Path(...).stat().st_size`
@@ -314,7 +295,7 @@ PTH202.py:69:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
67 | )
68 |
69 | os.path.getsize("file" + "name")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
70 |
71 | getsize \
|
@@ -322,17 +303,12 @@ PTH202.py:69:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
PTH202.py:71:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
69 | os.path.getsize("file" + "name")
69 | os.path.getsize("file" + "name")
70 |
71 | / getsize \
72 | | \
73 | | \
74 | | ( # comment
75 | | "filename",
76 | | )
| |_____^ PTH202
77 |
78 | getsize(Path("filename").resolve())
71 | getsize \
| ^^^^^^^ PTH202
72 | \
73 | \
|
= help: Replace with `Path(...).stat().st_size`
@@ -341,7 +317,7 @@ PTH202.py:78:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
76 | )
77 |
78 | getsize(Path("filename").resolve())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
79 |
80 | import pathlib
|
@@ -352,6 +328,6 @@ PTH202.py:82:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_s
80 | import pathlib
81 |
82 | os.path.getsize(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -6,7 +6,7 @@ PTH202_2.py:3:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_
1 | import os
2 |
3 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
|
@@ -16,7 +16,7 @@ PTH202_2.py:4:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_
|
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
5 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -26,6 +26,6 @@ PTH202_2.py:5:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -10,6 +10,7 @@ PTH203.py:5:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_a
6 | os.path.getatime(b"filename")
7 | os.path.getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:6:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -18,6 +19,7 @@ PTH203.py:6:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_a
| ^^^^^^^^^^^^^^^^ PTH203
7 | os.path.getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:7:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -26,6 +28,7 @@ PTH203.py:7:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_a
7 | os.path.getatime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:10:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -34,6 +37,7 @@ PTH203.py:10:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_
11 | getatime(b"filename")
12 | getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:11:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -42,6 +46,7 @@ PTH203.py:11:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_
| ^^^^^^^^ PTH203
12 | getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:12:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
@@ -50,3 +55,88 @@ PTH203.py:12:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_
12 | getatime(Path("filename"))
| ^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:17:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
15 | file = __file__
16 |
17 | os.path.getatime(file)
| ^^^^^^^^^^^^^^^^ PTH203
18 | os.path.getatime(filename="filename")
19 | os.path.getatime(filename=Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:18:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
17 | os.path.getatime(file)
18 | os.path.getatime(filename="filename")
| ^^^^^^^^^^^^^^^^ PTH203
19 | os.path.getatime(filename=Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:19:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
17 | os.path.getatime(file)
18 | os.path.getatime(filename="filename")
19 | os.path.getatime(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
20 |
21 | os.path.getatime( # comment 1
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:21:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
19 | os.path.getatime(filename=Path("filename"))
20 |
21 | os.path.getatime( # comment 1
| ^^^^^^^^^^^^^^^^ PTH203
22 | # comment 2
23 | "filename" # comment 3
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:29:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
27 | ) # comment 7
28 |
29 | os.path.getatime("file" + "name")
| ^^^^^^^^^^^^^^^^ PTH203
30 |
31 | getatime(Path("filename").resolve())
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:31:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
29 | os.path.getatime("file" + "name")
30 |
31 | getatime(Path("filename").resolve())
| ^^^^^^^^ PTH203
32 |
33 | os.path.getatime(pathlib.Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:33:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
31 | getatime(Path("filename").resolve())
32 |
33 | os.path.getatime(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
34 |
35 | getatime(Path("dir") / "file.txt")
|
= help: Replace with `Path.stat(...).st_atime`
PTH203.py:35:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
33 | os.path.getatime(pathlib.Path("filename"))
34 |
35 | getatime(Path("dir") / "file.txt")
| ^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
snapshot_kind: text
---
PTH204.py:6:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -9,6 +8,7 @@ PTH204.py:6:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_m
7 | os.path.getmtime(b"filename")
8 | os.path.getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:7:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -17,6 +17,7 @@ PTH204.py:7:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_m
| ^^^^^^^^^^^^^^^^ PTH204
8 | os.path.getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:8:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -25,6 +26,7 @@ PTH204.py:8:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_m
8 | os.path.getmtime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH204
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:11:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -33,6 +35,7 @@ PTH204.py:11:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_
12 | getmtime(b"filename")
13 | getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:12:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -41,6 +44,7 @@ PTH204.py:12:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_
| ^^^^^^^^ PTH204
13 | getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
PTH204.py:13:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
@@ -49,3 +53,4 @@ PTH204.py:13:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_
13 | getmtime(Path("filename"))
| ^^^^^^^^ PTH204
|
= help: Replace with `Path.stat(...).st_mtime`

View File

@@ -8,6 +8,7 @@ PTH205.py:6:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_c
7 | os.path.getctime(b"filename")
8 | os.path.getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:7:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -16,6 +17,7 @@ PTH205.py:7:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_c
| ^^^^^^^^^^^^^^^^ PTH205
8 | os.path.getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:8:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -26,6 +28,7 @@ PTH205.py:8:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_c
9 |
10 | getctime("filename")
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:10:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -36,6 +39,7 @@ PTH205.py:10:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_
11 | getctime(b"filename")
12 | getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:11:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -44,6 +48,7 @@ PTH205.py:11:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_
| ^^^^^^^^ PTH205
12 | getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
PTH205.py:12:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
@@ -52,3 +57,4 @@ PTH205.py:12:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_
12 | getctime(Path("filename"))
| ^^^^^^^^ PTH205
|
= help: Replace with `Path.stat(...).st_ctime`

View File

@@ -4,7 +4,7 @@ source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
PTH202.py:10:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
10 | os.path.getsize("filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
|
@@ -24,7 +24,7 @@ PTH202.py:11:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
|
@@ -45,7 +45,7 @@ PTH202.py:12:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
13 | os.path.getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -65,7 +65,7 @@ PTH202.py:13:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
14 |
15 | os.path.getsize(filename)
|
@@ -86,7 +86,7 @@ PTH202.py:15:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
13 | os.path.getsize(__file__)
14 |
15 | os.path.getsize(filename)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
|
@@ -106,7 +106,7 @@ PTH202.py:16:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
17 | os.path.getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
@@ -126,7 +126,7 @@ PTH202.py:17:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
18 |
19 | os.path.getsize(filename="filename")
|
@@ -147,7 +147,7 @@ PTH202.py:19:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
17 | os.path.getsize(filename2)
18 |
19 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
|
@@ -167,7 +167,7 @@ PTH202.py:20:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
|
@@ -188,7 +188,7 @@ PTH202.py:21:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
22 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -208,7 +208,7 @@ PTH202.py:22:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
23 |
24 | getsize("filename")
|
@@ -229,7 +229,7 @@ PTH202.py:24:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
22 | os.path.getsize(filename=__file__)
23 |
24 | getsize("filename")
| ^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
25 | getsize(b"filename")
26 | getsize(Path("filename"))
|
@@ -249,7 +249,7 @@ PTH202.py:25:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
24 | getsize("filename")
25 | getsize(b"filename")
| ^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
26 | getsize(Path("filename"))
27 | getsize(__file__)
|
@@ -270,7 +270,7 @@ PTH202.py:26:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
24 | getsize("filename")
25 | getsize(b"filename")
26 | getsize(Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
27 | getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -290,7 +290,7 @@ PTH202.py:27:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
25 | getsize(b"filename")
26 | getsize(Path("filename"))
27 | getsize(__file__)
| ^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
28 |
29 | getsize(filename="filename")
|
@@ -311,7 +311,7 @@ PTH202.py:29:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
27 | getsize(__file__)
28 |
29 | getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
|
@@ -331,7 +331,7 @@ PTH202.py:30:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
|
@@ -352,7 +352,7 @@ PTH202.py:31:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
32 | getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -372,7 +372,7 @@ PTH202.py:32:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
33 |
34 | getsize(filename)
|
@@ -393,7 +393,7 @@ PTH202.py:34:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
32 | getsize(filename=__file__)
33 |
34 | getsize(filename)
| ^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
35 | getsize(filename1)
36 | getsize(filename2)
|
@@ -413,7 +413,7 @@ PTH202.py:35:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
|
34 | getsize(filename)
35 | getsize(filename1)
| ^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
36 | getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
@@ -433,7 +433,7 @@ PTH202.py:36:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
34 | getsize(filename)
35 | getsize(filename1)
36 | getsize(filename2)
| ^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`
@@ -449,12 +449,10 @@ PTH202.py:36:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:39:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
39 | / os.path.getsize(
40 | | "filename", # comment
41 | | )
| |_^ PTH202
42 |
43 | os.path.getsize(
39 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
40 | "filename", # comment
41 | )
|
= help: Replace with `Path(...).stat().st_size`
@@ -472,17 +470,12 @@ PTH202.py:39:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:43:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
41 | )
41 | )
42 |
43 | / os.path.getsize(
44 | | # comment
45 | | "filename"
46 | | ,
47 | | # comment
48 | | )
| |_^ PTH202
49 |
50 | os.path.getsize(
43 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
44 | # comment
45 | "filename"
|
= help: Replace with `Path(...).stat().st_size`
@@ -503,16 +496,12 @@ PTH202.py:43:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:50:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
48 | )
48 | )
49 |
50 | / os.path.getsize(
51 | | # comment
52 | | b"filename"
53 | | # comment
54 | | )
| |_^ PTH202
55 |
56 | os.path.getsize( # comment
50 | os.path.getsize(
| ^^^^^^^^^^^^^^^ PTH202
51 | # comment
52 | b"filename"
|
= help: Replace with `Path(...).stat().st_size`
@@ -532,15 +521,12 @@ PTH202.py:50:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:56:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
54 | )
54 | )
55 |
56 | / os.path.getsize( # comment
57 | | Path(__file__)
58 | | # comment
59 | | ) # comment
| |_^ PTH202
60 |
61 | getsize( # comment
56 | os.path.getsize( # comment
| ^^^^^^^^^^^^^^^ PTH202
57 | Path(__file__)
58 | # comment
|
= help: Replace with `Path(...).stat().st_size`
@@ -559,13 +545,11 @@ PTH202.py:56:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:61:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
59 | ) # comment
59 | ) # comment
60 |
61 | / getsize( # comment
62 | | "filename")
| |_______________^ PTH202
63 |
64 | getsize( # comment
61 | getsize( # comment
| ^^^^^^^ PTH202
62 | "filename")
|
= help: Replace with `Path(...).stat().st_size`
@@ -582,15 +566,12 @@ PTH202.py:61:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:64:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
62 | "filename")
62 | "filename")
63 |
64 | / getsize( # comment
65 | | b"filename",
66 | | #comment
67 | | )
| |_^ PTH202
68 |
69 | os.path.getsize("file" + "name")
64 | getsize( # comment
| ^^^^^^^ PTH202
65 | b"filename",
66 | #comment
|
= help: Replace with `Path(...).stat().st_size`
@@ -612,7 +593,7 @@ PTH202.py:69:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
67 | )
68 |
69 | os.path.getsize("file" + "name")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
70 |
71 | getsize \
|
@@ -630,17 +611,12 @@ PTH202.py:69:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
PTH202.py:71:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
69 | os.path.getsize("file" + "name")
69 | os.path.getsize("file" + "name")
70 |
71 | / getsize \
72 | | \
73 | | \
74 | | ( # comment
75 | | "filename",
76 | | )
| |_____^ PTH202
77 |
78 | getsize(Path("filename").resolve())
71 | getsize \
| ^^^^^^^ PTH202
72 | \
73 | \
|
= help: Replace with `Path(...).stat().st_size`
@@ -664,7 +640,7 @@ PTH202.py:78:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
76 | )
77 |
78 | getsize(Path("filename").resolve())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^ PTH202
79 |
80 | import pathlib
|
@@ -685,7 +661,7 @@ PTH202.py:82:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().
80 | import pathlib
81 |
82 | os.path.getsize(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -6,7 +6,7 @@ PTH202_2.py:3:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat()
1 | import os
2 |
3 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
|
@@ -25,7 +25,7 @@ PTH202_2.py:4:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat()
|
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
5 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
@@ -44,7 +44,7 @@ PTH202_2.py:5:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat()
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
| ^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -0,0 +1,284 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH203.py:5:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
3 | from os.path import getatime
4 |
5 | os.path.getatime("filename")
| ^^^^^^^^^^^^^^^^ PTH203
6 | os.path.getatime(b"filename")
7 | os.path.getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
2 2 | from pathlib import Path
3 3 | from os.path import getatime
4 4 |
5 |-os.path.getatime("filename")
5 |+Path("filename").stat().st_atime
6 6 | os.path.getatime(b"filename")
7 7 | os.path.getatime(Path("filename"))
8 8 |
PTH203.py:6:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
5 | os.path.getatime("filename")
6 | os.path.getatime(b"filename")
| ^^^^^^^^^^^^^^^^ PTH203
7 | os.path.getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
3 3 | from os.path import getatime
4 4 |
5 5 | os.path.getatime("filename")
6 |-os.path.getatime(b"filename")
6 |+Path(b"filename").stat().st_atime
7 7 | os.path.getatime(Path("filename"))
8 8 |
9 9 |
PTH203.py:7:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
5 | os.path.getatime("filename")
6 | os.path.getatime(b"filename")
7 | os.path.getatime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
4 4 |
5 5 | os.path.getatime("filename")
6 6 | os.path.getatime(b"filename")
7 |-os.path.getatime(Path("filename"))
7 |+Path("filename").stat().st_atime
8 8 |
9 9 |
10 10 | getatime("filename")
PTH203.py:10:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
10 | getatime("filename")
| ^^^^^^^^ PTH203
11 | getatime(b"filename")
12 | getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
7 7 | os.path.getatime(Path("filename"))
8 8 |
9 9 |
10 |-getatime("filename")
10 |+Path("filename").stat().st_atime
11 11 | getatime(b"filename")
12 12 | getatime(Path("filename"))
13 13 |
PTH203.py:11:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
10 | getatime("filename")
11 | getatime(b"filename")
| ^^^^^^^^ PTH203
12 | getatime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
8 8 |
9 9 |
10 10 | getatime("filename")
11 |-getatime(b"filename")
11 |+Path(b"filename").stat().st_atime
12 12 | getatime(Path("filename"))
13 13 |
14 14 |
PTH203.py:12:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
10 | getatime("filename")
11 | getatime(b"filename")
12 | getatime(Path("filename"))
| ^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
9 9 |
10 10 | getatime("filename")
11 11 | getatime(b"filename")
12 |-getatime(Path("filename"))
12 |+Path("filename").stat().st_atime
13 13 |
14 14 |
15 15 | file = __file__
PTH203.py:17:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
15 | file = __file__
16 |
17 | os.path.getatime(file)
| ^^^^^^^^^^^^^^^^ PTH203
18 | os.path.getatime(filename="filename")
19 | os.path.getatime(filename=Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
14 14 |
15 15 | file = __file__
16 16 |
17 |-os.path.getatime(file)
17 |+Path(file).stat().st_atime
18 18 | os.path.getatime(filename="filename")
19 19 | os.path.getatime(filename=Path("filename"))
20 20 |
PTH203.py:18:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
17 | os.path.getatime(file)
18 | os.path.getatime(filename="filename")
| ^^^^^^^^^^^^^^^^ PTH203
19 | os.path.getatime(filename=Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
15 15 | file = __file__
16 16 |
17 17 | os.path.getatime(file)
18 |-os.path.getatime(filename="filename")
18 |+Path("filename").stat().st_atime
19 19 | os.path.getatime(filename=Path("filename"))
20 20 |
21 21 | os.path.getatime( # comment 1
PTH203.py:19:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
17 | os.path.getatime(file)
18 | os.path.getatime(filename="filename")
19 | os.path.getatime(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
20 |
21 | os.path.getatime( # comment 1
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
16 16 |
17 17 | os.path.getatime(file)
18 18 | os.path.getatime(filename="filename")
19 |-os.path.getatime(filename=Path("filename"))
19 |+Path("filename").stat().st_atime
20 20 |
21 21 | os.path.getatime( # comment 1
22 22 | # comment 2
PTH203.py:21:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
19 | os.path.getatime(filename=Path("filename"))
20 |
21 | os.path.getatime( # comment 1
| ^^^^^^^^^^^^^^^^ PTH203
22 | # comment 2
23 | "filename" # comment 3
|
= help: Replace with `Path.stat(...).st_atime`
Unsafe fix
18 18 | os.path.getatime(filename="filename")
19 19 | os.path.getatime(filename=Path("filename"))
20 20 |
21 |-os.path.getatime( # comment 1
22 |- # comment 2
23 |- "filename" # comment 3
24 |- # comment 4
25 |- , # comment 5
26 |- # comment 6
27 |-) # comment 7
21 |+Path("filename").stat().st_atime # comment 7
28 22 |
29 23 | os.path.getatime("file" + "name")
30 24 |
PTH203.py:29:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
27 | ) # comment 7
28 |
29 | os.path.getatime("file" + "name")
| ^^^^^^^^^^^^^^^^ PTH203
30 |
31 | getatime(Path("filename").resolve())
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
26 26 | # comment 6
27 27 | ) # comment 7
28 28 |
29 |-os.path.getatime("file" + "name")
29 |+Path("file" + "name").stat().st_atime
30 30 |
31 31 | getatime(Path("filename").resolve())
32 32 |
PTH203.py:31:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
29 | os.path.getatime("file" + "name")
30 |
31 | getatime(Path("filename").resolve())
| ^^^^^^^^ PTH203
32 |
33 | os.path.getatime(pathlib.Path("filename"))
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
28 28 |
29 29 | os.path.getatime("file" + "name")
30 30 |
31 |-getatime(Path("filename").resolve())
31 |+Path(Path("filename").resolve()).stat().st_atime
32 32 |
33 33 | os.path.getatime(pathlib.Path("filename"))
34 34 |
PTH203.py:33:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
31 | getatime(Path("filename").resolve())
32 |
33 | os.path.getatime(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
34 |
35 | getatime(Path("dir") / "file.txt")
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
30 30 |
31 31 | getatime(Path("filename").resolve())
32 32 |
33 |-os.path.getatime(pathlib.Path("filename"))
33 |+pathlib.Path("filename").stat().st_atime
34 34 |
35 35 | getatime(Path("dir") / "file.txt")
PTH203.py:35:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
33 | os.path.getatime(pathlib.Path("filename"))
34 |
35 | getatime(Path("dir") / "file.txt")
| ^^^^^^^^ PTH203
|
= help: Replace with `Path.stat(...).st_atime`
Safe fix
32 32 |
33 33 | os.path.getatime(pathlib.Path("filename"))
34 34 |
35 |-getatime(Path("dir") / "file.txt")
35 |+Path(Path("dir") / "file.txt").stat().st_atime

View File

@@ -0,0 +1,110 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH204.py:6:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
6 | os.path.getmtime("filename")
| ^^^^^^^^^^^^^^^^ PTH204
7 | os.path.getmtime(b"filename")
8 | os.path.getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
3 3 | from os.path import getmtime
4 4 |
5 5 |
6 |-os.path.getmtime("filename")
6 |+Path("filename").stat().st_mtime
7 7 | os.path.getmtime(b"filename")
8 8 | os.path.getmtime(Path("filename"))
9 9 |
PTH204.py:7:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
6 | os.path.getmtime("filename")
7 | os.path.getmtime(b"filename")
| ^^^^^^^^^^^^^^^^ PTH204
8 | os.path.getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
4 4 |
5 5 |
6 6 | os.path.getmtime("filename")
7 |-os.path.getmtime(b"filename")
7 |+Path(b"filename").stat().st_mtime
8 8 | os.path.getmtime(Path("filename"))
9 9 |
10 10 |
PTH204.py:8:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
6 | os.path.getmtime("filename")
7 | os.path.getmtime(b"filename")
8 | os.path.getmtime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH204
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
5 5 |
6 6 | os.path.getmtime("filename")
7 7 | os.path.getmtime(b"filename")
8 |-os.path.getmtime(Path("filename"))
8 |+Path("filename").stat().st_mtime
9 9 |
10 10 |
11 11 | getmtime("filename")
PTH204.py:11:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
11 | getmtime("filename")
| ^^^^^^^^ PTH204
12 | getmtime(b"filename")
13 | getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
8 8 | os.path.getmtime(Path("filename"))
9 9 |
10 10 |
11 |-getmtime("filename")
11 |+Path("filename").stat().st_mtime
12 12 | getmtime(b"filename")
13 13 | getmtime(Path("filename"))
PTH204.py:12:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
11 | getmtime("filename")
12 | getmtime(b"filename")
| ^^^^^^^^ PTH204
13 | getmtime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
9 9 |
10 10 |
11 11 | getmtime("filename")
12 |-getmtime(b"filename")
12 |+Path(b"filename").stat().st_mtime
13 13 | getmtime(Path("filename"))
PTH204.py:13:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime`
|
11 | getmtime("filename")
12 | getmtime(b"filename")
13 | getmtime(Path("filename"))
| ^^^^^^^^ PTH204
|
= help: Replace with `Path.stat(...).st_mtime`
Safe fix
10 10 |
11 11 | getmtime("filename")
12 12 | getmtime(b"filename")
13 |-getmtime(Path("filename"))
13 |+Path("filename").stat().st_mtime

View File

@@ -0,0 +1,114 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH205.py:6:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
6 | os.path.getctime("filename")
| ^^^^^^^^^^^^^^^^ PTH205
7 | os.path.getctime(b"filename")
8 | os.path.getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
3 3 | from os.path import getctime
4 4 |
5 5 |
6 |-os.path.getctime("filename")
6 |+Path("filename").stat().st_ctime
7 7 | os.path.getctime(b"filename")
8 8 | os.path.getctime(Path("filename"))
9 9 |
PTH205.py:7:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
6 | os.path.getctime("filename")
7 | os.path.getctime(b"filename")
| ^^^^^^^^^^^^^^^^ PTH205
8 | os.path.getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
4 4 |
5 5 |
6 6 | os.path.getctime("filename")
7 |-os.path.getctime(b"filename")
7 |+Path(b"filename").stat().st_ctime
8 8 | os.path.getctime(Path("filename"))
9 9 |
10 10 | getctime("filename")
PTH205.py:8:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
6 | os.path.getctime("filename")
7 | os.path.getctime(b"filename")
8 | os.path.getctime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH205
9 |
10 | getctime("filename")
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
5 5 |
6 6 | os.path.getctime("filename")
7 7 | os.path.getctime(b"filename")
8 |-os.path.getctime(Path("filename"))
8 |+Path("filename").stat().st_ctime
9 9 |
10 10 | getctime("filename")
11 11 | getctime(b"filename")
PTH205.py:10:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
8 | os.path.getctime(Path("filename"))
9 |
10 | getctime("filename")
| ^^^^^^^^ PTH205
11 | getctime(b"filename")
12 | getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
7 7 | os.path.getctime(b"filename")
8 8 | os.path.getctime(Path("filename"))
9 9 |
10 |-getctime("filename")
10 |+Path("filename").stat().st_ctime
11 11 | getctime(b"filename")
12 12 | getctime(Path("filename"))
PTH205.py:11:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
10 | getctime("filename")
11 | getctime(b"filename")
| ^^^^^^^^ PTH205
12 | getctime(Path("filename"))
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
8 8 | os.path.getctime(Path("filename"))
9 9 |
10 10 | getctime("filename")
11 |-getctime(b"filename")
11 |+Path(b"filename").stat().st_ctime
12 12 | getctime(Path("filename"))
PTH205.py:12:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime`
|
10 | getctime("filename")
11 | getctime(b"filename")
12 | getctime(Path("filename"))
| ^^^^^^^^ PTH205
|
= help: Replace with `Path.stat(...).st_ctime`
Safe fix
9 9 |
10 10 | getctime("filename")
11 11 | getctime(b"filename")
12 |-getctime(Path("filename"))
12 |+Path("filename").stat().st_ctime

View File

@@ -24,13 +24,14 @@ use crate::checkers::ast::Checker;
/// ## Example
/// ```python
/// with open("file", "rwx") as f:
/// return f.read()
/// content = f.read()
/// ```
///
/// Use instead:
///
/// ```python
/// with open("file", "r") as f:
/// return f.read()
/// content = f.read()
/// ```
///
/// ## References

View File

@@ -15,21 +15,23 @@ use crate::{Edit, Fix, FixAvailability, Violation};
///
/// ## Example
/// ```python
/// for x in foo:
/// yield x
/// def bar():
/// for x in foo:
/// yield x
///
/// global y
/// for y in foo:
/// yield y
/// global y
/// for y in foo:
/// yield y
/// ```
///
/// Use instead:
/// ```python
/// yield from foo
/// def bar():
/// yield from foo
///
/// for _element in foo:
/// y = _element
/// yield y
/// for _element in foo:
/// y = _element
/// yield y
/// ```
///
/// ## Fix safety

View File

@@ -91,7 +91,7 @@ impl SequenceType {
.map(Ranged::start)
.unwrap_or(pattern.end()),
)];
let after_last_patttern = &source[TextRange::new(
let after_last_pattern = &source[TextRange::new(
pattern.start(),
pattern
.patterns
@@ -100,7 +100,7 @@ impl SequenceType {
.unwrap_or(pattern.end()),
)];
if before_first_pattern.starts_with('[') && !after_last_patttern.ends_with(',') {
if before_first_pattern.starts_with('[') && !after_last_pattern.ends_with(',') {
SequenceType::List
} else if before_first_pattern.starts_with('(') {
// If the pattern is empty, it must be a parenthesized tuple with no members. (This

View File

@@ -26,8 +26,11 @@ use self::traits::{NotificationHandler, RequestHandler};
use super::{Result, schedule::BackgroundSchedule};
/// Defines the `document_url` method for implementers of [`traits::Notification`] and [`traits::Request`],
/// given the parameter type used by the implementer.
/// Defines the `document_url` method for implementers of [`Notification`] and [`Request`], given
/// the request or notification parameter type.
///
/// This would only work if the parameter type has a `text_document` field with a `uri` field
/// that is of type [`lsp_types::Url`].
macro_rules! define_document_url {
($params:ident: &$p:ty) => {
fn document_url($params: &$p) -> std::borrow::Cow<lsp_types::Url> {

View File

@@ -1,4 +1,31 @@
//! A stateful LSP implementation that calls into the Ruff API.
//! Traits for handling requests and notifications from the LSP client.
//!
//! This module defines the trait abstractions used by the language server to handle incoming
//! requests and notifications from clients. It provides a type-safe way to implement LSP handlers
//! with different execution models (synchronous or asynchronous) and automatic retry capabilities.
//!
//! All request and notification handlers must implement the base traits [`RequestHandler`] and
//! [`NotificationHandler`], respectively, which associate them with specific LSP request or
//! notification types. These base traits are then extended by more specific traits that define
//! the execution model of the handler.
//!
//! The [`SyncRequestHandler`] and [`SyncNotificationHandler`] traits are for handlers that
//! executes synchronously on the main loop, providing mutable access to the [`Session`] that
//! contains the current state of the server. This is useful for handlers that need to modify
//! the server state such as when the content of a file changes.
//!
//! The [`BackgroundDocumentRequestHandler`] and [`BackgroundDocumentNotificationHandler`] traits
//! are for handlers that operate on a single document and can be executed on a background thread.
//! These handlers will have access to a snapshot of the document at the time of the request or
//! notification, allowing them to perform operations without blocking the main loop.
//!
//! The [`SyncNotificationHandler`] is the most common trait that would be used because most
//! notifications are specific to a single document and require updating the server state.
//! Similarly, the [`BackgroundDocumentRequestHandler`] is the most common request handler that
//! would be used as most requests are document-specific and can be executed in the background.
//!
//! See the `./requests` and `./notifications` directories for concrete implementations of these
//! traits in action.
use crate::session::{Client, DocumentSnapshot, Session};
@@ -12,9 +39,10 @@ pub(super) trait RequestHandler {
}
/// A request handler that needs mutable access to the session.
/// This will block the main message receiver loop, meaning that no
/// incoming requests or notifications will be handled while `run` is
/// executing. Try to avoid doing any I/O or long-running computations.
///
/// This will block the main message receiver loop, meaning that no incoming requests or
/// notifications will be handled while `run` is executing. Try to avoid doing any I/O or
/// long-running computations.
pub(super) trait SyncRequestHandler: RequestHandler {
fn run(
session: &mut Session,
@@ -24,10 +52,14 @@ pub(super) trait SyncRequestHandler: RequestHandler {
}
/// A request handler that can be run on a background thread.
///
/// This handler is specific to requests that operate on a single document.
pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
/// `document_url` can be implemented automatically with
/// `define_document_url!(params: &<YourParameterType>)` in the trait
/// implementation.
/// Returns the URL of the document that this request handler operates on.
///
/// This method can be implemented automatically using the [`define_document_url`] macro.
///
/// [`define_document_url`]: super::define_document_url
fn document_url(
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
) -> std::borrow::Cow<lsp_types::Url>;
@@ -47,9 +79,10 @@ pub(super) trait NotificationHandler {
}
/// A notification handler that needs mutable access to the session.
/// This will block the main message receiver loop, meaning that no
/// incoming requests or notifications will be handled while `run` is
/// executing. Try to avoid doing any I/O or long-running computations.
///
/// This will block the main message receiver loop, meaning that no incoming requests or
/// notifications will be handled while `run` is executing. Try to avoid doing any I/O or
/// long-running computations.
pub(super) trait SyncNotificationHandler: NotificationHandler {
fn run(
session: &mut Session,
@@ -60,9 +93,11 @@ pub(super) trait SyncNotificationHandler: NotificationHandler {
/// A notification handler that can be run on a background thread.
pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler {
/// `document_url` can be implemented automatically with
/// `define_document_url!(params: &<YourParameterType>)` in the trait
/// implementation.
/// Returns the URL of the document that this notification handler operates on.
///
/// This method can be implemented automatically using the [`define_document_url`] macro.
///
/// [`define_document_url`]: super::define_document_url
fn document_url(
params: &<<Self as NotificationHandler>::NotificationType as LSPNotification>::Params,
) -> std::borrow::Cow<lsp_types::Url>;

View File

@@ -297,6 +297,7 @@ possibly-unresolved-reference = "ignore"
A list of file and directory patterns to exclude from type checking.
Patterns follow a syntax similar to `.gitignore`:
- `./src/` matches only a directory
- `./src` matches both files and directories
- `src` matches files or directories named `src`

View File

@@ -296,6 +296,11 @@ impl MainLoop {
tracing::warn!("No python files found under the given path(s)");
}
// TODO: We should have an official flag to silence workspace diagnostics.
if std::env::var("TY_MEMORY_REPORT").as_deref() == Ok("mypy_primer") {
return Ok(ExitStatus::Success);
}
let mut stdout = stdout().lock();
if result.is_empty() {

View File

@@ -11,6 +11,7 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
bitflags = { workspace = true }
ruff_db = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }

View File

@@ -1,13 +1,12 @@
use crate::find_node::covering_node;
use crate::{Db, HasNavigationTargets, NavigationTarget, NavigationTargets, RangedValue};
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::semantic_index::definition::Definition;
use ty_python_semantic::types::Type;
use ty_python_semantic::{HasDefinition, HasType, SemanticModel};
use ty_python_semantic::{HasType, SemanticModel};
pub fn goto_type_definition(
db: &dyn Db,
@@ -30,35 +29,6 @@ pub fn goto_type_definition(
})
}
pub fn goto_definition(
db: &dyn Db,
file: File,
offset: TextSize,
) -> Option<RangedValue<NavigationTargets>> {
let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db, file);
let definitions = goto_target.definitions(&model)?;
tracing::debug!("Definitions of covering node is found");
let targets = definitions.into_iter().map(|definition| {
let full_range = definition.full_range(db, &module);
NavigationTarget {
file: full_range.file(),
focus_range: definition.focus_range(db, &module).range(),
full_range: full_range.range(),
}
});
Some(RangedValue {
range: FileRange::new(file, goto_target.range()),
value: NavigationTargets::unique(targets),
})
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum GotoTarget<'a> {
Expression(ast::ExprRef<'a>),
@@ -184,16 +154,6 @@ impl GotoTarget<'_> {
Some(ty)
}
pub(crate) fn definitions<'db>(
self,
model: &SemanticModel<'db>,
) -> Option<Vec<Definition<'db>>> {
match self {
GotoTarget::Expression(expr_ref) => expr_ref.definitions(model),
_ => None,
}
}
}
impl Ranged for GotoTarget<'_> {
@@ -294,7 +254,7 @@ pub(crate) fn find_goto_target(
#[cfg(test)]
mod tests {
use crate::tests::{CursorTest, IntoDiagnostic, cursor_test};
use crate::{NavigationTarget, goto_definition, goto_type_definition};
use crate::{NavigationTarget, goto_type_definition};
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
@@ -832,13 +792,13 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/types.pyi:689:11
--> stdlib/types.pyi:691:11
|
687 | if sys.version_info >= (3, 10):
688 | @final
689 | class NoneType:
689 | if sys.version_info >= (3, 10):
690 | @final
691 | class NoneType:
| ^^^^^^^^
690 | def __bool__(self) -> Literal[False]: ...
692 | def __bool__(self) -> Literal[False]: ...
|
info: Source
--> main.py:3:17
@@ -868,516 +828,6 @@ f(**kwargs<CURSOR>)
");
}
#[test]
fn goto_def_function_call() {
let test = cursor_test(
r#"
def ab(a, b): ...
a<CURSOR>b(1, 2)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:17
|
2 | def ab(a, b): ...
| ^^
3 |
4 | ab(1, 2)
|
info: Source
--> main.py:4:13
|
2 | def ab(a, b): ...
3 |
4 | ab(1, 2)
| ^^
|
");
}
#[test]
fn goto_def_local_load() {
let test = cursor_test(
r#"
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:13
|
2 | ab = 1
| ^^
3 | print(ab)
|
info: Source
--> main.py:3:19
|
2 | ab = 1
3 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_rebind() {
let test = cursor_test(
r#"
ab = 1
ab = 2
ab = 3
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:4:13
|
2 | ab = 1
3 | ab = 2
4 | ab = 3
| ^^
5 | print(ab)
|
info: Source
--> main.py:5:19
|
3 | ab = 2
4 | ab = 3
5 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_cond_rebind() {
let test = cursor_test(
r#"
ab = 1
if cond:
ab = 2
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:13
|
2 | ab = 1
| ^^
3 | if cond:
4 | ab = 2
|
info: Source
--> main.py:5:19
|
3 | if cond:
4 | ab = 2
5 | print(ab)
| ^^
|
info[goto-type-definition]: Type definition
--> main.py:4:17
|
2 | ab = 1
3 | if cond:
4 | ab = 2
| ^^
5 | print(ab)
|
info: Source
--> main.py:5:19
|
3 | if cond:
4 | ab = 2
5 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_exhaustive_bind() {
let test = cursor_test(
r#"
if cond:
ab = 2
else:
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:3:17
|
2 | if cond:
3 | ab = 2
| ^^
4 | else:
5 | ab = 1
|
info: Source
--> main.py:6:19
|
4 | else:
5 | ab = 1
6 | print(ab)
| ^^
|
info[goto-type-definition]: Type definition
--> main.py:5:17
|
3 | ab = 2
4 | else:
5 | ab = 1
| ^^
6 | print(ab)
|
info: Source
--> main.py:6:19
|
4 | else:
5 | ab = 1
6 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_only_decl() {
let test = cursor_test(
r#"
ab: int
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @"No definitions found");
}
#[test]
fn goto_def_local_load_exhaustive_bind_decl() {
let test = cursor_test(
r#"
ab: int
if cond:
ab = 2
else:
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:4:17
|
2 | ab: int
3 | if cond:
4 | ab = 2
| ^^
5 | else:
6 | ab = 1
|
info: Source
--> main.py:7:19
|
5 | else:
6 | ab = 1
7 | print(ab)
| ^^
|
info[goto-type-definition]: Type definition
--> main.py:6:17
|
4 | ab = 2
5 | else:
6 | ab = 1
| ^^
7 | print(ab)
|
info: Source
--> main.py:7:19
|
5 | else:
6 | ab = 1
7 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_bind_decl() {
let test = cursor_test(
r#"
ab: int
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:3:13
|
2 | ab: int
3 | ab = 1
| ^^
4 | print(ab)
|
info: Source
--> main.py:4:19
|
2 | ab: int
3 | ab = 1
4 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_first_store() {
let test = cursor_test(
r#"
a<CURSOR>b = 1
print(ab)
ab = 2
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_local_second_store() {
let test = cursor_test(
r#"
ab = 1
print(ab)
a<CURSOR>b = 2
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_local_loadstore() {
let test = cursor_test(
r#"
ab = 1
print(ab)
a<CURSOR>b += 2
print(ab)
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_class() {
let test = cursor_test(
r#"
class AB:
def __init__(self, val: int):
self.myval = val
x = A<CURSOR>B(5)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:19
|
2 | class AB:
| ^^
3 | def __init__(self, val: int):
4 | self.myval = val
|
info: Source
--> main.py:6:17
|
4 | self.myval = val
5 |
6 | x = AB(5)
| ^^
|
");
}
#[test]
fn goto_def_class_implicit_instance_variable() {
let test = cursor_test(
r#"
class AB:
def __init__(self, val: int):
self.myval = val
x = AB(5)
print(x.my<CURSOR>val)
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_class_explicit_instance_variable() {
let test = cursor_test(
r#"
class AB:
myval: int
def __init__(self, val: int):
self.myval = val
x = AB(5)
print(x.my<CURSOR>val)
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_path_parent() {
let test = cursor_test(
r#"
class AB:
def __init__(self, val: int):
self.myval = val
xyz = AB(5)
print(x<CURSOR>yz.myval)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:6:13
|
4 | self.myval = val
5 |
6 | xyz = AB(5)
| ^^^
7 | print(xyz.myval)
|
info: Source
--> main.py:7:19
|
6 | xyz = AB(5)
7 | print(xyz.myval)
| ^^^
|
");
}
#[test]
fn goto_def_class_class_variable() {
let test = cursor_test(
r#"
class AB:
RED = "red"
BLUE = "blue"
x = AB.RE<CURSOR>D
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_class_path_parent() {
let test = cursor_test(
r#"
class AB:
RED = "red"
BLUE = "blue"
x = A<CURSOR>B.RED
"#,
);
assert_snapshot!(test.goto_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:2:19
|
2 | class AB:
| ^^
3 | RED = "red"
4 | BLUE = "blue"
|
info: Source
--> main.py:6:17
|
4 | BLUE = "blue"
5 |
6 | x = AB.RED
| ^^
|
"#);
}
#[test]
fn goto_def_global_decl() {
let test = cursor_test(
r#"
ab = 1
def myfunc():
global a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_global_load() {
let test = cursor_test(
r#"
ab = 1
def myfunc():
global ab
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @"No definitions found");
}
#[test]
fn goto_def_global_store() {
let test = cursor_test(
r#"
ab = 1
def myfunc():
global ab
a<CURSOR>b = 2
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
impl CursorTest {
fn goto_type_definition(&self) -> String {
let Some(targets) =
@@ -1397,24 +847,6 @@ f(**kwargs<CURSOR>)
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
)
}
fn goto_definition(&self) -> String {
let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset)
else {
return "No goto target found".to_string();
};
if targets.is_empty() {
return "No definitions found".to_string();
}
let source = targets.range;
self.render_diagnostics(
targets
.into_iter()
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
)
}
}
struct GotoTypeDefinitionDiagnostic {

View File

@@ -5,13 +5,17 @@ mod goto;
mod hover;
mod inlay_hints;
mod markup;
mod semantic_tokens;
pub use completion::completion;
pub use db::Db;
pub use goto::{goto_definition, goto_type_definition};
pub use goto::goto_type_definition;
pub use hover::hover;
pub use inlay_hints::inlay_hints;
pub use markup::MarkupKind;
pub use semantic_tokens::{
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens,
};
use ruff_db::files::{File, FileRange};
use ruff_text_size::{Ranged, TextRange};

File diff suppressed because it is too large Load Diff

View File

@@ -324,8 +324,8 @@ impl SalsaMemoryDump {
struct DisplayShort<'a>(&'a SalsaMemoryDump);
fn round_memory(total: usize) -> usize {
// Round the number to the nearest power of 1.1. This gives us a
// 5% threshold before the memory usage number is considered to have
// Round the number to the nearest power of 1.05. This gives us a
// 2.5% threshold before the memory usage number is considered to have
// changed.
//
// TODO: Small changes in memory usage may cause the number to be rounded
@@ -334,7 +334,7 @@ impl SalsaMemoryDump {
// over time that are unrelated to the current change. Ideally we could compare
// the exact numbers across runs and compute the difference, but we don't have
// the infrastructure for that currently.
const BASE: f64 = 1.1;
const BASE: f64 = 1.05;
BASE.powf(bytes_to_mb(total).log(BASE).round()) as usize
}

View File

@@ -577,6 +577,7 @@ pub struct SrcOptions {
/// A list of file and directory patterns to exclude from type checking.
///
/// Patterns follow a syntax similar to `.gitignore`:
///
/// - `./src/` matches only a directory
/// - `./src` matches both files and directories
/// - `src` matches files or directories named `src`

View File

@@ -343,7 +343,7 @@ def _(c: Callable[[int, Unpack[Ts]], int]):
from typing import Callable
def _(c: Callable[[int], int]):
reveal_type(c.__init__) # revealed: def __init__(self) -> None
reveal_type(c.__init__) # revealed: bound method object.__init__() -> None
reveal_type(c.__class__) # revealed: type
reveal_type(c.__call__) # revealed: (int, /) -> int
```

View File

@@ -37,9 +37,7 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion.
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
# TODO: Should be `bytes` with no error, like mypy and pyright?
# error: [unresolved-attribute]
reveal_type(c_instance.declared_only) # revealed: Unknown
reveal_type(c_instance.declared_only) # revealed: bytes
reveal_type(c_instance.declared_and_bound) # revealed: bool
@@ -58,6 +56,9 @@ c_instance.declared_and_bound = "incompatible"
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `<class 'C'>` itself."
reveal_type(C.inferred_from_value) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(C.declared_and_bound) # revealed: Unknown
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `<class 'C'>`"
C.inferred_from_value = "overwritten on class"
@@ -143,16 +144,14 @@ class C:
c_instance = C(True)
reveal_type(c_instance.only_declared_in_body) # revealed: str | None
# TODO: should be `str | None` without error
# error: [unresolved-attribute]
reveal_type(c_instance.only_declared_in_init) # revealed: Unknown
reveal_type(c_instance.only_declared_in_init) # revealed: str | None
reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None
reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
# TODO: This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API,
# which is planned in https://github.com/astral-sh/ruff/issues/14297
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | Literal["a"]
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
```
@@ -183,9 +182,7 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
# TODO: should be `bytes` with no error, like mypy and pyright?
# error: [unresolved-attribute]
reveal_type(c_instance.declared_only) # revealed: Unknown
reveal_type(c_instance.declared_only) # revealed: bytes
reveal_type(c_instance.declared_and_bound) # revealed: bool
@@ -692,16 +689,14 @@ class C:
reveal_type(C.pure_class_variable1) # revealed: str
# TODO: Should be `Unknown | Literal[1]`.
reveal_type(C.pure_class_variable2) # revealed: Unknown
reveal_type(C.pure_class_variable2) # revealed: Unknown | Literal[1]
c_instance = C()
# It is okay to access a pure class variable on an instance.
reveal_type(c_instance.pure_class_variable1) # revealed: str
# TODO: Should be `Unknown | Literal[1]`.
reveal_type(c_instance.pure_class_variable2) # revealed: Unknown
reveal_type(c_instance.pure_class_variable2) # revealed: Unknown | Literal[1]
# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `C`"
c_instance.pure_class_variable1 = "value set on instance"
@@ -717,6 +712,24 @@ class Subclass(C):
reveal_type(Subclass.pure_class_variable1) # revealed: str
```
If a class variable is additionally qualified as `Final`, we do not union with `Unknown` for bare
`ClassVar`s:
```py
from typing import Final
class D:
final1: Final[ClassVar] = 1
final2: ClassVar[Final] = 1
final3: ClassVar[Final[int]] = 1
final4: Final[ClassVar[int]] = 1
reveal_type(D.final1) # revealed: Literal[1]
reveal_type(D.final2) # revealed: Literal[1]
reveal_type(D.final3) # revealed: int
reveal_type(D.final4) # revealed: int
```
#### Variable only mentioned in a class method
We also consider a class variable to be a pure class variable if it is only mentioned in a class
@@ -1774,7 +1787,7 @@ date.day = 8
date.month = 4
date.year = 2025
# error: [unresolved-attribute] "Can not assign object of `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method."
# error: [unresolved-attribute] "Can not assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method."
date.tz = "UTC"
```

View File

@@ -201,6 +201,36 @@ type IntOrStr = int | str
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm
```
## Method calls on types not disjoint from `None`
Very few methods are defined on `object`, `None`, and other types not disjoint from `None`. However,
descriptor-binding behaviour works on these types in exactly the same way as descriptor binding on
other types. This is despite the fact that `None` is used as a sentinel internally by the descriptor
protocol to indicate that a method was accessed on the class itself rather than an instance of the
class:
```py
from typing import Protocol, Literal
from ty_extensions import AlwaysFalsy
class Foo: ...
class SupportsStr(Protocol):
def __str__(self) -> str: ...
class Falsy(Protocol):
def __bool__(self) -> Literal[False]: ...
def _(a: object, b: SupportsStr, c: Falsy, d: AlwaysFalsy, e: None, f: Foo | None):
a.__str__()
b.__str__()
c.__str__()
d.__str__()
# TODO: these should not error
e.__str__() # error: [missing-argument]
f.__str__() # error: [missing-argument]
```
## Error cases: Calling `__get__` for methods
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
@@ -234,16 +264,18 @@ method_wrapper(C())
method_wrapper(C(), None)
method_wrapper(None, C)
# Passing `None` without an `owner` argument is an
# error: [invalid-argument-type] "Argument to method wrapper `__get__` of function `f` is incorrect: Expected `~None`, found `None`"
reveal_type(object.__str__.__get__(object(), None)()) # revealed: str
# TODO: passing `None` without an `owner` argument fails at runtime.
# Ideally we would emit a diagnostic here:
method_wrapper(None)
# Passing something that is not assignable to `type` as the `owner` argument is an
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
method_wrapper(None, 1)
# Passing `None` as the `owner` argument when `instance` is `None` is an
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
# TODO: passing `None` as the `owner` argument when `instance` is `None` fails at runtime.
# Ideally we would emit a diagnostic here.
method_wrapper(None, None)
# Calling `__get__` without any arguments is an

View File

@@ -444,6 +444,33 @@ To do
To do
## `Final` fields
Dataclass fields can be annotated with `Final`, which means that the field cannot be reassigned
after the instance is created. Fields that are additionally annotated with `ClassVar` are not part
of the `__init__` signature.
```py
from dataclasses import dataclass
from typing import Final, ClassVar
@dataclass
class C:
# a `Final` annotation without a right-hand side is not allowed in normal classes,
# but valid for dataclasses. The field will be initialized in the synthesized
# `__init__` method
instance_variable_no_default: Final[int]
instance_variable: Final[int] = 1
class_variable1: ClassVar[Final[int]] = 1
class_variable2: ClassVar[Final[int]] = 1
reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None
c = C(1)
# TODO: this should be an error
c.instance_variable = 2
```
## Inheritance
### Normal class inheriting from a dataclass

View File

@@ -619,8 +619,9 @@ wrapper_descriptor()
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
wrapper_descriptor(f)
# Calling it without the `owner` argument if `instance` is not `None` is an
# error: [invalid-argument-type] "Argument to wrapper descriptor `FunctionType.__get__` is incorrect: Expected `~None`, found `None`"
# TODO: Calling it without the `owner` argument if `instance` is not `None` fails at runtime.
# Ideally we would emit a diagnostic here,
# but this is hard to model without introducing false positives elsewhere
wrapper_descriptor(f, None)
# But calling it with an instance is fine (in this case, the `owner` argument is optional):

View File

@@ -2,27 +2,53 @@
## Basic functionality
<!-- snapshot-diagnostics -->
`assert_never` makes sure that the type of the argument is `Never`.
`assert_never` makes sure that the type of the argument is `Never`. If it is not, a
`type-assertion-failure` diagnostic is emitted.
### Correct usage
```py
from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown
def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
def _(never: Never):
assert_never(never) # fine
```
### Diagnostics
<!-- snapshot-diagnostics -->
If it is not, a `type-assertion-failure` diagnostic is emitted.
```py
from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown
def _():
assert_never(0) # error: [type-assertion-failure]
def _():
assert_never("") # error: [type-assertion-failure]
def _():
assert_never(None) # error: [type-assertion-failure]
def _():
assert_never([]) # error: [type-assertion-failure]
def _():
assert_never({}) # error: [type-assertion-failure]
def _():
assert_never(()) # error: [type-assertion-failure]
def _(flag: bool, never: Never):
assert_never(1 if flag else never) # error: [type-assertion-failure]
def _(any_: Any):
assert_never(any_) # error: [type-assertion-failure]
def _(unknown: Unknown):
assert_never(unknown) # error: [type-assertion-failure]
```

View File

@@ -205,3 +205,18 @@ python-version = "3.13"
import aifc # error: [unresolved-import]
from distutils import sysconfig # error: [unresolved-import]
```
## Cannot shadow core standard library modules
`types.py`:
```py
x: int
```
```py
# error: [unresolved-import]
from types import x
from types import FunctionType
```

View File

@@ -1476,8 +1476,7 @@ class P1(Protocol):
class P2(Protocol):
def x(self, y: int) -> None: ...
# TODO: this should pass
static_assert(is_equivalent_to(P1, P2)) # error: [static-assert-error]
static_assert(is_equivalent_to(P1, P2))
```
As with protocols that only have non-method members, this also holds true when they appear in
@@ -1487,8 +1486,7 @@ differently ordered unions:
class A: ...
class B: ...
# TODO: this should pass
static_assert(is_equivalent_to(A | B | P1, P2 | B | A)) # error: [static-assert-error]
static_assert(is_equivalent_to(A | B | P1, P2 | B | A))
```
## Narrowing of protocols
@@ -1862,6 +1860,21 @@ class Bar(Protocol):
static_assert(is_equivalent_to(Foo, Bar))
```
### Disjointness of recursive protocol and recursive final type
```py
from typing import Protocol
from ty_extensions import is_disjoint_from, static_assert
class Proto(Protocol):
x: "Proto"
class Nominal:
x: "Nominal"
static_assert(not is_disjoint_from(Proto, Nominal))
```
### Regression test: narrowing with self-referential protocols
This snippet caused us to panic on an early version of the implementation for protocols.
@@ -1881,6 +1894,86 @@ if isinstance(obj, (B, A)):
reveal_type(obj) # revealed: (Unknown & B) | (Unknown & A)
```
### Protocols that use `Self`
`Self` is a `TypeVar` with an upper bound of the class in which it is defined. This means that
`Self` annotations in protocols can also be tricky to handle without infinite recursion and stack
overflows.
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import Protocol, Self
from ty_extensions import static_assert
class _HashObject(Protocol):
def copy(self) -> Self: ...
class Foo: ...
# Attempting to build this union caused us to overflow on an early version of
# <https://github.com/astral-sh/ruff/pull/18659>
x: Foo | _HashObject
```
Some other similar cases that caused issues in our early `Protocol` implementation:
`a.py`:
```py
from typing_extensions import Protocol, Self
class PGconn(Protocol):
def connect(self) -> Self: ...
class Connection:
pgconn: PGconn
def is_crdb(conn: PGconn) -> bool:
return isinstance(conn, Connection)
```
and:
`b.py`:
```py
from typing_extensions import Protocol
class PGconn(Protocol):
def connect[T: PGconn](self: T) -> T: ...
class Connection:
pgconn: PGconn
def f(x: PGconn):
isinstance(x, Connection)
```
### Recursive protocols used as the first argument to `cast()`
These caused issues in an early version of our `Protocol` implementation due to the fact that we use
a recursive function in our `cast()` implementation to check whether a type contains `Unknown` or
`Todo`. Recklessly recursing into a type causes stack overflows if the type is recursive:
```toml
[environment]
python-version = "3.12"
```
```py
from typing import cast, Protocol
class Iterator[T](Protocol):
def __iter__(self) -> Iterator[T]: ...
def f(value: Iterator):
cast(Iterator, value) # error: [redundant-cast]
```
## TODO
Add tests for:

View File

@@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assert_never.md - `assert_never` - Basic functionality
mdtest name: assert_never.md - `assert_never` - Basic functionality - Diagnostics
mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.md
---
@@ -15,35 +15,47 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.
1 | from typing_extensions import assert_never, Never, Any
2 | from ty_extensions import Unknown
3 |
4 | def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
5 | assert_never(never) # fine
4 | def _():
5 | assert_never(0) # error: [type-assertion-failure]
6 |
7 | assert_never(0) # error: [type-assertion-failure]
7 | def _():
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
14 |
15 | assert_never(any_) # error: [type-assertion-failure]
16 | assert_never(unknown) # error: [type-assertion-failure]
9 |
10 | def _():
11 | assert_never(None) # error: [type-assertion-failure]
12 |
13 | def _():
14 | assert_never([]) # error: [type-assertion-failure]
15 |
16 | def _():
17 | assert_never({}) # error: [type-assertion-failure]
18 |
19 | def _():
20 | assert_never(()) # error: [type-assertion-failure]
21 |
22 | def _(flag: bool, never: Never):
23 | assert_never(1 if flag else never) # error: [type-assertion-failure]
24 |
25 | def _(any_: Any):
26 | assert_never(any_) # error: [type-assertion-failure]
27 |
28 | def _(unknown: Unknown):
29 | assert_never(unknown) # error: [type-assertion-failure]
```
# Diagnostics
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:7:5
--> src/mdtest_snippet.py:5:5
|
5 | assert_never(never) # fine
6 |
7 | assert_never(0) # error: [type-assertion-failure]
4 | def _():
5 | assert_never(0) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^-^
| |
| Inferred type of argument is `Literal[0]`
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
6 |
7 | def _():
|
info: `Never` and `Literal[0]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -54,13 +66,13 @@ info: rule `type-assertion-failure` is enabled by default
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:8:5
|
7 | assert_never(0) # error: [type-assertion-failure]
7 | def _():
8 | assert_never("") # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `Literal[""]`
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
9 |
10 | def _():
|
info: `Never` and `Literal[""]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -69,16 +81,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:9:5
--> src/mdtest_snippet.py:11:5
|
7 | assert_never(0) # error: [type-assertion-failure]
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
10 | def _():
11 | assert_never(None) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^----^
| |
| Inferred type of argument is `None`
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
12 |
13 | def _():
|
info: `Never` and `None` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -87,16 +98,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:10:5
--> src/mdtest_snippet.py:14:5
|
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
13 | def _():
14 | assert_never([]) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `list[Unknown]`
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
15 |
16 | def _():
|
info: `Never` and `list[Unknown]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -105,16 +115,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:11:5
--> src/mdtest_snippet.py:17:5
|
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
16 | def _():
17 | assert_never({}) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `dict[Unknown, Unknown]`
12 | assert_never(()) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
18 |
19 | def _():
|
info: `Never` and `dict[Unknown, Unknown]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -123,15 +132,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:12:5
--> src/mdtest_snippet.py:20:5
|
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
19 | def _():
20 | assert_never(()) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `tuple[()]`
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
21 |
22 | def _(flag: bool, never: Never):
|
info: `Never` and `tuple[()]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -140,16 +149,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:13:5
--> src/mdtest_snippet.py:23:5
|
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
22 | def _(flag: bool, never: Never):
23 | assert_never(1 if flag else never) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--------------------^
| |
| Inferred type of argument is `Literal[1]`
14 |
15 | assert_never(any_) # error: [type-assertion-failure]
24 |
25 | def _(any_: Any):
|
info: `Never` and `Literal[1]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -158,15 +166,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:15:5
--> src/mdtest_snippet.py:26:5
|
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
14 |
15 | assert_never(any_) # error: [type-assertion-failure]
25 | def _(any_: Any):
26 | assert_never(any_) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^----^
| |
| Inferred type of argument is `Any`
16 | assert_never(unknown) # error: [type-assertion-failure]
27 |
28 | def _(unknown: Unknown):
|
info: `Never` and `Any` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@@ -175,10 +183,10 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:16:5
--> src/mdtest_snippet.py:29:5
|
15 | assert_never(any_) # error: [type-assertion-failure]
16 | assert_never(unknown) # error: [type-assertion-failure]
28 | def _(unknown: Unknown):
29 | assert_never(unknown) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^-------^
| |
| Inferred type of argument is `Unknown`

View File

@@ -570,6 +570,229 @@ def f():
reveal_type(x) # revealed: Literal[1]
```
## Calls to functions returning `Never` / `NoReturn`
These calls should be treated as terminal statements.
### No implicit return
If we see a call to a function returning `Never`, we should be able to understand that the function
cannot implicitly return `None`. In the below examples, verify that there are no errors emitted for
invalid return type.
```py
from typing import NoReturn
import sys
def f() -> NoReturn:
sys.exit(1)
```
Let's try cases where the function annotated with `NoReturn` is some sub-expression.
```py
from typing import NoReturn
import sys
# TODO: this is currently not yet supported
# error: [invalid-return-type]
def _() -> NoReturn:
3 + sys.exit(1)
# TODO: this is currently not yet supported
# error: [invalid-return-type]
def _() -> NoReturn:
3 if sys.exit(1) else 4
```
### Type narrowing
If a variable's type is a union, and some types in the union result in a function marked with
`NoReturn` being called, then we should correctly narrow the variable's type.
```py
from typing import NoReturn
import sys
def g(x: int | None):
if x is None:
sys.exit(1)
# TODO: should be just `int`, not `int | None`
# See https://github.com/astral-sh/ty/issues/685
reveal_type(x) # revealed: int | None
```
### Possibly unresolved diagnostics
If the codepath on which a variable is not defined eventually returns `Never`, use of the variable
should not give any diagnostics.
```py
import sys
def _(flag: bool):
if flag:
x = 3
else:
sys.exit()
x # No possibly-unresolved-references diagnostic here.
```
Similarly, there shouldn't be any diagnostics if the `except` block of a `try/except` construct has
a call with `NoReturn`.
```py
import sys
def _():
try:
x = 3
except:
sys.exit()
x # No possibly-unresolved-references diagnostic here.
```
### Bindings in branches
In case of a `NoReturn` call being present in conditionals, the revealed type of the end of the
branch should reflect the path which did not hit any of the `NoReturn` calls. These tests are
similar to the ones for `return` above.
```py
import sys
def call_in_then_branch(cond: bool):
if cond:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
sys.exit()
else:
x = "test"
reveal_type(x) # revealed: Literal["test"]
reveal_type(x) # revealed: Literal["test"]
def call_in_else_branch(cond: bool):
if cond:
x = "test"
reveal_type(x) # revealed: Literal["test"]
else:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
sys.exit()
reveal_type(x) # revealed: Literal["test"]
def call_in_both_branches(cond: bool):
if cond:
x = "terminal1"
reveal_type(x) # revealed: Literal["terminal1"]
sys.exit()
else:
x = "terminal2"
reveal_type(x) # revealed: Literal["terminal2"]
sys.exit()
reveal_type(x) # revealed: Never
def call_in_nested_then_branch(cond1: bool, cond2: bool):
if cond1:
x = "test1"
reveal_type(x) # revealed: Literal["test1"]
else:
if cond2:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
sys.exit()
else:
x = "test2"
reveal_type(x) # revealed: Literal["test2"]
reveal_type(x) # revealed: Literal["test2"]
reveal_type(x) # revealed: Literal["test1", "test2"]
def call_in_nested_else_branch(cond1: bool, cond2: bool):
if cond1:
x = "test1"
reveal_type(x) # revealed: Literal["test1"]
else:
if cond2:
x = "test2"
reveal_type(x) # revealed: Literal["test2"]
else:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
sys.exit()
reveal_type(x) # revealed: Literal["test2"]
reveal_type(x) # revealed: Literal["test1", "test2"]
def call_in_both_nested_branches(cond1: bool, cond2: bool):
if cond1:
x = "test"
reveal_type(x) # revealed: Literal["test"]
else:
x = "terminal0"
if cond2:
x = "terminal1"
reveal_type(x) # revealed: Literal["terminal1"]
sys.exit()
else:
x = "terminal2"
reveal_type(x) # revealed: Literal["terminal2"]
sys.exit()
reveal_type(x) # revealed: Literal["test"]
```
### Overloads
If only some overloads of a function are marked with `NoReturn`, we should run the overload
evaluation algorithm when evaluating the constraints.
```py
from typing import NoReturn, overload
@overload
def f(x: int) -> NoReturn: ...
@overload
def f(x: str) -> int: ...
def f(x): ...
# No errors
def _() -> NoReturn:
f(3)
# This should be an error because of implicitly returning `None`
# error: [invalid-return-type]
def _() -> NoReturn:
f("")
```
### Other callables
If other types of callables are annotated with `NoReturn`, we should still be ablt to infer correct
reachability.
```py
import sys
from typing import NoReturn
class C:
def __call__(self) -> NoReturn:
sys.exit()
def die(self) -> NoReturn:
sys.exit()
# No "implicitly returns `None`" diagnostic
def _() -> NoReturn:
C()()
# No "implicitly returns `None`" diagnostic
def _() -> NoReturn:
C().die()
```
## Nested functions
Free references inside of a function body refer to variables defined in the containing scope.

View File

@@ -300,6 +300,20 @@ static_assert(not is_equivalent_to(CallableTypeOf[f12], CallableTypeOf[f13]))
static_assert(not is_equivalent_to(CallableTypeOf[f13], CallableTypeOf[f12]))
```
### Unions containing `Callable`s
Two unions containing different `Callable` types are equivalent even if the unions are differently
ordered:
```py
from ty_extensions import CallableTypeOf, Unknown, is_equivalent_to, static_assert
def f(x): ...
def g(x: Unknown): ...
static_assert(is_equivalent_to(CallableTypeOf[f] | int | str, str | int | CallableTypeOf[g]))
```
### Unions containing `Callable`s containing unions
Differently ordered unions inside `Callable`s inside unions can still be equivalent:

View File

@@ -21,8 +21,7 @@ class C:
reveal_type(C.a) # revealed: int
reveal_type(C.b) # revealed: int
reveal_type(C.c) # revealed: int
# TODO: should be Unknown | Literal[1]
reveal_type(C.d) # revealed: Unknown
reveal_type(C.d) # revealed: Unknown | Literal[1]
reveal_type(C.e) # revealed: int
c = C()

View File

@@ -3,44 +3,124 @@
[`typing.Final`] is a type qualifier that is used to indicate that a symbol may not be reassigned in
any scope. Final names declared in class scopes cannot be overridden in subclasses.
## Basic
## Basic type inference
### `Final` with type
Declared symbols that are additionally qualified with `Final` use the declared type when accessed
from another scope. Local uses of the symbol will use the inferred type, which may be more specific:
`mod.py`:
```py
from typing import Final, Annotated
FINAL_A: int = 1
FINAL_A: Final[int] = 1
FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1
FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1
FINAL_D: Final = 1
FINAL_E: "Final[int]" = 1
FINAL_D: "Final[int]" = 1
FINAL_F: Final[int]
FINAL_F = 1
reveal_type(FINAL_A) # revealed: Literal[1]
reveal_type(FINAL_B) # revealed: Literal[1]
reveal_type(FINAL_C) # revealed: Literal[1]
reveal_type(FINAL_D) # revealed: Literal[1]
reveal_type(FINAL_E) # revealed: Literal[1]
reveal_type(FINAL_D) # revealed: Literal[1]
# TODO: All of these should be errors:
def nonlocal_uses():
reveal_type(FINAL_A) # revealed: int
reveal_type(FINAL_B) # revealed: int
reveal_type(FINAL_C) # revealed: int
reveal_type(FINAL_D) # revealed: int
reveal_type(FINAL_F) # revealed: int
```
Imported types:
```py
from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_F
reveal_type(FINAL_A) # revealed: int
reveal_type(FINAL_B) # revealed: int
reveal_type(FINAL_C) # revealed: int
reveal_type(FINAL_D) # revealed: int
reveal_type(FINAL_F) # revealed: int
```
### `Final` without a type
When a symbol is qualified with `Final` but no type is specified, the type is inferred from the
right-hand side of the assignment. We do not union the inferred type with `Unknown`, because the
symbol cannot be modified:
`mod.py`:
```py
from typing import Final
FINAL_A: Final = 1
reveal_type(FINAL_A) # revealed: Literal[1]
def nonlocal_uses():
reveal_type(FINAL_A) # revealed: Literal[1]
```
`main.py`:
```py
from mod import FINAL_A
reveal_type(FINAL_A) # revealed: Literal[1]
```
### In class definitions
```py
from typing import Final
class C:
FINAL_A: Final[int] = 1
FINAL_B: Final = 1
def __init__(self):
self.FINAL_C: Final[int] = 1
self.FINAL_D: Final = 1
reveal_type(C.FINAL_A) # revealed: int
reveal_type(C.FINAL_B) # revealed: Literal[1]
reveal_type(C().FINAL_A) # revealed: int
reveal_type(C().FINAL_B) # revealed: Literal[1]
reveal_type(C().FINAL_C) # revealed: int
# TODO: this should be `Literal[1]`
reveal_type(C().FINAL_D) # revealed: Unknown
```
## Not modifiable
Symbols qualified with `Final` cannot be reassigned, and attempting to do so will result in an
error:
```py
from typing import Final, Annotated
FINAL_A: Final[int] = 1
FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1
FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1
FINAL_D: "Final[int]" = 1
FINAL_E: Final[int]
FINAL_E = 1
FINAL_F: Final = 1
# TODO: all of these should be errors
FINAL_A = 2
FINAL_B = 2
FINAL_C = 2
FINAL_D = 2
FINAL_E = 2
```
Public types:
```py
from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E
# TODO: All of these should be Literal[1]
reveal_type(FINAL_A) # revealed: int
reveal_type(FINAL_B) # revealed: int
reveal_type(FINAL_C) # revealed: int
reveal_type(FINAL_D) # revealed: Unknown
reveal_type(FINAL_E) # revealed: int
FINAL_F = 2
```
## Too many arguments
@@ -82,6 +162,10 @@ from typing import Final
# TODO: This should be an error
NO_RHS: Final
class C:
# TODO: This should be an error
NO_RHS: Final
```
[`typing.final`]: https://docs.python.org/3/library/typing.html#typing.Final

View File

@@ -0,0 +1,4 @@
flake8
sphinx
prefect
trio

View File

@@ -15,7 +15,7 @@ pub use program::{
PythonVersionWithSource, SearchPathSettings,
};
pub use python_platform::PythonPlatform;
pub use semantic_model::{Completion, HasDefinition, HasType, NameKind, SemanticModel};
pub use semantic_model::{Completion, HasType, NameKind, SemanticModel};
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;

View File

@@ -156,6 +156,9 @@ pub enum KnownModule {
TyExtensions,
#[strum(serialize = "importlib")]
ImportLib,
#[cfg(test)]
#[strum(serialize = "unittest.mock")]
UnittestMock,
}
impl KnownModule {
@@ -175,6 +178,8 @@ impl KnownModule {
Self::TypeCheckerInternals => "_typeshed._type_checker_internals",
Self::TyExtensions => "ty_extensions",
Self::ImportLib => "importlib",
#[cfg(test)]
Self::UnittestMock => "unittest.mock",
}
}

View File

@@ -535,14 +535,23 @@ struct ModuleNameIngredient<'db> {
pub(super) name: ModuleName,
}
/// Returns `true` if the module name refers to a standard library module which can't be shadowed
/// by a first-party module.
///
/// This includes "builtin" modules, which can never be shadowed at runtime either, as well as the
/// `types` module, which tends to be imported early in Python startup, so can't be consistently
/// shadowed, and is important to type checking.
fn is_non_shadowable(minor_version: u8, module_name: &str) -> bool {
module_name == "types" || ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name)
}
/// Given a module name and a list of search paths in which to lookup modules,
/// attempt to resolve the module name
fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<ResolvedName> {
let program = Program::get(db);
let python_version = program.python_version(db);
let resolver_state = ResolverContext::new(db, python_version);
let is_builtin_module =
ruff_python_stdlib::sys::is_builtin_module(python_version.minor, name.as_str());
let is_non_shadowable = is_non_shadowable(python_version.minor, name.as_str());
let name = RelaxedModuleName::new(name);
let stub_name = name.to_stub_package();
@@ -553,7 +562,8 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<ResolvedName> {
// the module name always resolves to the stdlib module,
// even if there's a module of the same name in the first-party root
// (which would normally result in the stdlib module being overridden).
if is_builtin_module && !search_path.is_standard_library() {
// TODO: offer a diagnostic if there is a first-party module of the same name
if is_non_shadowable && !search_path.is_standard_library() {
continue;
}

View File

@@ -9,8 +9,8 @@ use crate::semantic_index::{
};
use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map};
use crate::types::{
KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType,
binding_type, declaration_type, todo_type,
DynamicType, KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder,
UnionType, binding_type, declaration_type, todo_type,
};
use crate::{Db, FxOrderSet, KnownModule, Program, resolve_module};
@@ -524,6 +524,21 @@ impl<'db> PlaceAndQualifiers<'db> {
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
}
/// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type.
pub(crate) fn is_bare_final(&self) -> Option<TypeQualifiers> {
match self {
PlaceAndQualifiers { place, qualifiers }
if (qualifiers.contains(TypeQualifiers::FINAL)
&& place
.ignore_possibly_unbound()
.is_some_and(|ty| ty.is_unknown())) =>
{
Some(*qualifiers)
}
_ => None,
}
}
#[must_use]
pub(crate) fn map_type(
self,
@@ -645,6 +660,42 @@ fn place_by_id<'db>(
ConsideredDefinitions::AllReachable => use_def.all_reachable_bindings(place_id),
};
// If a symbol is undeclared, but qualified with `typing.Final`, we use the right-hand side
// inferred type, without unioning with `Unknown`, because it can not be modified.
if let Some(qualifiers) = declared
.as_ref()
.ok()
.and_then(PlaceAndQualifiers::is_bare_final)
{
let bindings = all_considered_bindings();
return place_from_bindings_impl(db, bindings, requires_explicit_reexport)
.with_qualifiers(qualifiers);
}
// Handle bare `ClassVar` annotations by falling back to the union of `Unknown` and the
// inferred type.
match declared {
Ok(PlaceAndQualifiers {
place: Place::Type(Type::Dynamic(DynamicType::Unknown), declaredness),
qualifiers,
}) if qualifiers.contains(TypeQualifiers::CLASS_VAR) => {
let bindings = all_considered_bindings();
match place_from_bindings_impl(db, bindings, requires_explicit_reexport) {
Place::Type(inferred, boundness) => {
return Place::Type(
UnionType::from_elements(db, [Type::unknown(), inferred]),
boundness,
)
.with_qualifiers(qualifiers);
}
Place::Unbound => {
return Place::Type(Type::unknown(), declaredness).with_qualifiers(qualifiers);
}
}
}
_ => {}
}
match declared {
// Place is declared, trust the declared type
Ok(

View File

@@ -124,6 +124,30 @@ pub(crate) fn attribute_assignments<'db, 's>(
})
}
/// Returns all attribute declarations (and their method scope IDs) with a symbol name matching
/// the one given for a specific class body scope.
///
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
/// introduces a direct dependency on that file's AST.
pub(crate) fn attribute_declarations<'db, 's>(
db: &'db dyn Db,
class_body_scope: ScopeId<'db>,
name: &'s str,
) -> impl Iterator<Item = (DeclarationsIterator<'db, 'db>, FileScopeId)> + use<'s, 'db> {
let file = class_body_scope.file(db);
let index = semantic_index(db, file);
attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| {
let place_table = index.place_table(function_scope_id);
let place = place_table.place_id_by_instance_attribute_name(name)?;
let use_def = &index.use_def_maps[function_scope_id];
Some((
use_def.inner.all_reachable_declarations(place),
function_scope_id,
))
})
}
/// Returns all attribute assignments as scope IDs for a specific class body scope.
///
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it

View File

@@ -35,8 +35,8 @@ use crate::semantic_index::place::{
PlaceExprWithFlags, PlaceTableBuilder, Scope, ScopeId, ScopeKind, ScopedPlaceId,
};
use crate::semantic_index::predicate::{
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, PredicateOrLiteral,
ScopedPredicateId, StarImportPlaceholderPredicate,
CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode,
PredicateOrLiteral, ScopedPredicateId, StarImportPlaceholderPredicate,
};
use crate::semantic_index::re_exports::exported_names;
use crate::semantic_index::reachability_constraints::{
@@ -1901,11 +1901,45 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
value,
range: _,
node_index: _,
}) if self.in_module_scope() => {
if let Some(expr) = dunder_all_extend_argument(value) {
self.add_standalone_expression(expr);
}) => {
if self.in_module_scope() {
if let Some(expr) = dunder_all_extend_argument(value) {
self.add_standalone_expression(expr);
}
}
self.visit_expr(value);
// If the statement is a call, it could possibly be a call to a function
// marked with `NoReturn` (for example, `sys.exit()`). In this case, we use a special
// kind of constraint to mark the following code as unreachable.
//
// Ideally, these constraints should be added for every call expression, even those in
// sub-expressions and in the module-level scope. But doing so makes the number of
// such constraints so high that it significantly degrades performance. We thus cut
// scope here and add these constraints only at statement level function calls,
// like `sys.exit()`, and not within sub-expression like `3 + sys.exit()` etc.
//
// We also only add these inside function scopes, since considering module-level
// constraints can affect the the type of imported symbols, leading to a lot more
// work in third-party code.
if let ast::Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
if !self.source_type.is_stub() && self.in_function_scope() {
let callable = self.add_standalone_expression(func);
let call_expr = self.add_standalone_expression(value.as_ref());
let predicate = Predicate {
node: PredicateNode::ReturnsNever(CallableAndCallExpr {
callable,
call_expr,
}),
is_positive: false,
};
self.record_reachability_constraint(PredicateOrLiteral::Predicate(
predicate,
));
}
}
}
_ => {
walk_stmt(self, stmt);

View File

@@ -102,9 +102,16 @@ impl PredicateOrLiteral<'_> {
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) struct CallableAndCallExpr<'db> {
pub(crate) callable: Expression<'db>,
pub(crate) call_expr: Expression<'db>,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) enum PredicateNode<'db> {
Expression(Expression<'db>),
ReturnsNever(CallableAndCallExpr<'db>),
Pattern(PatternPredicate<'db>),
StarImportPlaceholder(StarImportPlaceholderPredicate<'db>),
}

View File

@@ -204,7 +204,8 @@ use crate::place::{RequiresExplicitReExport, imported_symbol};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::place_table;
use crate::semantic_index::predicate::{
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId,
CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode,
Predicates, ScopedPredicateId,
};
use crate::types::{Truthiness, Type, infer_expression_type};
@@ -684,6 +685,53 @@ impl ReachabilityConstraints {
let ty = infer_expression_type(db, test_expr);
ty.bool(db).negate_if(!predicate.is_positive)
}
PredicateNode::ReturnsNever(CallableAndCallExpr {
callable,
call_expr,
}) => {
// We first infer just the type of the callable. In the most likely case that the
// function is not marked with `NoReturn`, or that it always returns `NoReturn`,
// doing so allows us to avoid the more expensive work of inferring the entire call
// expression (which could involve inferring argument types to possibly run the overload
// selection algorithm).
// Avoiding this on the happy-path is important because these constraints can be
// very large in number, since we add them on all statement level function calls.
let ty = infer_expression_type(db, callable);
let overloads_iterator =
if let Some(Type::Callable(callable)) = ty.into_callable(db) {
callable.signatures(db).overloads.iter()
} else {
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);
};
let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator
.fold((true, true), |(none, all), overload| {
let overload_returns_never =
overload.return_ty.is_some_and(|return_type| {
return_type.is_equivalent_to(db, Type::Never)
});
(
none && !overload_returns_never,
all && overload_returns_never,
)
});
if no_overloads_return_never {
Truthiness::AlwaysFalse
} else if all_overloads_return_never {
Truthiness::AlwaysTrue
} else {
let call_expr_ty = infer_expression_type(db, call_expr);
if call_expr_ty.is_equivalent_to(db, Type::Never) {
Truthiness::AlwaysTrue
} else {
Truthiness::AlwaysFalse
}
}
.negate_if(!predicate.is_positive)
}
PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner),
PredicateNode::StarImportPlaceholder(star_import) => {
let place_table = place_table(db, star_import.scope(db));

View File

@@ -192,6 +192,17 @@
//! for that place that we need for that use or definition. When we reach the end of the scope, it
//! records the state for each place as the public definitions of that place.
//!
//! ```python
//! x = 1
//! x = 2
//! y = x
//! if flag:
//! x = 3
//! else:
//! x = 4
//! z = x
//! ```
//!
//! Let's walk through the above example. Initially we do not have any record of `x`. When we add
//! the new place (before we process the first binding), we create a new undefined `PlaceState`
//! which has a single live binding (the "unbound" definition) and a single live declaration (the
@@ -498,6 +509,18 @@ impl<'db> UseDefMap<'db> {
.is_always_false()
}
pub(crate) fn is_declaration_reachable(
&self,
db: &dyn crate::Db,
declaration: &DeclarationWithConstraint<'db>,
) -> Truthiness {
self.reachability_constraints.evaluate(
db,
&self.predicates,
declaration.reachability_constraint,
)
}
pub(crate) fn is_binding_reachable(
&self,
db: &dyn crate::Db,

View File

@@ -1,14 +1,12 @@
use ruff_db::files::{File, FilePath};
use ruff_db::source::line_index;
use ruff_python_ast::{self as ast, ExprContext};
use ruff_python_ast as ast;
use ruff_python_ast::{Expr, ExprRef, name::Name};
use ruff_source_file::LineIndex;
use crate::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::{KnownModule, Module, resolve_module};
use crate::semantic_index::ast_ids::HasScopedUseId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::place::FileScopeId;
use crate::semantic_index::semantic_index;
use crate::types::ide_support::all_declarations_and_bindings;
@@ -177,41 +175,6 @@ pub struct Completion {
pub builtin: bool,
}
pub trait HasDefinition {
/// Returns the definitions of `self`.
///
/// ## Panics
/// May panic if `self` is from another file than `model`.
fn definitions<'db>(&self, model: &SemanticModel<'db>) -> Option<Vec<Definition<'db>>>;
}
impl HasDefinition for ast::ExprRef<'_> {
fn definitions<'db>(&self, model: &SemanticModel<'db>) -> Option<Vec<Definition<'db>>> {
match self {
ExprRef::Name(name) => match name.ctx {
ExprContext::Load => {
let index = semantic_index(model.db, model.file);
let file_scope = index.expression_scope_id(*self);
let scope = file_scope.to_scope_id(model.db, model.file);
let use_def = index.use_def_map(file_scope);
let use_id = self.scoped_use_id(model.db, scope);
Some(
use_def
.bindings_at_use(use_id)
.filter_map(|binding| binding.binding.definition())
.collect(),
)
}
ExprContext::Store => None,
ExprContext::Del => None,
ExprContext::Invalid => None,
},
_ => None,
}
}
}
pub trait HasType {
/// Returns the inferred type of `self`.
///

View File

@@ -19,7 +19,7 @@ use ruff_text_size::{Ranged, TextRange};
use type_ordering::union_or_intersection_elements_ordering;
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub(crate) use self::cyclic::TypeTransformer;
pub(crate) use self::cyclic::{PairVisitor, TypeTransformer};
pub use self::diagnostic::TypeCheckDiagnostics;
pub(crate) use self::diagnostic::register_lints;
pub(crate) use self::infer::{
@@ -46,7 +46,7 @@ use crate::types::generics::{
GenericContext, PartialSpecialization, Specialization, walk_generic_context,
walk_partial_specialization, walk_specialization,
};
pub use crate::types::ide_support::all_members;
pub use crate::types::ide_support::{all_members, definition_kind_for_name};
use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
@@ -1095,6 +1095,74 @@ impl<'db> Type<'db> {
}
}
pub(crate) fn into_callable(self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Type::Callable(_) => Some(self),
Type::Dynamic(_) => Some(CallableType::single(db, Signature::dynamic(self))),
Type::FunctionLiteral(function_literal) => {
Some(Type::Callable(function_literal.into_callable_type(db)))
}
Type::BoundMethod(bound_method) => Some(bound_method.into_callable_type(db)),
Type::NominalInstance(_) | Type::ProtocolInstance(_) => {
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place;
if let Place::Type(ty, Boundness::Bound) = call_symbol {
ty.into_callable(db)
} else {
None
}
}
Type::ClassLiteral(class_literal) => {
Some(ClassType::NonGeneric(class_literal).into_callable(db))
}
Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)),
// TODO: This is unsound so in future we can consider an opt-in option to disable it.
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Class(class) => Some(class.into_callable(db)),
SubclassOfInner::Dynamic(dynamic) => Some(CallableType::single(
db,
Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))),
)),
},
Type::Union(union) => union.try_map(db, |element| element.into_callable(db)),
Type::Never
| Type::DataclassTransformer(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::Tuple(_)
| Type::TypeIs(_) => None,
// TODO
Type::MethodWrapper(_)
| Type::WrapperDescriptor(_)
| Type::DataclassDecorator(_)
| Type::ModuleLiteral(_)
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::PropertyInstance(_)
| Type::Intersection(_)
| Type::TypeVar(_)
| Type::BoundSuper(_) => None,
}
}
/// Return true if this type is a [subtype of] type `target`.
///
/// For fully static types, this means that the set of objects represented by `self` is a
@@ -1305,24 +1373,14 @@ impl<'db> Type<'db> {
| Type::ModuleLiteral(_),
) => false,
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place;
// If the type of __call__ is a subtype of a callable type, this instance is.
// Don't add other special cases here; our subtyping of a callable type
// shouldn't get out of sync with the calls we will actually allow.
if let Place::Type(t, Boundness::Bound) = call_symbol {
t.has_relation_to(db, target, relation)
} else {
false
}
(Type::Callable(self_callable), Type::Callable(other_callable)) => {
self_callable.has_relation_to(db, other_callable, relation)
}
(_, Type::Callable(_)) => self
.into_callable(db)
.is_some_and(|callable| callable.has_relation_to(db, target, relation)),
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.has_relation_to(db, right, relation)
}
@@ -1349,16 +1407,6 @@ impl<'db> Type<'db> {
) => (self.literal_fallback_instance(db))
.is_some_and(|instance| instance.has_relation_to(db, target, relation)),
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
self_function_literal
.into_callable_type(db)
.has_relation_to(db, target, relation)
}
(Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method
.into_callable_type(db)
.has_relation_to(db, target, relation),
// A `FunctionLiteral` type is a single-valued type like the other literals handled above,
// so it also, for now, just delegates to its instance fallback.
(Type::FunctionLiteral(_), _) => KnownClass::FunctionType
@@ -1376,10 +1424,6 @@ impl<'db> Type<'db> {
.to_instance(db)
.has_relation_to(db, target, relation),
(Type::Callable(self_callable), Type::Callable(other_callable)) => {
self_callable.has_relation_to(db, other_callable, relation)
}
(Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => {
// TODO: Implement subtyping using an equivalent `Callable` type.
false
@@ -1456,26 +1500,6 @@ impl<'db> Type<'db> {
self_subclass_ty.has_relation_to(db, target_subclass_ty, relation)
}
(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
ClassType::NonGeneric(class_literal)
.into_callable(db)
.has_relation_to(db, target, relation)
}
(Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias)
.into_callable(db)
.has_relation_to(db, target, relation),
// TODO: This is unsound so in future we can consider an opt-in option to disable it.
(Type::SubclassOf(subclass_of_ty), Type::Callable(_))
if subclass_of_ty.subclass_of().into_class().is_some() =>
{
let class = subclass_of_ty.subclass_of().into_class().unwrap();
class
.into_callable(db)
.has_relation_to(db, target, relation)
}
// `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`.
// `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object
// is an instance of its metaclass `abc.ABCMeta`.
@@ -1613,17 +1637,30 @@ impl<'db> Type<'db> {
/// Note: This function aims to have no false positives, but might return
/// wrong `false` answers in some cases.
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
let mut visitor = PairVisitor::new(false);
self.is_disjoint_from_impl(db, other, &mut visitor)
}
pub(crate) fn is_disjoint_from_impl(
self,
db: &'db dyn Db,
other: Type<'db>,
visitor: &mut PairVisitor<'db>,
) -> bool {
fn any_protocol_members_absent_or_disjoint<'db>(
db: &'db dyn Db,
protocol: ProtocolInstanceType<'db>,
other: Type<'db>,
visitor: &mut PairVisitor<'db>,
) -> bool {
protocol.interface(db).members(db).any(|member| {
other
.member(db, member.name())
.place
.ignore_possibly_unbound()
.is_none_or(|attribute_type| member.has_disjoint_type_from(db, attribute_type))
.is_none_or(|attribute_type| {
member.has_disjoint_type_from(db, attribute_type, visitor)
})
})
}
@@ -1657,19 +1694,19 @@ impl<'db> Type<'db> {
match typevar.bound_or_constraints(db) {
None => false,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.is_disjoint_from(db, other)
bound.is_disjoint_from_impl(db, other, visitor)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_disjoint_from(db, other)),
.all(|constraint| constraint.is_disjoint_from_impl(db, other, visitor)),
}
}
(Type::Union(union), other) | (other, Type::Union(union)) => union
.elements(db)
.iter()
.all(|e| e.is_disjoint_from(db, other)),
.all(|e| e.is_disjoint_from_impl(db, other, visitor)),
// If we have two intersections, we test the positive elements of each one against the other intersection
// Negative elements need a positive element on the other side in order to be disjoint.
@@ -1678,11 +1715,11 @@ impl<'db> Type<'db> {
self_intersection
.positive(db)
.iter()
.any(|p| p.is_disjoint_from(db, other))
.any(|p| p.is_disjoint_from_impl(db, other, visitor))
|| other_intersection
.positive(db)
.iter()
.any(|p: &Type<'_>| p.is_disjoint_from(db, self))
.any(|p: &Type<'_>| p.is_disjoint_from_impl(db, self, visitor))
}
(Type::Intersection(intersection), other)
@@ -1690,7 +1727,7 @@ impl<'db> Type<'db> {
intersection
.positive(db)
.iter()
.any(|p| p.is_disjoint_from(db, other))
.any(|p| p.is_disjoint_from_impl(db, other, visitor))
// A & B & Not[C] is disjoint from C
|| intersection
.negative(db)
@@ -1804,17 +1841,17 @@ impl<'db> Type<'db> {
}
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.is_disjoint_from(db, right)
left.is_disjoint_from_impl(db, right, visitor)
}
(Type::ProtocolInstance(protocol), Type::SpecialForm(special_form))
| (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => {
any_protocol_members_absent_or_disjoint(db, protocol, special_form.instance_fallback(db))
any_protocol_members_absent_or_disjoint(db, protocol, special_form.instance_fallback(db), visitor)
}
(Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance))
| (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => {
any_protocol_members_absent_or_disjoint(db, protocol, known_instance.instance_fallback(db))
any_protocol_members_absent_or_disjoint(db, protocol, known_instance.instance_fallback(db), visitor)
}
// The absence of a protocol member on one of these types guarantees
@@ -1867,7 +1904,7 @@ impl<'db> Type<'db> {
| Type::ModuleLiteral(..)
| Type::GenericAlias(..)
| Type::IntLiteral(..)),
) => any_protocol_members_absent_or_disjoint(db, protocol, ty),
) => any_protocol_members_absent_or_disjoint(db, protocol, ty, visitor),
// This is the same as the branch above --
// once guard patterns are stabilised, it could be unified with that branch
@@ -1876,7 +1913,7 @@ impl<'db> Type<'db> {
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol))
if n.class.is_final(db) =>
{
any_protocol_members_absent_or_disjoint(db, protocol, nominal)
any_protocol_members_absent_or_disjoint(db, protocol, nominal, visitor)
}
(Type::ProtocolInstance(protocol), other)
@@ -1884,7 +1921,7 @@ impl<'db> Type<'db> {
protocol.interface(db).members(db).any(|member| {
matches!(
other.member(db, member.name()).place,
Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type)
Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type, visitor)
)
})
}
@@ -1907,18 +1944,18 @@ impl<'db> Type<'db> {
}
}
(Type::SubclassOf(left), Type::SubclassOf(right)) => left.is_disjoint_from(db, right),
(Type::SubclassOf(left), Type::SubclassOf(right)) => left.is_disjoint_from_impl(db, right),
// for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`,
// so although the type is dynamic we can still determine disjointedness in some situations
(Type::SubclassOf(subclass_of_ty), other)
| (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Dynamic(_) => {
KnownClass::Type.to_instance(db).is_disjoint_from(db, other)
KnownClass::Type.to_instance(db).is_disjoint_from_impl(db, other, visitor)
}
SubclassOfInner::Class(class) => class
.metaclass_instance_type(db)
.is_disjoint_from(db, other),
.is_disjoint_from_impl(db, other, visitor),
},
(Type::SpecialForm(special_form), Type::NominalInstance(instance))
@@ -2003,18 +2040,18 @@ impl<'db> Type<'db> {
(Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType
.to_instance(db)
.is_disjoint_from(db, other),
.is_disjoint_from_impl(db, other, visitor),
(Type::MethodWrapper(_), other) | (other, Type::MethodWrapper(_)) => {
KnownClass::MethodWrapperType
.to_instance(db)
.is_disjoint_from(db, other)
.is_disjoint_from_impl(db, other, visitor)
}
(Type::WrapperDescriptor(_), other) | (other, Type::WrapperDescriptor(_)) => {
KnownClass::WrapperDescriptorType
.to_instance(db)
.is_disjoint_from(db, other)
.is_disjoint_from_impl(db, other, visitor)
}
(Type::Callable(_) | Type::FunctionLiteral(_), Type::Callable(_))
@@ -2076,15 +2113,15 @@ impl<'db> Type<'db> {
(Type::ModuleLiteral(..), other @ Type::NominalInstance(..))
| (other @ Type::NominalInstance(..), Type::ModuleLiteral(..)) => {
// Modules *can* actually be instances of `ModuleType` subclasses
other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db))
other.is_disjoint_from_impl(db, KnownClass::ModuleType.to_instance(db), visitor)
}
(Type::NominalInstance(left), Type::NominalInstance(right)) => {
left.is_disjoint_from(db, right)
left.is_disjoint_from_impl(db, right)
}
(Type::Tuple(tuple), Type::Tuple(other_tuple)) => {
tuple.is_disjoint_from(db, other_tuple)
tuple.is_disjoint_from_impl(db, other_tuple, visitor)
}
(Type::Tuple(tuple), Type::NominalInstance(instance))
@@ -2097,13 +2134,13 @@ impl<'db> Type<'db> {
(Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => {
KnownClass::Property
.to_instance(db)
.is_disjoint_from(db, other)
.is_disjoint_from_impl(db, other, visitor)
}
(Type::BoundSuper(_), Type::BoundSuper(_)) => !self.is_equivalent_to(db, other),
(Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => KnownClass::Super
.to_instance(db)
.is_disjoint_from(db, other),
.is_disjoint_from_impl(db, other, visitor),
}
}
@@ -3032,10 +3069,6 @@ impl<'db> Type<'db> {
Type::ModuleLiteral(module) => module.static_member(db, name_str).into(),
Type::AlwaysFalsy | Type::AlwaysTruthy => {
self.class_member_with_policy(db, name, policy)
}
_ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
db,
name_str,
@@ -3057,6 +3090,8 @@ impl<'db> Type<'db> {
| Type::KnownInstance(..)
| Type::PropertyInstance(..)
| Type::FunctionLiteral(..)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::TypeIs(..) => {
let fallback = self.instance_member(db, name_str);
@@ -3496,7 +3531,6 @@ impl<'db> Type<'db> {
// For `builtins.property.__get__`, we use the same signature. The return types are not
// specified yet, they will be dynamically added in `Bindings::evaluate_known_cases`.
let not_none = Type::none(db).negate(db);
CallableBinding::from_overloads(
self,
[
@@ -3512,7 +3546,7 @@ impl<'db> Type<'db> {
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("instance")))
.with_annotated_type(not_none),
.with_annotated_type(Type::object(db)),
Parameter::positional_only(Some(Name::new_static("owner")))
.with_annotated_type(UnionType::from_elements(
db,
@@ -3538,7 +3572,6 @@ impl<'db> Type<'db> {
// TODO: Consider merging this signature with the one in the previous match clause,
// since the previous one is just this signature with the `self` parameters
// removed.
let not_none = Type::none(db).negate(db);
let descriptor = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => {
KnownClass::FunctionType.to_instance(db)
@@ -3569,7 +3602,7 @@ impl<'db> Type<'db> {
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(descriptor),
Parameter::positional_only(Some(Name::new_static("instance")))
.with_annotated_type(not_none),
.with_annotated_type(Type::object(db)),
Parameter::positional_only(Some(Name::new_static("owner")))
.with_annotated_type(UnionType::from_elements(
db,
@@ -7189,7 +7222,7 @@ impl<'db> BoundMethodType<'db> {
#[derive(PartialOrd, Ord)]
pub struct CallableType<'db> {
#[returns(ref)]
signatures: CallableSignature<'db>,
pub(crate) signatures: CallableSignature<'db>,
/// We use `CallableType` to represent function-like objects, like the synthesized methods
/// of dataclasses or NamedTuples. These callables act like real functions when accessed
@@ -7303,6 +7336,10 @@ impl<'db> CallableType<'db> {
///
/// See [`Type::is_equivalent_to`] for more details.
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
if self == other {
return true;
}
self.is_function_like(db) == other.is_function_like(db)
&& self
.signatures(db)

View File

@@ -730,38 +730,6 @@ impl<'db> Bindings<'db> {
}
}
Some(KnownFunction::Overload) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `typing.overload` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}
}
Some(KnownFunction::Override) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `typing.override` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}
}
Some(KnownFunction::AbstractMethod) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `abc.abstractmethod` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}
}
Some(KnownFunction::Final) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `typing.final` is an identity function.
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(*ty);
}
}
Some(KnownFunction::GetattrStatic) => {
let [Some(instance_ty), Some(attr_name), default] =
overload.parameter_types()

View File

@@ -11,7 +11,7 @@ use super::{
};
use crate::semantic_index::definition::{Definition, DefinitionState};
use crate::semantic_index::place::NodeWithScopeKind;
use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex};
use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex, attribute_declarations};
use crate::types::context::InferContext;
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
use crate::types::function::{DataclassTransformerParams, KnownFunction};
@@ -1763,10 +1763,7 @@ impl<'db> ClassLiteral<'db> {
let class_map = use_def_map(db, class_body_scope);
let class_table = place_table(db, class_body_scope);
for (attribute_assignments, method_scope_id) in
attribute_assignments(db, class_body_scope, name)
{
let method_scope = method_scope_id.to_scope_id(db, file);
let is_valid_scope = |method_scope: ScopeId<'db>| {
if let Some(method_def) = method_scope.node(db).as_function(&module) {
let method_name = method_def.name.as_str();
if let Place::Type(Type::FunctionLiteral(method_type), _) =
@@ -1774,10 +1771,53 @@ impl<'db> ClassLiteral<'db> {
{
let method_decorator = MethodDecorator::try_from_fn_type(db, method_type);
if method_decorator != Ok(target_method_decorator) {
continue;
return false;
}
}
}
true
};
// First check declarations
for (attribute_declarations, method_scope_id) in
attribute_declarations(db, class_body_scope, name)
{
let method_scope = method_scope_id.to_scope_id(db, file);
if !is_valid_scope(method_scope) {
continue;
}
for attribute_declaration in attribute_declarations {
let DefinitionState::Defined(decl) = attribute_declaration.declaration else {
continue;
};
let DefinitionKind::AnnotatedAssignment(annotated) = decl.kind(db) else {
continue;
};
if use_def_map(db, method_scope)
.is_declaration_reachable(db, &attribute_declaration)
.is_always_false()
{
continue;
}
let annotation_ty =
infer_expression_type(db, index.expression(annotated.annotation(&module)));
return Place::bound(annotation_ty);
}
}
for (attribute_assignments, method_scope_id) in
attribute_assignments(db, class_body_scope, name)
{
let method_scope = method_scope_id.to_scope_id(db, file);
if !is_valid_scope(method_scope) {
continue;
}
let method_map = use_def_map(db, method_scope);
// The attribute assignment inherits the reachability of the method which contains it
@@ -2015,6 +2055,7 @@ impl<'db> ClassLiteral<'db> {
let declarations = use_def.end_of_scope_declarations(place_id);
let declared_and_qualifiers = place_from_declarations(db, declarations);
match declared_and_qualifiers {
Ok(PlaceAndQualifiers {
place: mut declared @ Place::Type(declared_ty, declaredness),

View File

@@ -1,25 +1,61 @@
use rustc_hash::FxHashMap;
use crate::FxIndexSet;
use crate::types::Type;
use std::cmp::Eq;
use std::hash::Hash;
#[derive(Debug, Default)]
pub(crate) struct TypeTransformer<'db> {
seen: FxIndexSet<Type<'db>>,
pub(crate) type TypeTransformer<'db> = CycleDetector<Type<'db>, Type<'db>>;
impl Default for TypeTransformer<'_> {
fn default() -> Self {
// TODO: proper recursive type handling
// This must be Any, not e.g. a todo type, because Any is the normalized form of the
// dynamic type (that is, todo types are normalized to Any).
CycleDetector::new(Type::any())
}
}
impl<'db> TypeTransformer<'db> {
pub(crate) fn visit(
&mut self,
ty: Type<'db>,
func: impl FnOnce(&mut Self) -> Type<'db>,
) -> Type<'db> {
if !self.seen.insert(ty) {
// TODO: proper recursive type handling
pub(crate) type PairVisitor<'db> = CycleDetector<(Type<'db>, Type<'db>), bool>;
// This must be Any, not e.g. a todo type, because Any is the normalized form of the
// dynamic type (that is, todo types are normalized to Any).
return Type::any();
#[derive(Debug)]
pub(crate) struct CycleDetector<T, R> {
/// If the type we're visiting is present in `seen`,
/// it indicates that we've hit a cycle (due to a recursive type);
/// we need to immediately short circuit the whole operation and return the fallback value.
/// That's why we pop items off the end of `seen` after we've visited them.
seen: FxIndexSet<T>,
/// Unlike `seen`, this field is a pure performance optimisation (and an essential one).
/// If the type we're trying to normalize is present in `cache`, it doesn't necessarily mean we've hit a cycle:
/// it just means that we've already visited this inner type as part of a bigger call chain we're currently in.
/// Since this cache is just a performance optimisation, it doesn't make sense to pop items off the end of the
/// cache after they've been visited (it would sort-of defeat the point of a cache if we did!)
cache: FxHashMap<T, R>,
fallback: R,
}
impl<T: Hash + Eq + Copy, R: Copy> CycleDetector<T, R> {
pub(crate) fn new(fallback: R) -> Self {
CycleDetector {
seen: FxIndexSet::default(),
cache: FxHashMap::default(),
fallback,
}
}
pub(crate) fn visit(&mut self, item: T, func: impl FnOnce(&mut Self) -> R) -> R {
if !self.seen.insert(item) {
return self.fallback;
}
if let Some(ty) = self.cache.get(&item) {
self.seen.pop();
return *ty;
}
let ret = func(self);
self.cache.insert(item, ret);
self.seen.pop();
ret
}

View File

@@ -2426,7 +2426,7 @@ fn report_invalid_base<'ctx, 'db>(
/// This function receives an unresolved `from foo import bar` import,
/// where `foo` can be resolved to a module but that module does not
/// have a `bar` member or submdoule.
/// have a `bar` member or submodule.
///
/// If the `foo` module originates from the standard library and `foo.bar`
/// *does* exist as a submodule in the standard library on *other* Python

View File

@@ -767,8 +767,8 @@ impl<'db> FunctionType<'db> {
}
/// Convert the `FunctionType` into a [`Type::Callable`].
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> {
Type::Callable(CallableType::new(db, self.signature(db), false))
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
CallableType::new(db, self.signature(db), false)
}
/// Convert the `FunctionType` into a [`Type::BoundMethod`].

View File

@@ -1,10 +1,13 @@
use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
use crate::semantic_index::definition::DefinitionKind;
use crate::semantic_index::place::ScopeId;
use crate::semantic_index::{
attribute_scopes, global_scope, imported_modules, place_table, semantic_index, use_def_map,
};
use crate::types::{ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type};
use crate::{Db, NameKind};
use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use rustc_hash::FxHashSet;
@@ -241,3 +244,37 @@ impl AllMembers {
pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Name> {
AllMembers::of(db, ty).members
}
/// Get the primary definition kind for a name expression within a specific file.
/// Returns the first definition kind that is reachable for this name in its scope.
/// This is useful for IDE features like semantic tokens.
pub fn definition_kind_for_name<'db>(
db: &'db dyn Db,
file: File,
name: &ast::ExprName,
) -> Option<DefinitionKind<'db>> {
let index = semantic_index(db, file);
let name_str = name.id.as_str();
// Get the scope for this name expression
let file_scope = index.try_expression_scope_id(&ast::Expr::Name(name.clone()))?;
// Get the place table for this scope
let place_table = index.place_table(file_scope);
// Look up the place by name
let place_id = place_table.place_id_by_name(name_str)?;
// Get the use-def map and look up definitions for this place
let use_def_map = index.use_def_map(file_scope);
let declarations = use_def_map.all_reachable_declarations(place_id);
// Find the first valid definition and return its kind
for declaration in declarations {
if let Some(def) = declaration.declaration.definition() {
return Some(def.kind(db).clone());
}
}
None
}

View File

@@ -2122,7 +2122,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
node_index: _,
value,
}) => {
self.infer_expression(value);
// If this is a call expression, we would have added a `ReturnsNever` constraint,
// meaning this will be a standalone expression.
self.infer_maybe_standalone_expression(value);
}
ast::Stmt::If(if_statement) => self.infer_if_statement(if_statement),
ast::Stmt::Try(try_statement) => self.infer_try_statement(try_statement),
@@ -3493,7 +3495,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
{
builder.into_diagnostic(format_args!(
"Can not assign object of `{}` to attribute \
"Can not assign object of type `{}` to attribute \
`{attribute}` on type `{}` with \
custom `__setattr__` method.",
value_ty.display(db),
@@ -5263,7 +5265,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.
let call_arguments = Self::parse_arguments(arguments);
let callable_type = self.infer_expression(func);
let callable_type = self.infer_maybe_standalone_expression(func);
if let Type::FunctionLiteral(function) = callable_type {
// Make sure that the `function.definition` is only called when the function is defined

View File

@@ -5,6 +5,7 @@ use std::marker::PhantomData;
use super::protocol_class::ProtocolInterface;
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
use crate::place::PlaceAndQualifiers;
use crate::types::cyclic::PairVisitor;
use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::TupleType;
use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance};
@@ -118,7 +119,7 @@ impl<'db> NominalInstanceType<'db> {
self.class.is_equivalent_to(db, other.class)
}
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
pub(super) fn is_disjoint_from_impl(self, db: &'db dyn Db, other: Self) -> bool {
!self.class.could_coexist_in_mro_with(db, other.class)
}
@@ -269,7 +270,14 @@ impl<'db> ProtocolInstanceType<'db> {
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.normalized(db) == other.normalized(db)
if self == other {
return true;
}
let self_normalized = self.normalized(db);
if self_normalized == Type::ProtocolInstance(other) {
return true;
}
self_normalized == other.normalized(db)
}
/// Return `true` if this protocol type is disjoint from the protocol `other`.
@@ -277,7 +285,12 @@ impl<'db> ProtocolInstanceType<'db> {
/// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y`
/// have a member with the same name but disjoint types
#[expect(clippy::unused_self)]
pub(super) fn is_disjoint_from(self, _db: &'db dyn Db, _other: Self) -> bool {
pub(super) fn is_disjoint_from_impl(
self,
_db: &'db dyn Db,
_other: Self,
_visitor: &mut PairVisitor<'db>,
) -> bool {
false
}

View File

@@ -3,7 +3,7 @@ use crate::semantic_index::expression::Expression;
use crate::semantic_index::place::{PlaceExpr, PlaceTable, ScopeId, ScopedPlaceId};
use crate::semantic_index::place_table;
use crate::semantic_index::predicate::{
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode,
CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode,
};
use crate::types::function::KnownFunction;
use crate::types::infer::infer_same_file_expression_type;
@@ -59,6 +59,7 @@ pub(crate) fn infer_narrowing_constraint<'db>(
all_negative_narrowing_constraints_for_pattern(db, pattern)
}
}
PredicateNode::ReturnsNever(_) => return None,
PredicateNode::StarImportPlaceholder(_) => return None,
};
if let Some(constraints) = constraints {
@@ -346,6 +347,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
PredicateNode::Pattern(pattern) => {
self.evaluate_pattern_predicate(pattern, self.is_positive)
}
PredicateNode::ReturnsNever(_) => return None,
PredicateNode::StarImportPlaceholder(_) => return None,
};
if let Some(mut constraints) = constraints {
@@ -429,6 +431,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
match self.predicate {
PredicateNode::Expression(expression) => expression.scope(self.db),
PredicateNode::Pattern(pattern) => pattern.scope(self.db),
PredicateNode::ReturnsNever(CallableAndCallExpr { callable, .. }) => {
callable.scope(self.db)
}
PredicateNode::StarImportPlaceholder(definition) => definition.scope(self.db),
}
}

View File

@@ -218,6 +218,7 @@ mod flaky {
use itertools::Itertools;
use super::{intersection, union};
use crate::types::{KnownClass, Type};
// Negating `T` twice is equivalent to `T`.
type_property_test!(
@@ -311,4 +312,14 @@ mod flaky {
bottom_materialization_of_type_is_assigneble_to_type, db,
forall types t. t.bottom_materialization(db).is_assignable_to(db, t)
);
// Any type assignable to `Iterable[object]` should be considered iterable.
//
// Note that the inverse is not true, due to the fact that we recognize the old-style
// iteration protocol as well as the new-style iteration protocol: not all objects that
// we consider iterable are assignable to `Iterable[object]`.
type_property_test!(
all_type_assignable_to_iterable_are_iterable, db,
forall types t. t.is_assignable_to(db, KnownClass::Iterable.to_specialized_instance(db, [Type::object(db)])) => t.try_iterate(db).is_ok()
);
}

View File

@@ -55,6 +55,10 @@ pub(crate) enum Ty {
params: CallableParams,
returns: Option<Box<Ty>>,
},
/// `unittest.mock.Mock` is interesting because it is a nominal instance type
/// where the class has `Any` in its MRO
UnittestMockInstance,
UnittestMockLiteral,
}
#[derive(Debug, Clone, PartialEq)]
@@ -144,6 +148,13 @@ impl Ty {
Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s)
.place
.expect_type(),
Ty::UnittestMockLiteral => known_module_symbol(db, KnownModule::UnittestMock, "Mock")
.place
.expect_type(),
Ty::UnittestMockInstance => Ty::UnittestMockLiteral
.into_type(db)
.to_instance(db)
.unwrap(),
Ty::TypingLiteral => Type::SpecialForm(SpecialFormType::Literal),
Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).place.expect_type(),
Ty::KnownClassInstance(known_class) => known_class.to_instance(db),
@@ -223,11 +234,13 @@ fn arbitrary_core_type(g: &mut Gen, fully_static: bool) -> Ty {
let bool_lit = Ty::BooleanLiteral(bool::arbitrary(g));
// Update this if new non-fully-static types are added below.
let fully_static_index = 3;
let fully_static_index = 5;
let types = &[
Ty::Any,
Ty::Unknown,
Ty::SubclassOfAny,
Ty::UnittestMockLiteral,
Ty::UnittestMockInstance,
// Add fully static types below, dynamic types above.
// Update `fully_static_index` above if adding new dynamic types!
Ty::Never,

View File

@@ -11,6 +11,7 @@ use crate::{
types::{
CallableType, ClassBase, ClassLiteral, KnownFunction, PropertyInstanceType, Signature,
Type, TypeMapping, TypeQualifiers, TypeRelation, TypeTransformer, TypeVarInstance,
cyclic::PairVisitor,
signatures::{Parameter, Parameters},
},
};
@@ -259,7 +260,7 @@ impl<'db> ProtocolMemberData<'db> {
#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash)]
enum ProtocolMemberKind<'db> {
Method(Type<'db>), // TODO: use CallableType
Method(CallableType<'db>),
Property(PropertyInstanceType<'db>),
Other(Type<'db>),
}
@@ -334,7 +335,7 @@ fn walk_protocol_member<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>(
visitor: &mut V,
) {
match member.kind {
ProtocolMemberKind::Method(method) => visitor.visit_type(db, method),
ProtocolMemberKind::Method(method) => visitor.visit_callable_type(db, method),
ProtocolMemberKind::Property(property) => {
visitor.visit_property_instance_type(db, property);
}
@@ -353,17 +354,24 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
fn ty(&self) -> Type<'db> {
match &self.kind {
ProtocolMemberKind::Method(callable) => *callable,
ProtocolMemberKind::Method(callable) => Type::Callable(*callable),
ProtocolMemberKind::Property(property) => Type::PropertyInstance(*property),
ProtocolMemberKind::Other(ty) => *ty,
}
}
pub(super) fn has_disjoint_type_from(&self, db: &'db dyn Db, other: Type<'db>) -> bool {
pub(super) fn has_disjoint_type_from(
&self,
db: &'db dyn Db,
other: Type<'db>,
visitor: &mut PairVisitor<'db>,
) -> bool {
match &self.kind {
// TODO: implement disjointness for property/method members as well as attribute members
ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => false,
ProtocolMemberKind::Other(ty) => ty.is_disjoint_from(db, other),
ProtocolMemberKind::Other(ty) => {
visitor.visit((*ty, other), |v| ty.is_disjoint_from_impl(db, other, v))
}
}
}
@@ -500,13 +508,10 @@ fn cached_protocol_interface<'db>(
(Type::Callable(callable), BoundOnClass::Yes)
if callable.is_function_like(db) =>
{
ProtocolMemberKind::Method(ty)
ProtocolMemberKind::Method(callable)
}
// TODO: method members that have `FunctionLiteral` types should be upcast
// to `CallableType` so that two protocols with identical method members
// are recognized as equivalent.
(Type::FunctionLiteral(_function), BoundOnClass::Yes) => {
ProtocolMemberKind::Method(ty)
(Type::FunctionLiteral(function), BoundOnClass::Yes) => {
ProtocolMemberKind::Method(function.into_callable_type(db))
}
_ => ProtocolMemberKind::Other(ty),
};

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