Compare commits

...

42 Commits

Author SHA1 Message Date
Aria Desires
b0c31d0c59 leave some TODOs 2025-12-02 14:39:24 -05:00
Aria Desires
7ca384d0de allow looking up types of identifiers 2025-12-02 12:48:55 -05:00
Alex Waygood
05d053376b Delete my-script.py (#21751) 2025-12-02 14:48:01 +00:00
Alex Waygood
ac2552b11b [ty] Move all_members, and related types/routines, out of ide_support.rs (#21695) 2025-12-02 14:45:24 +00:00
Micha Reiser
644096ea8a [ty] Fix find-references for import aliases (#21736) 2025-12-02 14:37:50 +01:00
Aria Desires
015ab9e576 [ty] add tests for workspaces (#21741)
Here are a bunch of (variously failing and passing) mdtests that reflect
the kinds of issues people encounter when running ty over an entire
workspace without sufficient hand-holding (especially because in the IDE
it is unclear *how* to provide that hand-holding).
2025-12-02 06:43:41 -05:00
Douglas Creager
cf4196466c [ty] Stop testing the (brittle) constraint set display implementation (#21743)
The `Display` implementation for constraint sets is brittle, and
deserves a rethink. But later! It's perfectly fine for printf debugging;
we just shouldn't be writing mdtests that depend on any particular
rendering details. Most of these tests can be replaced with an
equivalence check that actually validates that the _behavior_ of two
constraint sets are identical.
2025-12-02 09:17:29 +01:00
Micha Reiser
2182c750db [ty] Use generator over list comprehension to avoid cast (#21748) 2025-12-02 08:47:47 +01:00
Charlie Marsh
72304b01eb [ty] Add a diagnostic for prohibited NamedTuple attribute overrides (#21717)
## Summary

Closes https://github.com/astral-sh/ty/issues/1684.
2025-12-01 21:46:58 -05:00
Ibraheem Ahmed
ec854c7199 [ty] Fix subtyping with type[T] and unions (#21740)
## Summary

Resolves
https://github.com/astral-sh/ruff/pull/21685#issuecomment-3591695954.
2025-12-01 18:20:13 -05:00
William Woodruff
edc6ed5077 Use npm ci --ignore-scripts everywhere (#21742) 2025-12-01 17:13:52 -05:00
Dan Parizher
f052bd644c [flake8-simplify] Fix truthiness assumption for non-iterable arguments in tuple/list/set calls (SIM222, SIM223) (#21479)
## Summary

Fixes false positives in SIM222 and SIM223 where truthiness was
incorrectly assumed for `tuple(x)`, `list(x)`, `set(x)` when `x` is not
iterable.

Fixes #21473.

## Problem

`Truthiness::from_expr` recursively called itself on arguments to
iterable initializers (`tuple`, `list`, `set`) without checking if the
argument is iterable, causing false positives for cases like `tuple(0)
or True` and `tuple("") or True`.

## Approach

Added `is_definitely_not_iterable` helper and updated
`Truthiness::from_expr` to return `Unknown` for non-iterable arguments
(numbers, booleans, None) and string literals when called with iterable
initializers, preventing incorrect truthiness assumptions.

## Test Plan

Added test cases to `SIM222.py` and `SIM223.py` for `tuple("")`,
`tuple(0)`, `tuple(1)`, `tuple(False)`, and `tuple(None)` with `or True`
and `and False` patterns.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-12-01 16:57:51 -05:00
Dan Parizher
bc44dc2afb [flake8-use-pathlib] Mark fixes unsafe for return type changes (PTH104, PTH105, PTH109, PTH115) (#21440)
## Summary

Marks fixes as unsafe when they change return types (`None` → `Path`,
`str`/`bytes` → `Path`, `str` → `Path`), except when the call is a
top-level expression.

Fixes #21431.

## Problem

Fixes for `os.rename`, `os.replace`, `os.getcwd`/`os.getcwdb`, and
`os.readlink` were marked safe despite changing return types, which can
break code that uses the return value.

## Approach

Added `is_top_level_expression_call` helper to detect when a call is a
top-level expression (return value unused). Updated
`check_os_pathlib_two_arg_calls` and `check_os_pathlib_single_arg_calls`
to mark fixes as unsafe unless the call is a top-level expression.
Updated PTH109 to use the helper for applicability determination.

## Test Plan

Updated snapshots for `preview_full_name.py`, `preview_import_as.py`,
`preview_import_from.py`, and `preview_import_from_as.py` to reflect
unsafe markers.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-12-01 15:26:55 -05:00
Andrew Gallant
52f59c5c39 [ty] Fix auto-import code action to handle pre-existing import
Previously, the code action to do auto-import on a pre-existing symbol
assumed that the auto-importer would always generate an import
statement. But sometimes an import statement already exists.

A good example of this is the following snippet:

```
import warnings

@deprecated
def myfunc(): pass
```

Specifically, `deprecated` exists in `warnings` but isn't currently
imported. A code action to fix this could feasibly do two
transformations here. One is:

```
import warnings

@warnings.deprecated
def myfunc(): pass
```

Another is:

```
from warnings import deprecated
import warnings

@deprecated
def myfunc(): pass
```

The existing auto-import infrastructure chooses the former, since it
reuses a pre-existing import statement. But this PR chooses the latter
for the case of a code action. I'm not 100% sure this is the correct
choice, but it seems to defer more strongly to what the user has typed.
That is, that they want to use it unqualified because it's what has been
typed. So we should add the necessary import statement to make that
work.

Fixes astral-sh/ty#1668
2025-12-01 14:20:47 -05:00
William Woodruff
53299cbff4 Enable PEP 740 attestations when publishing to PyPI (#21735) 2025-12-01 13:15:20 -05:00
Micha Reiser
3738ab1c46 [ty] Fix find references for type defined in stub (#21732) 2025-12-01 17:53:45 +01:00
Micha Reiser
b4f618e180 Use OIDC instead of codspeed token (#21719) 2025-12-01 17:51:34 +01:00
Andrew Gallant
a561e6659d [ty] Exclude typing_extensions from completions unless it's really available
This works by adding a third module resolution mode that lets the caller
opt into _some_ shadowing of modules that is otherwise not allowed (for
`typing` and `typing_extensions`).

Fixes astral-sh/ty#1658
2025-12-01 11:24:16 -05:00
Alex Waygood
0e651b50b7 [ty] Fix false positives for class F(Generic[*Ts]): ... (#21723) 2025-12-01 13:24:07 +00:00
David Peter
116fd7c7af [ty] Remove GenericAlias-related todo type (#21728)
## Summary

If you manage to create an `typing.GenericAlias` instance without us
knowing how that was created, then we don't know what to do with this in
a type annotation. So it's better to be explicit and show an error
instead of failing silently with a `@Todo` type.

## Test Plan

* New Markdown tests
* Zero ecosystem impact
2025-12-01 13:02:38 +00:00
David Peter
5358ddae88 [ty] Exhaustiveness checking for generic classes (#21726)
## Summary

We had tests for this already, but they used generic classes that were
bivariant in their type parameter, and so this case wasn't captured.

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

## Test Plan

Updated Markdown tests
2025-12-01 13:52:36 +01:00
Alex Waygood
3a11e714c6 [ty] Show the user where the type variable was defined in invalid-type-arguments diagnostics (#21727) 2025-12-01 12:25:49 +00:00
Alex Waygood
a2096ee2cb [ty] Emit invalid-named-tuple on namedtuple classes that have field names starting with underscores (#21697) 2025-12-01 11:36:02 +00:00
Micha Reiser
2e229aa8cb [ty] LSP Benchmarks (#21625) 2025-12-01 11:33:53 +00:00
Carl Meyer
c2773b4c6f [ty] support type[tuple[...]] (#21652)
Fixes https://github.com/astral-sh/ty/issues/1649

## Summary

We missed this when adding support for `type[]` of a specialized
generic.

## Test Plan

Added mdtests.
2025-12-01 11:49:26 +01:00
David Peter
bc6517a807 [ty] Add missing projects to good.txt (#21721)
## Summary

These projects from `mypy_primer` were missing from both `good.txt` and
`bad.txt` for some reason. I thought about writing a script that would
verify that `good.txt` + `bad.txt` = `mypy_primer.projects`, but that's
not completely trivial since there are projects like `cpython` only
appear once in `good.txt`. Given that we can hopefully soon get rid of
both of these files (and always run on all projects), it's probably not
worth the effort. We are usually notified of all `mypy_primer` changes.

## Test Plan

CI on this PR
2025-12-01 11:18:41 +01:00
Kieran Ryan
4686c36079 docs: Output file option with GitLab integration (#21706)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-12-01 10:07:25 +00:00
Shunsuke Shibayama
a6cbc138d2 [ty] remove the visitor parameter in the recursive_type_normalized_impl method (#21701) 2025-12-01 08:48:43 +01:00
renovate[bot]
846df40a6e Update Swatinem/rust-cache action to v2.8.2 (#21710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 08:03:17 +01:00
renovate[bot]
c61e885527 Update salsa digest to 59aa107 (#21708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 08:02:44 +01:00
renovate[bot]
13af584428 Update taiki-e/install-action action to v2.62.60 (#21711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 08:02:09 +01:00
renovate[bot]
984480a586 Update tokio-tracing monorepo (#21712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 08:01:14 +01:00
renovate[bot]
aef056954b Update actions/setup-python action to v6.1.0 (#21713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 08:00:05 +01:00
renovate[bot]
5265af4eee Update cargo-bins/cargo-binstall action to v1.16.2 (#21714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 07:59:44 +01:00
renovate[bot]
5b32908920 Update CodSpeedHQ/action action to v4.4.1 (#21716)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 07:58:56 +01:00
renovate[bot]
d8d1464d96 Update dependency ruff to v0.14.7 (#21709)
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.14.6` -> `==0.14.7` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.14.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.14.6/0.14.7?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.14.7`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0147)

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

Released on 2025-11-28.

##### Preview features

- \[`flake8-bandit`] Handle string literal bindings in
suspicious-url-open-usage (`S310`)
([#&#8203;21469](https://redirect.github.com/astral-sh/ruff/pull/21469))
- \[`pylint`] Fix `PLR1708` false positives on nested functions
([#&#8203;21177](https://redirect.github.com/astral-sh/ruff/pull/21177))
- \[`pylint`] Fix suppression for empty dict without tuple key
annotation (`PLE1141`)
([#&#8203;21290](https://redirect.github.com/astral-sh/ruff/pull/21290))
- \[`ruff`] Add rule `RUF066` to detect unnecessary class properties
([#&#8203;21535](https://redirect.github.com/astral-sh/ruff/pull/21535))
- \[`ruff`] Catch more dummy variable uses (`RUF052`)
([#&#8203;19799](https://redirect.github.com/astral-sh/ruff/pull/19799))

##### Bug fixes

- \[server] Set severity for non-rule diagnostics
([#&#8203;21559](https://redirect.github.com/astral-sh/ruff/pull/21559))
- \[`flake8-implicit-str-concat`] Avoid invalid fix in (`ISC003`)
([#&#8203;21517](https://redirect.github.com/astral-sh/ruff/pull/21517))
- \[`parser`] Fix panic when parsing IPython escape command expressions
([#&#8203;21480](https://redirect.github.com/astral-sh/ruff/pull/21480))

##### CLI

- Show partial fixability indicator in statistics output
([#&#8203;21513](https://redirect.github.com/astral-sh/ruff/pull/21513))

##### Contributors

- [@&#8203;mikeleppane](https://redirect.github.com/mikeleppane)
- [@&#8203;senekor](https://redirect.github.com/senekor)
- [@&#8203;ShaharNaveh](https://redirect.github.com/ShaharNaveh)
- [@&#8203;JumboBear](https://redirect.github.com/JumboBear)
- [@&#8203;prakhar1144](https://redirect.github.com/prakhar1144)
- [@&#8203;tsvikas](https://redirect.github.com/tsvikas)
- [@&#8203;danparizher](https://redirect.github.com/danparizher)
- [@&#8203;chirizxc](https://redirect.github.com/chirizxc)
- [@&#8203;AlexWaygood](https://redirect.github.com/AlexWaygood)
- [@&#8203;MichaReiser](https://redirect.github.com/MichaReiser)

</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:eyJjcmVhdGVkSW5WZXIiOiI0Mi4xOS45IiwidXBkYXRlZEluVmVyIjoiNDIuMTkuOSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 01:02:48 +00:00
Charlie Marsh
e7beb7e1f4 [ty] Forbid use of super() in NamedTuple subclasses (#21700)
## Summary

The exact behavior around what's allowed vs. disallowed was partly
detected through trial and error in the runtime.

I was a little confused by [this
comment](https://github.com/python/cpython/pull/129352) that says
"`NamedTuple` subclasses cannot be inherited from" because in practice
that doesn't appear to error at runtime.

Closes [#1683](https://github.com/astral-sh/ty/issues/1683).
2025-11-30 15:49:06 +00:00
Alex Waygood
b02e8212c9 [ty] Don't introduce invalid syntax when autofixing override-of-final-method (#21699) 2025-11-30 13:40:33 +00:00
Alex Waygood
69ace00210 [ty] Rename types::liskov to types::overrides (#21694) 2025-11-29 14:54:00 +00:00
Micha Reiser
d40590c8f9 [ty] Add code action to ignore diagnostic on the current line (#21595) 2025-11-29 15:41:54 +01:00
RasmusNygren
b2387f4eab [ty] fix typo in HasDefinition trait docstring (#21689)
## Summary
Fixes a typo in the docstring for the definition method in the
HasDefinition trait
2025-11-29 11:13:54 +00:00
Dhruv Manilawala
8795d9f0cb [ty] Split ParamSpec mdtests to separate legacy and PEP 695 tests (#21687)
## Summary

This is another small refactor for
https://github.com/astral-sh/ruff/pull/21445 that splits the single
`paramspec.md` into `generics/legacy/paramspec.md` and
`generics/pep695/paramspec.md`.

## Test Plan

Make sure that all mdtests pass.
2025-11-29 06:49:39 +00:00
156 changed files with 8773 additions and 4666 deletions

View File

@@ -7,10 +7,6 @@ serial = { max-threads = 1 }
filter = 'binary(file_watching)'
test-group = 'serial'
[[profile.default.overrides]]
filter = 'binary(e2e)'
test-group = 'serial'
[profile.ci]
# Print out output for failing tests as soon as they fail, and also at the end
# of the run (for easy scrollability).

View File

@@ -43,7 +43,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -72,7 +72,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -114,7 +114,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: arm64
@@ -170,7 +170,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.platform.arch }}
@@ -223,7 +223,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -300,7 +300,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -365,7 +365,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -431,7 +431,7 @@ jobs:
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"

View File

@@ -230,7 +230,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
@@ -252,7 +252,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ruff-linux-debug
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -261,11 +261,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-insta
- name: "Install uv"
@@ -315,7 +315,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
@@ -323,7 +323,7 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-nextest
- name: "Install uv"
@@ -350,13 +350,13 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-nextest
- name: "Install uv"
@@ -378,7 +378,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
@@ -415,7 +415,7 @@ jobs:
with:
file: "Cargo.toml"
field: "workspace.package.rust-version"
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
@@ -439,7 +439,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "fuzz -> target"
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -448,7 +448,7 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@ae04fb5e853ae6cd3ad7de4a1d554a8b646d12aa # v1.15.11
uses: cargo-bins/cargo-binstall@3fc81674af4165a753833a94cae9f91d8849049f # v1.16.2
- name: "Install cargo-fuzz"
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
@@ -467,7 +467,7 @@ jobs:
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ruff-linux-debug
save-if: false
@@ -498,7 +498,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
@@ -547,7 +547,7 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ruff-linux-debug
save-if: false
@@ -643,7 +643,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
@@ -688,7 +688,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@ae04fb5e853ae6cd3ad7de4a1d554a8b646d12aa # v1.15.11
- uses: cargo-bins/cargo-binstall@3fc81674af4165a753833a94cae9f91d8849049f # v1.16.2
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -702,7 +702,7 @@ jobs:
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
@@ -723,11 +723,11 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Prep README.md"
@@ -753,7 +753,7 @@ jobs:
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
@@ -785,7 +785,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Add SSH key"
@@ -829,7 +829,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Install Rust toolchain"
@@ -857,7 +857,7 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ruff-linux-debug
save-if: false
@@ -875,7 +875,7 @@ jobs:
repository: "astral-sh/ruff-lsp"
path: ruff-lsp
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
# installation fails on 3.13 and newer
python-version: "3.12"
@@ -908,7 +908,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
@@ -918,7 +918,7 @@ jobs:
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies"
run: npm ci
run: npm ci --ignore-scripts
working-directory: playground
- name: "Build playgrounds"
run: npm run dev:wasm
@@ -942,13 +942,16 @@ jobs:
needs.determine_changes.outputs.linter == 'true'
)
timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
@@ -957,7 +960,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-codspeed
@@ -965,11 +968,10 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
with:
mode: instrumentation
mode: simulation
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
benchmarks-instrumented-ty:
name: "benchmarks instrumented (ty)"
@@ -982,13 +984,16 @@ jobs:
needs.determine_changes.outputs.ty == 'true'
)
timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
@@ -997,7 +1002,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-codspeed
@@ -1005,11 +1010,10 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
with:
mode: instrumentation
mode: simulation
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
benchmarks-walltime:
name: "benchmarks walltime (${{ matrix.benchmarks }})"
@@ -1017,6 +1021,9 @@ jobs:
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
permissions:
contents: read # required for actions/checkout
id-token: write # required for OIDC authentication with CodSpeed
strategy:
matrix:
benchmarks:
@@ -1028,7 +1035,7 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
@@ -1037,7 +1044,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-codspeed
@@ -1045,7 +1052,7 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
env:
# enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't
# appear to provide much useful insight for our walltime benchmarks right now
@@ -1054,4 +1061,3 @@ jobs:
with:
mode: walltime
run: cargo codspeed run --bench ty_walltime "${{ matrix.benchmarks }}"
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -39,7 +39,7 @@ jobs:
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
- 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

@@ -45,7 +45,7 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
@@ -83,7 +83,7 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"

View File

@@ -28,7 +28,7 @@ jobs:
ref: ${{ inputs.ref }}
persist-credentials: true
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: 3.12
@@ -68,7 +68,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}

View File

@@ -37,7 +37,7 @@ jobs:
package-manager-cache: false
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies"
run: npm ci
run: npm ci --ignore-scripts
working-directory: playground
- name: "Run TypeScript checks"
run: npm run check

View File

@@ -18,8 +18,7 @@ jobs:
environment:
name: release
permissions:
# For PyPI's trusted publishing.
id-token: write
id-token: write # For PyPI's trusted publishing + PEP 740 attestations
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
@@ -28,5 +27,8 @@ jobs:
pattern: wheels-*
path: wheels
merge-multiple: true
- uses: astral-sh/attest-action@2c727738cea36d6c97dd85eb133ea0e0e8fe754b # v0.0.4
with:
paths: wheels/*
- name: Publish to PyPi
run: uv publish -v wheels/*

View File

@@ -41,7 +41,7 @@ jobs:
package-manager-cache: false
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies"
run: npm ci
run: npm ci --ignore-scripts
working-directory: playground
- name: "Run TypeScript checks"
run: npm run check

View File

@@ -198,7 +198,7 @@ jobs:
run: |
rm "${VENDORED_TYPESHED}/pyproject.toml"
git commit -am "Remove pyproject.toml file"
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
- name: "Install Rust toolchain"
if: ${{ success() }}
run: rustup show
@@ -207,12 +207,12 @@ jobs:
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
if: ${{ success() }}
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-nextest
- name: "Install cargo insta"
if: ${{ success() }}
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with:
tool: cargo-insta
- name: Update snapshots

View File

@@ -37,7 +37,7 @@ jobs:
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact

View File

@@ -33,7 +33,7 @@ jobs:
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact

View File

@@ -45,7 +45,7 @@ jobs:
path: typing
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"

32
Cargo.lock generated
View File

@@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1763,7 +1763,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -3570,7 +3570,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -3588,7 +3588,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0"
dependencies = [
"boxcar",
"compact_str",
@@ -3612,12 +3612,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0"
dependencies = [
"proc-macro2",
"quote",
@@ -3971,7 +3971,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -4216,9 +4216,9 @@ checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"log",
"pin-project-lite",
@@ -4228,9 +4228,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.30"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -4239,9 +4239,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.34"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [
"once_cell",
"valuable",
@@ -4283,9 +4283,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"chrono",
"matchers",
@@ -5024,7 +5024,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]

View File

@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "17bc55d699565e5a1cb1bd42363b905af2f9f3e7", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -354,6 +354,13 @@ impl Diagnostic {
Arc::make_mut(&mut self.inner).fix = Some(fix);
}
/// If `fix` is `Some`, set the fix for this diagnostic.
pub fn set_optional_fix(&mut self, fix: Option<Fix>) {
if let Some(fix) = fix {
self.set_fix(fix);
}
}
/// Remove the fix for this diagnostic.
pub fn remove_fix(&mut self) {
Arc::make_mut(&mut self.inner).fix = None;

View File

@@ -149,6 +149,10 @@ impl Fix {
&self.edits
}
pub fn into_edits(self) -> Vec<Edit> {
self.edits
}
/// Return the [`Applicability`] of the [`Fix`].
pub fn applicability(&self) -> Applicability {
self.applicability

View File

@@ -216,3 +216,15 @@ def get_items_list():
def get_items_set():
return tuple({item for item in items}) or None # OK
# https://github.com/astral-sh/ruff/issues/21473
tuple("") or True # SIM222
tuple(t"") or True # OK
tuple(0) or True # OK
tuple(1) or True # OK
tuple(False) or True # OK
tuple(None) or True # OK
tuple(...) or True # OK
tuple(lambda x: x) or True # OK
tuple(x for x in range(0)) or True # OK

View File

@@ -157,3 +157,15 @@ print(f"{1}{''}" and "bar")
# https://github.com/astral-sh/ruff/issues/7127
def f(a: "'' and 'b'"): ...
# https://github.com/astral-sh/ruff/issues/21473
tuple("") and False # SIM223
tuple(t"") and False # OK
tuple(0) and False # OK
tuple(1) and False # OK
tuple(False) and False # OK
tuple(None) and False # OK
tuple(...) and False # OK
tuple(lambda x: x) and False # OK
tuple(x for x in range(0)) and False # OK

View File

@@ -1144,3 +1144,23 @@ help: Replace with `(i for i in range(1))`
208 | # https://github.com/astral-sh/ruff/issues/21136
209 | def get_items():
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `True` instead of `... or True`
--> SIM222.py:222:1
|
221 | # https://github.com/astral-sh/ruff/issues/21473
222 | tuple("") or True # SIM222
| ^^^^^^^^^^^^^^^^^
223 | tuple(t"") or True # OK
224 | tuple(0) or True # OK
|
help: Replace with `True`
219 |
220 |
221 | # https://github.com/astral-sh/ruff/issues/21473
- tuple("") or True # SIM222
222 + True # SIM222
223 | tuple(t"") or True # OK
224 | tuple(0) or True # OK
225 | tuple(1) or True # OK
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1025,3 +1025,23 @@ help: Replace with `f"{''}{''}"`
156 |
157 |
note: This is an unsafe fix and may change runtime behavior
SIM223 [*] Use `tuple("")` instead of `tuple("") and ...`
--> SIM223.py:163:1
|
162 | # https://github.com/astral-sh/ruff/issues/21473
163 | tuple("") and False # SIM223
| ^^^^^^^^^^^^^^^^^^^
164 | tuple(t"") and False # OK
165 | tuple(0) and False # OK
|
help: Replace with `tuple("")`
160 |
161 |
162 | # https://github.com/astral-sh/ruff/issues/21473
- tuple("") and False # SIM223
163 + tuple("") # SIM223
164 | tuple(t"") and False # OK
165 | tuple(0) and False # OK
166 | tuple(1) and False # OK
note: This is an unsafe fix and may change runtime behavior

View File

@@ -57,7 +57,7 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
fn_argument: &str,
fix_enabled: bool,
violation: impl Violation,
applicability: Option<Applicability>,
applicability: Applicability,
) {
if call.arguments.len() != 1 {
return;
@@ -91,18 +91,14 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
let edit = Edit::range_replacement(replacement, range);
let fix = match applicability {
Some(Applicability::Unsafe) => Fix::unsafe_edits(edit, [import_edit]),
_ => {
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
Fix::applicable_edits(edit, [import_edit], applicability)
}
let applicability = match applicability {
Applicability::DisplayOnly => Applicability::DisplayOnly,
_ if checker.comment_ranges().intersects(range) => Applicability::Unsafe,
_ => applicability,
};
let fix = Fix::applicable_edits(edit, [import_edit], applicability);
Ok(fix)
});
}
@@ -138,6 +134,7 @@ pub(crate) fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool
typing::is_int(binding, semantic)
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn check_os_pathlib_two_arg_calls(
checker: &Checker,
call: &ExprCall,
@@ -146,6 +143,7 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
second_arg: &str,
fix_enabled: bool,
violation: impl Violation,
applicability: Applicability,
) {
let range = call.range();
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
@@ -174,10 +172,10 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
format!("{binding}({path_code}).{attr}({second_code})")
};
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
let applicability = match applicability {
Applicability::DisplayOnly => Applicability::DisplayOnly,
_ if checker.comment_ranges().intersects(range) => Applicability::Unsafe,
_ => applicability,
};
Ok(Fix::applicable_edits(
@@ -209,3 +207,9 @@ pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, positio
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())
}
/// Returns `true` if the given call is a top-level expression in its statement.
/// This means the call's return value is not used, so return type changes don't matter.
pub(crate) fn is_top_level_expression_call(checker: &Checker) -> bool {
checker.semantic().current_expression_parent().is_none()
}

View File

@@ -1,12 +1,14 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_getcwd_enabled;
use crate::{FixAvailability, Violation};
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_getcwd_enabled;
use crate::rules::flake8_use_pathlib::helpers::is_top_level_expression_call;
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `os.getcwd` and `os.getcwdb`.
///
@@ -37,6 +39,8 @@ use ruff_text_size::Ranged;
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
/// Additionally, the fix is marked as unsafe when the return value is used because the type changes
/// from `str` or `bytes` to a `Path` object.
///
/// ## References
/// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd)
@@ -83,7 +87,10 @@ pub(crate) fn os_getcwd(checker: &Checker, call: &ExprCall, segments: &[&str]) {
checker.semantic(),
)?;
let applicability = if checker.comment_ranges().intersects(range) {
// Unsafe when the fix would delete comments or change a used return value
let applicability = if checker.comment_ranges().intersects(range)
|| !is_top_level_expression_call(checker)
{
Applicability::Unsafe
} else {
Applicability::Safe

View File

@@ -45,6 +45,10 @@ use crate::{FixAvailability, Violation};
/// behaviors is required, there's no existing `pathlib` alternative. See CPython issue
/// [#69200](https://github.com/python/cpython/issues/69200).
///
/// Additionally, the fix is marked as unsafe because `os.path.abspath()` returns `str` or `bytes` (`AnyStr`),
/// while `Path.resolve()` returns a `Path` object. This change in return type can break code that uses
/// the return value.
///
/// ## References
/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
@@ -85,6 +89,6 @@ pub(crate) fn os_path_abspath(checker: &Checker, call: &ExprCall, segments: &[&s
"path",
is_fix_os_path_abspath_enabled(checker.settings()),
OsPathAbspath,
Some(Applicability::Unsafe),
Applicability::Unsafe,
);
}

View File

@@ -82,6 +82,6 @@ pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&
"p",
is_fix_os_path_basename_enabled(checker.settings()),
OsPathBasename,
Some(Applicability::Unsafe),
Applicability::Unsafe,
);
}

View File

@@ -42,6 +42,10 @@ use crate::{FixAvailability, Violation};
/// As a result, code relying on the exact string returned by `os.path.dirname`
/// may behave differently after the fix.
///
/// Additionally, the fix is marked as unsafe because `os.path.dirname()` returns `str` or `bytes` (`AnyStr`),
/// while `Path.parent` returns a `Path` object. This change in return type can break code that uses
/// the return value.
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
@@ -82,6 +86,6 @@ pub(crate) fn os_path_dirname(checker: &Checker, call: &ExprCall, segments: &[&s
"p",
is_fix_os_path_dirname_enabled(checker.settings()),
OsPathDirname,
Some(Applicability::Unsafe),
Applicability::Unsafe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -72,6 +73,6 @@ pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&st
"path",
is_fix_os_path_exists_enabled(checker.settings()),
OsPathExists,
None,
Applicability::Safe,
);
}

View File

@@ -41,6 +41,10 @@ use crate::{FixAvailability, Violation};
/// directory can't be resolved: `os.path.expanduser` returns the
/// input unchanged, while `Path.expanduser` raises `RuntimeError`.
///
/// Additionally, the fix is marked as unsafe because `os.path.expanduser()` returns `str` or `bytes` (`AnyStr`),
/// while `Path.expanduser()` returns a `Path` object. This change in return type can break code that uses
/// the return value.
///
/// ## References
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser)
@@ -76,6 +80,6 @@ pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &
"path",
is_fix_os_path_expanduser_enabled(checker.settings()),
OsPathExpanduser,
Some(Applicability::Unsafe),
Applicability::Unsafe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -75,6 +76,6 @@ pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&
"filename",
is_fix_os_path_getatime_enabled(checker.settings()),
OsPathGetatime,
None,
Applicability::Safe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -76,6 +77,6 @@ pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&
"filename",
is_fix_os_path_getctime_enabled(checker.settings()),
OsPathGetctime,
None,
Applicability::Safe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -76,6 +77,6 @@ pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&
"filename",
is_fix_os_path_getmtime_enabled(checker.settings()),
OsPathGetmtime,
None,
Applicability::Safe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -76,6 +77,6 @@ pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&s
"filename",
is_fix_os_path_getsize_enabled(checker.settings()),
OsPathGetsize,
None,
Applicability::Safe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -71,6 +72,6 @@ pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str
"s",
is_fix_os_path_isabs_enabled(checker.settings()),
OsPathIsabs,
None,
Applicability::Safe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -73,6 +74,6 @@ pub(crate) fn os_path_isdir(checker: &Checker, call: &ExprCall, segments: &[&str
"s",
is_fix_os_path_isdir_enabled(checker.settings()),
OsPathIsdir,
None,
Applicability::Safe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -73,6 +74,6 @@ pub(crate) fn os_path_isfile(checker: &Checker, call: &ExprCall, segments: &[&st
"path",
is_fix_os_path_isfile_enabled(checker.settings()),
OsPathIsfile,
None,
Applicability::Safe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -73,6 +74,6 @@ pub(crate) fn os_path_islink(checker: &Checker, call: &ExprCall, segments: &[&st
"path",
is_fix_os_path_islink_enabled(checker.settings()),
OsPathIslink,
None,
Applicability::Safe,
);
}

View File

@@ -1,11 +1,13 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_samefile_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.samefile`.
@@ -79,5 +81,6 @@ pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&
"f2",
fix_enabled,
OsPathSamefile,
Applicability::Safe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ExprCall, PythonVersion};
@@ -5,6 +6,7 @@ use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_readlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
is_top_level_expression_call,
};
use crate::{FixAvailability, Violation};
@@ -38,6 +40,8 @@ use crate::{FixAvailability, Violation};
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
/// Additionally, the fix is marked as unsafe when the return value is used because the type changes
/// from `str` or `bytes` (`AnyStr`) to a `Path` object.
///
/// ## References
/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline)
@@ -82,6 +86,13 @@ pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str])
return;
}
let applicability = if !is_top_level_expression_call(checker) {
// Unsafe because the return type changes (str/bytes -> Path)
Applicability::Unsafe
} else {
Applicability::Safe
};
check_os_pathlib_single_arg_calls(
checker,
call,
@@ -89,6 +100,6 @@ pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str])
"path",
is_fix_os_readlink_enabled(checker.settings()),
OsReadlink,
None,
applicability,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -84,6 +85,6 @@ pub(crate) fn os_remove(checker: &Checker, call: &ExprCall, segments: &[&str]) {
"path",
is_fix_os_remove_enabled(checker.settings()),
OsRemove,
None,
Applicability::Safe,
);
}

View File

@@ -1,12 +1,14 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_rename_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default,
is_keyword_only_argument_non_default, is_top_level_expression_call,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.rename`.
@@ -38,6 +40,8 @@ use ruff_python_ast::ExprCall;
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
/// Additionally, the fix is marked as unsafe when the return value is used because the type changes
/// from `None` to a `Path` object.
///
/// ## References
/// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
@@ -87,5 +91,22 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
);
check_os_pathlib_two_arg_calls(checker, call, "rename", "src", "dst", fix_enabled, OsRename);
// Unsafe when the fix would delete comments or change a used return value
let applicability = if !is_top_level_expression_call(checker) {
// Unsafe because the return type changes (None -> Path)
Applicability::Unsafe
} else {
Applicability::Safe
};
check_os_pathlib_two_arg_calls(
checker,
call,
"rename",
"src",
"dst",
fix_enabled,
OsRename,
applicability,
);
}

View File

@@ -1,12 +1,14 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_replace_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default,
is_keyword_only_argument_non_default, is_top_level_expression_call,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.replace`.
@@ -41,6 +43,8 @@ use ruff_python_ast::ExprCall;
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
/// Additionally, the fix is marked as unsafe when the return value is used because the type changes
/// from `None` to a `Path` object.
///
/// ## References
/// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
@@ -90,6 +94,14 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
);
// Unsafe when the fix would delete comments or change a used return value
let applicability = if !is_top_level_expression_call(checker) {
// Unsafe because the return type changes (None -> Path)
Applicability::Unsafe
} else {
Applicability::Safe
};
check_os_pathlib_two_arg_calls(
checker,
call,
@@ -98,5 +110,6 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
"dst",
fix_enabled,
OsReplace,
applicability,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -84,6 +85,6 @@ pub(crate) fn os_rmdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
"path",
is_fix_os_rmdir_enabled(checker.settings()),
OsRmdir,
None,
Applicability::Safe,
);
}

View File

@@ -1,3 +1,4 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -84,6 +85,6 @@ pub(crate) fn os_unlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
"path",
is_fix_os_unlink_enabled(checker.settings()),
OsUnlink,
None,
Applicability::Safe,
);
}

View File

@@ -1322,14 +1322,22 @@ impl Truthiness {
&& arguments.keywords.is_empty()
{
// Ex) `list([1, 2, 3])`
// For tuple(generator), we can't determine statically if the result will
// be empty or not, so return Unknown. The generator itself is truthy, but
// tuple(empty_generator) is falsy. ListComp and SetComp are handled by
// recursing into Self::from_expr below, which returns Unknown for them.
if argument.is_generator_expr() {
Self::Unknown
} else {
Self::from_expr(argument, is_builtin)
match argument {
// Return Unknown for types with definite truthiness that might
// result in empty iterables (t-strings and generators) or will
// raise a type error (non-iterable types like numbers, booleans,
// None, etc.).
Expr::NumberLiteral(_)
| Expr::BooleanLiteral(_)
| Expr::NoneLiteral(_)
| Expr::EllipsisLiteral(_)
| Expr::TString(_)
| Expr::Lambda(_)
| Expr::Generator(_) => Self::Unknown,
// Recurse for all other types - collections, comprehensions, variables, etc.
// StringLiteral, FString, and BytesLiteral recurse because Self::from_expr
// correctly handles their truthiness (checking if empty or not).
_ => Self::from_expr(argument, is_builtin),
}
} else {
Self::Unknown

View File

@@ -74,7 +74,7 @@ def f(): # a
The other option is to use the playground (also check the playground README):
```shell
cd playground && npm install && npm run dev:wasm && npm run dev
cd playground && npm ci --ignore-scripts && npm run dev:wasm && npm run dev
```
Run`npm run dev:wasm` and reload the page in the browser to refresh.

204
crates/ty/docs/rules.md generated
View File

@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L134" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L135" target="_blank">View source</a>
</small>
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L178" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L179" target="_blank">View source</a>
</small>
@@ -95,7 +95,7 @@ f(int) # error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L204" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L205" target="_blank">View source</a>
</small>
@@ -126,7 +126,7 @@ a = 1
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L229" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L230" target="_blank">View source</a>
</small>
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L255" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256" target="_blank">View source</a>
</small>
@@ -190,7 +190,7 @@ class B(A): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L281" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L282" target="_blank">View source</a>
</small>
@@ -218,7 +218,7 @@ type B = A
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L342" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L343" target="_blank">View source</a>
</small>
@@ -245,7 +245,7 @@ class B(A, A): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L363" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L364" target="_blank">View source</a>
</small>
@@ -357,7 +357,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L590" target="_blank">View source</a>
</small>
@@ -387,7 +387,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L591" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L614" target="_blank">View source</a>
</small>
@@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L395" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L396" target="_blank">View source</a>
</small>
@@ -502,7 +502,7 @@ an atypical memory layout.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L645" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L668" target="_blank">View source</a>
</small>
@@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L708" target="_blank">View source</a>
</small>
@@ -557,7 +557,7 @@ a: int = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1948" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1998" target="_blank">View source</a>
</small>
@@ -591,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L707" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L730" target="_blank">View source</a>
</small>
@@ -627,7 +627,7 @@ asyncio.run(main())
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L737" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L760" target="_blank">View source</a>
</small>
@@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811" target="_blank">View source</a>
</small>
@@ -678,7 +678,7 @@ with 1:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L832" target="_blank">View source</a>
</small>
@@ -707,7 +707,7 @@ a: str
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L832" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L855" target="_blank">View source</a>
</small>
@@ -751,7 +751,7 @@ except ZeroDivisionError:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1645" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1668" target="_blank">View source</a>
</small>
@@ -793,7 +793,7 @@ class D(A):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L868" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L891" target="_blank">View source</a>
</small>
@@ -826,7 +826,7 @@ class C[U](Generic[T]): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L612" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L635" target="_blank">View source</a>
</small>
@@ -865,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917" target="_blank">View source</a>
</small>
@@ -900,7 +900,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L991" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1014" target="_blank">View source</a>
</small>
@@ -934,7 +934,7 @@ class B(metaclass=f): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2076" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2126" target="_blank">View source</a>
</small>
@@ -1041,7 +1041,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L542" target="_blank">View source</a>
</small>
@@ -1052,7 +1052,8 @@ Checks for invalidly defined `NamedTuple` classes.
**Why is this bad?**
An invalidly defined `NamedTuple` class may lead to the type checker
drawing incorrect conclusions. It may also lead to `TypeError`s at runtime.
drawing incorrect conclusions. It may also lead to `TypeError`s or
`AttributeError`s at runtime.
**Examples**
@@ -1067,13 +1068,34 @@ in a class's bases list.
TypeError: can only inherit from a NamedTuple type and Generic
```
Further, `NamedTuple` field names cannot start with an underscore:
```pycon
>>> from typing import NamedTuple
>>> class Foo(NamedTuple):
... _bar: int
ValueError: Field names cannot start with an underscore: '_bar'
```
`NamedTuple` classes also have certain synthesized attributes (like `_asdict`, `_make`,
`_replace`, etc.) that cannot be overwritten. Attempting to assign to these attributes
without a type annotation will raise an `AttributeError` at runtime.
```pycon
>>> from typing import NamedTuple
>>> class Foo(NamedTuple):
... x: int
... _asdict = 42
AttributeError: Cannot overwrite NamedTuple attribute _asdict
```
## `invalid-newtype`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L967" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L990" target="_blank">View source</a>
</small>
@@ -1103,7 +1125,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1018" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1041" target="_blank">View source</a>
</small>
@@ -1153,7 +1175,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1117" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1140" target="_blank">View source</a>
</small>
@@ -1179,7 +1201,7 @@ def f(a: int = ''): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L922" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L945" target="_blank">View source</a>
</small>
@@ -1210,7 +1232,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L477" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L478" target="_blank">View source</a>
</small>
@@ -1244,7 +1266,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1137" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1160" target="_blank">View source</a>
</small>
@@ -1293,7 +1315,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L666" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L689" target="_blank">View source</a>
</small>
@@ -1318,7 +1340,7 @@ def func() -> int:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1180" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1203" target="_blank">View source</a>
</small>
@@ -1376,7 +1398,7 @@ TODO #14889
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L946" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L969" target="_blank">View source</a>
</small>
@@ -1403,7 +1425,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1435" target="_blank">View source</a>
</small>
@@ -1450,7 +1472,7 @@ Bar[int] # error: too few arguments
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1242" target="_blank">View source</a>
</small>
@@ -1480,7 +1502,7 @@ TYPE_CHECKING = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1243" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1266" target="_blank">View source</a>
</small>
@@ -1510,7 +1532,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1295" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1318" target="_blank">View source</a>
</small>
@@ -1544,7 +1566,7 @@ f(10) # Error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1267" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290" target="_blank">View source</a>
</small>
@@ -1578,7 +1600,7 @@ class C:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1346" target="_blank">View source</a>
</small>
@@ -1613,7 +1635,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1352" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1375" target="_blank">View source</a>
</small>
@@ -1638,7 +1660,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2049" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2099" target="_blank">View source</a>
</small>
@@ -1671,7 +1693,7 @@ alice["age"] # KeyError
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394" target="_blank">View source</a>
</small>
@@ -1700,7 +1722,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1417" target="_blank">View source</a>
</small>
@@ -1724,7 +1746,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1476" target="_blank">View source</a>
</small>
@@ -1750,7 +1772,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1618" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1641" target="_blank">View source</a>
</small>
@@ -1783,7 +1805,7 @@ class B(A):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1504" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1527" target="_blank">View source</a>
</small>
@@ -1810,7 +1832,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1802" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1852" target="_blank">View source</a>
</small>
@@ -1868,7 +1890,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1924" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1974" target="_blank">View source</a>
</small>
@@ -1898,7 +1920,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1595" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1618" target="_blank">View source</a>
</small>
@@ -1921,13 +1943,47 @@ class A: ...
class B(A): ... # Error raised here
```
## `super-call-in-named-tuple-method`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1786" target="_blank">View source</a>
</small>
**What it does**
Checks for calls to `super()` inside methods of `NamedTuple` classes.
**Why is this bad?**
Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime.
**Examples**
```python
from typing import NamedTuple
class F(NamedTuple):
x: int
def method(self):
super() # error: super() is not supported in methods of NamedTuple classes
```
**References**
- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)
## `too-many-positional-arguments`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1703" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1726" target="_blank">View source</a>
</small>
@@ -1954,7 +2010,7 @@ f("foo") # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1681" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1704" target="_blank">View source</a>
</small>
@@ -1982,7 +2038,7 @@ def _(x: int):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1724" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1747" target="_blank">View source</a>
</small>
@@ -2028,7 +2084,7 @@ class A:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1781" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1831" target="_blank">View source</a>
</small>
@@ -2055,7 +2111,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1823" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1873" target="_blank">View source</a>
</small>
@@ -2083,7 +2139,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1845" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1895" target="_blank">View source</a>
</small>
@@ -2108,7 +2164,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1864" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1914" target="_blank">View source</a>
</small>
@@ -2133,7 +2189,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1473" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1496" target="_blank">View source</a>
</small>
@@ -2170,7 +2226,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1883" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1933" target="_blank">View source</a>
</small>
@@ -2198,7 +2254,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1905" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1955" target="_blank">View source</a>
</small>
@@ -2223,7 +2279,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L507" target="_blank">View source</a>
</small>
@@ -2264,7 +2320,7 @@ class SubProto(BaseProto, Protocol):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L321" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L322" target="_blank">View source</a>
</small>
@@ -2352,7 +2408,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1525" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1548" target="_blank">View source</a>
</small>
@@ -2380,7 +2436,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L152" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L153" target="_blank">View source</a>
</small>
@@ -2412,7 +2468,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1547" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1570" target="_blank">View source</a>
</small>
@@ -2444,7 +2500,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1976" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2026" target="_blank">View source</a>
</small>
@@ -2471,7 +2527,7 @@ cast(int, f()) # Redundant
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1763" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1813" target="_blank">View source</a>
</small>
@@ -2495,7 +2551,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1997" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2047" target="_blank">View source</a>
</small>
@@ -2553,7 +2609,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L755" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L778" target="_blank">View source</a>
</small>
@@ -2592,7 +2648,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1084" target="_blank">View source</a>
</small>
@@ -2655,7 +2711,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L303" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L304" target="_blank">View source</a>
</small>
@@ -2679,7 +2735,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1573" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1596" target="_blank">View source</a>
</small>

View File

@@ -25,4 +25,4 @@ scope-simple-long-identifier,main.py,0,1
tstring-completions,main.py,0,1
ty-extensions-lower-stdlib,main.py,0,8
type-var-typing-over-ast,main.py,0,3
type-var-typing-over-ast,main.py,1,278
type-var-typing-over-ast,main.py,1,275
1 name file index rank
25 tstring-completions main.py 0 1
26 ty-extensions-lower-stdlib main.py 0 8
27 type-var-typing-over-ast main.py 0 3
28 type-var-typing-over-ast main.py 1 278 275

View File

@@ -1,6 +1,6 @@
use ruff_db::files::File;
use ty_project::Db;
use ty_python_semantic::{Module, all_modules};
use ty_python_semantic::{Module, ModuleName, all_modules, resolve_real_shadowable_module};
use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
@@ -8,12 +8,20 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
///
/// Returns symbols from all files in the workspace and dependencies, filtered
/// by the query.
pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolInfo<'db>> {
pub fn all_symbols<'db>(
db: &'db dyn Db,
importing_from: File,
query: &QueryPattern,
) -> Vec<AllSymbolInfo<'db>> {
// If the query is empty, return immediately to avoid expensive file scanning
if query.will_match_everything() {
return Vec::new();
}
let typing_extensions = ModuleName::new("typing_extensions").unwrap();
let is_typing_extensions_available = importing_from.is_stub(db)
|| resolve_real_shadowable_module(db, &typing_extensions).is_some();
let results = std::sync::Mutex::new(Vec::new());
{
let modules = all_modules(db);
@@ -28,6 +36,11 @@ pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolI
let Some(file) = module.file(&*db) else {
continue;
};
// TODO: also make it available in `TYPE_CHECKING` blocks
// (we'd need https://github.com/astral-sh/ty/issues/1553 to do this well)
if !is_typing_extensions_available && module.name(&*db) == &typing_extensions {
continue;
}
s.spawn(move |_| {
for (_, symbol) in symbols_for_file_global_only(&*db, file).search(query) {
// It seems like we could do better here than
@@ -143,7 +156,7 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
impl CursorTest {
fn all_symbols(&self, query: &str) -> String {
let symbols = all_symbols(&self.db, &QueryPattern::fuzzy(query));
let symbols = all_symbols(&self.db, self.cursor.file, &QueryPattern::fuzzy(query));
if symbols.is_empty() {
return "No symbols found".to_string();

View File

@@ -1,8 +1,10 @@
use crate::{completion, find_node::covering_node};
use ruff_db::{files::File, parsed::parsed_module};
use ruff_diagnostics::Edit;
use ruff_text_size::TextRange;
use ty_project::Db;
use ty_python_semantic::create_suppression_fix;
use ty_python_semantic::types::UNRESOLVED_REFERENCE;
/// A `QuickFix` Code Action
@@ -18,26 +20,501 @@ pub fn code_actions(
file: File,
diagnostic_range: TextRange,
diagnostic_id: &str,
) -> Option<Vec<QuickFix>> {
) -> Vec<QuickFix> {
let registry = db.lint_registry();
let Ok(lint_id) = registry.get(diagnostic_id) else {
return None;
return Vec::new();
};
if lint_id.name() == UNRESOLVED_REFERENCE.name() {
let parsed = parsed_module(db, file).load(db);
let node = covering_node(parsed.syntax().into(), diagnostic_range).node();
let symbol = &node.expr_name()?.id;
let fixes = completion::missing_imports(db, file, &parsed, symbol, node)
let mut actions = Vec::new();
if lint_id.name() == UNRESOLVED_REFERENCE.name()
&& let Some(import_quick_fix) = create_import_symbol_quick_fix(db, file, diagnostic_range)
{
actions.extend(import_quick_fix);
}
actions.push(QuickFix {
title: format!("Ignore '{}' for this line", lint_id.name()),
edits: create_suppression_fix(db, file, lint_id, diagnostic_range).into_edits(),
preferred: false,
});
actions
}
fn create_import_symbol_quick_fix(
db: &dyn Db,
file: File,
diagnostic_range: TextRange,
) -> Option<impl Iterator<Item = QuickFix>> {
let parsed = parsed_module(db, file).load(db);
let node = covering_node(parsed.syntax().into(), diagnostic_range).node();
let symbol = &node.expr_name()?.id;
Some(
completion::missing_imports(db, file, &parsed, symbol, node)
.into_iter()
.map(|import| QuickFix {
title: import.label,
edits: vec![import.edit],
preferred: true,
})
.collect();
Some(fixes)
} else {
None
}),
)
}
#[cfg(test)]
mod tests {
use crate::code_actions;
use insta::assert_snapshot;
use ruff_db::{
diagnostic::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig,
LintName, Span, SubDiagnostic,
},
files::{File, system_path_to_file},
system::{DbWithWritableSystem, SystemPathBuf},
};
use ruff_diagnostics::Fix;
use ruff_text_size::{TextRange, TextSize};
use ty_project::ProjectMetadata;
use ty_python_semantic::{lint::LintMetadata, types::UNRESOLVED_REFERENCE};
#[test]
fn add_ignore() {
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10"#);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:1:5
|
1 | b = a / 10
| ^
|
- b = a / 10
1 + b = a / 10 # ty:ignore[unresolved-reference]
");
}
#[test]
fn add_ignore_existing_comment() {
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10 # fmt: off"#);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:1:5
|
1 | b = a / 10 # fmt: off
| ^
|
- b = a / 10 # fmt: off
1 + b = a / 10 # fmt: off # ty:ignore[unresolved-reference]
");
}
#[test]
fn add_ignore_trailing_whitespace() {
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10 "#);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:1:5
|
1 | b = a / 10
| ^
|
- b = a / 10
1 + b = a / 10 # ty:ignore[unresolved-reference]
");
}
#[test]
fn add_code_existing_ignore() {
let test = CodeActionTest::with_source(
r#"
b = <START>a<END> / 0 # ty:ignore[division-by-zero]
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:2:17
|
2 | b = a / 0 # ty:ignore[division-by-zero]
| ^
|
1 |
- b = a / 0 # ty:ignore[division-by-zero]
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
3 |
");
}
#[test]
fn add_code_existing_ignore_trailing_comma() {
let test = CodeActionTest::with_source(
r#"
b = <START>a<END> / 0 # ty:ignore[division-by-zero,]
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:2:17
|
2 | b = a / 0 # ty:ignore[division-by-zero,]
| ^
|
1 |
- b = a / 0 # ty:ignore[division-by-zero,]
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
3 |
");
}
#[test]
fn add_code_existing_ignore_trailing_whitespace() {
let test = CodeActionTest::with_source(
r#"
b = <START>a<END> / 0 # ty:ignore[division-by-zero ]
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:2:17
|
2 | b = a / 0 # ty:ignore[division-by-zero ]
| ^
|
1 |
- b = a / 0 # ty:ignore[division-by-zero ]
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference ]
3 |
");
}
#[test]
fn add_code_existing_ignore_with_reason() {
let test = CodeActionTest::with_source(
r#"
b = <START>a<END> / 0 # ty:ignore[division-by-zero] some explanation
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:2:17
|
2 | b = a / 0 # ty:ignore[division-by-zero] some explanation
| ^
|
1 |
- b = a / 0 # ty:ignore[division-by-zero] some explanation
2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty:ignore[unresolved-reference]
3 |
");
}
#[test]
fn add_code_existing_ignore_start_line() {
let test = CodeActionTest::with_source(
r#"
b = (
<START>a # ty:ignore[division-by-zero]
/
0<END>
)
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:3:21
|
2 | b = (
3 | / a # ty:ignore[division-by-zero]
4 | | /
5 | | 0
| |_____________________^
6 | )
|
1 |
2 | b = (
- a # ty:ignore[division-by-zero]
3 + a # ty:ignore[division-by-zero, unresolved-reference]
4 | /
5 | 0
6 | )
");
}
#[test]
fn add_code_existing_ignore_end_line() {
let test = CodeActionTest::with_source(
r#"
b = (
<START>a
/
0<END> # ty:ignore[division-by-zero]
)
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:3:21
|
2 | b = (
3 | / a
4 | | /
5 | | 0 # ty:ignore[division-by-zero]
| |_____________________^
6 | )
|
2 | b = (
3 | a
4 | /
- 0 # ty:ignore[division-by-zero]
5 + 0 # ty:ignore[division-by-zero, unresolved-reference]
6 | )
7 |
");
}
#[test]
fn add_code_existing_ignores() {
let test = CodeActionTest::with_source(
r#"
b = (
<START>a # ty:ignore[division-by-zero]
/
0<END> # ty:ignore[division-by-zero]
)
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:3:21
|
2 | b = (
3 | / a # ty:ignore[division-by-zero]
4 | | /
5 | | 0 # ty:ignore[division-by-zero]
| |_____________________^
6 | )
|
1 |
2 | b = (
- a # ty:ignore[division-by-zero]
3 + a # ty:ignore[division-by-zero, unresolved-reference]
4 | /
5 | 0 # ty:ignore[division-by-zero]
6 | )
");
}
#[test]
fn add_code_interpolated_string() {
let test = CodeActionTest::with_source(
r#"
b = f"""
{<START>a<END>}
more text
"""
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:3:18
|
2 | b = f"""
3 | {a}
| ^
4 | more text
5 | """
|
2 | b = f"""
3 | {a}
4 | more text
- """
5 + """ # ty:ignore[unresolved-reference]
6 |
"#);
}
#[test]
fn add_code_multiline_interpolation() {
let test = CodeActionTest::with_source(
r#"
b = f"""
{
<START>a<END>
}
more text
"""
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:4:17
|
2 | b = f"""
3 | {
4 | a
| ^
5 | }
6 | more text
|
1 |
2 | b = f"""
3 | {
- a
4 + a # ty:ignore[unresolved-reference]
5 | }
6 | more text
7 | """
"#);
}
#[test]
fn add_code_followed_by_multiline_string() {
let test = CodeActionTest::with_source(
r#"
b = <START>a<END> + """
more text
"""
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:2:17
|
2 | b = a + """
| ^
3 | more text
4 | """
|
1 |
2 | b = a + """
3 | more text
- """
4 + """ # ty:ignore[unresolved-reference]
5 |
"#);
}
#[test]
fn add_code_followed_by_continuation() {
let test = CodeActionTest::with_source(
r#"
b = <START>a<END> \
+ "test"
"#,
);
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
info[code-action]: Ignore 'unresolved-reference' for this line
--> main.py:2:17
|
2 | b = a \
| ^
3 | + "test"
|
1 |
2 | b = a \
- + "test"
3 + + "test" # ty:ignore[unresolved-reference]
4 |
"#);
}
pub(super) struct CodeActionTest {
pub(super) db: ty_project::TestDb,
pub(super) file: File,
pub(super) diagnostic_range: TextRange,
}
impl CodeActionTest {
pub(super) fn with_source(source: &str) -> Self {
let mut db = ty_project::TestDb::new(ProjectMetadata::new(
"test".into(),
SystemPathBuf::from("/"),
));
db.init_program().unwrap();
let mut cleansed = source.to_string();
let start = cleansed
.find("<START>")
.expect("source text should contain a `<START>` marker");
cleansed.replace_range(start..start + "<START>".len(), "");
let end = cleansed
.find("<END>")
.expect("source text should contain a `<END>` marker");
cleansed.replace_range(end..end + "<END>".len(), "");
assert!(start <= end, "<START> marker should be before <END> marker");
db.write_file("main.py", cleansed)
.expect("write to memory file system to be successful");
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
Self {
db,
file,
diagnostic_range: TextRange::new(
TextSize::try_from(start).unwrap(),
TextSize::try_from(end).unwrap(),
),
}
}
pub(super) fn code_actions(&self, lint: &'static LintMetadata) -> String {
use std::fmt::Write;
let mut buf = String::new();
let config = DisplayDiagnosticConfig::default()
.color(false)
.show_fix_diff(true)
.format(DiagnosticFormat::Full);
for mut action in code_actions(&self.db, self.file, self.diagnostic_range, &lint.name) {
let mut diagnostic = Diagnostic::new(
DiagnosticId::Lint(LintName::of("code-action")),
ruff_db::diagnostic::Severity::Info,
action.title,
);
diagnostic.annotate(Annotation::primary(
Span::from(self.file).with_range(self.diagnostic_range),
));
if action.preferred {
diagnostic.sub(SubDiagnostic::new(
ruff_db::diagnostic::SubDiagnosticSeverity::Help,
"This is a preferred code action",
));
}
let first_edit = action.edits.remove(0);
diagnostic.set_fix(Fix::safe_edits(first_edit, action.edits));
write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap();
}
buf
}
}
}

View File

@@ -417,7 +417,16 @@ pub fn completion<'db>(
}
if settings.auto_import {
if let Some(scoped) = scoped {
add_unimported_completions(db, file, &parsed, scoped, &mut completions);
add_unimported_completions(
db,
file,
&parsed,
scoped,
|module_name: &ModuleName, symbol: &str| {
ImportRequest::import_from(module_name.as_str(), symbol)
},
&mut completions,
);
}
}
}
@@ -453,7 +462,16 @@ pub(crate) fn missing_imports(
) -> Vec<ImportEdit> {
let mut completions = Completions::exactly(db, symbol);
let scoped = ScopedTarget { node };
add_unimported_completions(db, file, parsed, scoped, &mut completions);
add_unimported_completions(
db,
file,
parsed,
scoped,
|module_name: &ModuleName, symbol: &str| {
ImportRequest::import_from(module_name.as_str(), symbol).force()
},
&mut completions,
);
completions.into_imports()
}
@@ -502,6 +520,7 @@ fn add_unimported_completions<'db>(
file: File,
parsed: &ParsedModuleRef,
scoped: ScopedTarget<'_>,
create_import_request: impl for<'a> Fn(&'a ModuleName, &'a str) -> ImportRequest<'a>,
completions: &mut Completions<'db>,
) {
// This is redundant since `all_symbols` will also bail
@@ -517,14 +536,13 @@ fn add_unimported_completions<'db>(
let importer = Importer::new(db, &stylist, file, source.as_str(), parsed);
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
for symbol in all_symbols(db, &completions.query) {
for symbol in all_symbols(db, file, &completions.query) {
if symbol.module.file(db) == Some(file) || symbol.module.is_known(db, KnownModule::Builtins)
{
continue;
}
let request =
ImportRequest::import_from(symbol.module.name(db).as_str(), &symbol.symbol.name);
let request = create_import_request(symbol.module.name(db), &symbol.symbol.name);
// FIXME: `all_symbols` doesn't account for wildcard imports.
// Since we're looking at every module, this is probably
// "fine," but it might mean that we import a symbol from the
@@ -5566,10 +5584,7 @@ def foo(param: s<CURSOR>)
#[test]
fn from_import_no_space_not_suggests_import() {
let builder = completion_test_builder("from typing<CURSOR>");
assert_snapshot!(builder.build().snapshot(), @r"
typing
typing_extensions
");
assert_snapshot!(builder.build().snapshot(), @"typing");
}
#[test]
@@ -5785,6 +5800,86 @@ from .imp<CURSOR>
");
}
#[test]
fn typing_extensions_excluded_from_import() {
let builder = completion_test_builder("from typing<CURSOR>").module_names();
assert_snapshot!(builder.build().snapshot(), @"typing :: Current module");
}
#[test]
fn typing_extensions_excluded_from_auto_import() {
let builder = completion_test_builder("deprecated<CURSOR>")
.auto_import()
.module_names();
assert_snapshot!(builder.build().snapshot(), @r"
Deprecated :: importlib.metadata
DeprecatedList :: importlib.metadata
DeprecatedNonAbstract :: importlib.metadata
DeprecatedTuple :: importlib.metadata
deprecated :: warnings
");
}
#[test]
fn typing_extensions_included_from_import() {
let builder = CursorTest::builder()
.source("typing_extensions.py", "deprecated = 1")
.source("foo.py", "from typing<CURSOR>")
.completion_test_builder()
.module_names();
assert_snapshot!(builder.build().snapshot(), @r"
typing :: Current module
typing_extensions :: Current module
");
}
#[test]
fn typing_extensions_included_from_auto_import() {
let builder = CursorTest::builder()
.source("typing_extensions.py", "deprecated = 1")
.source("foo.py", "deprecated<CURSOR>")
.completion_test_builder()
.auto_import()
.module_names();
assert_snapshot!(builder.build().snapshot(), @r"
Deprecated :: importlib.metadata
DeprecatedList :: importlib.metadata
DeprecatedNonAbstract :: importlib.metadata
DeprecatedTuple :: importlib.metadata
deprecated :: typing_extensions
deprecated :: warnings
");
}
#[test]
fn typing_extensions_included_from_import_in_stub() {
let builder = CursorTest::builder()
.source("foo.pyi", "from typing<CURSOR>")
.completion_test_builder()
.module_names();
assert_snapshot!(builder.build().snapshot(), @r"
typing :: Current module
typing_extensions :: Current module
");
}
#[test]
fn typing_extensions_included_from_auto_import_in_stub() {
let builder = CursorTest::builder()
.source("foo.pyi", "deprecated<CURSOR>")
.completion_test_builder()
.auto_import()
.module_names();
assert_snapshot!(builder.build().snapshot(), @r"
Deprecated :: importlib.metadata
DeprecatedList :: importlib.metadata
DeprecatedNonAbstract :: importlib.metadata
DeprecatedTuple :: importlib.metadata
deprecated :: typing_extensions
deprecated :: warnings
");
}
/// A way to create a simple single-file (named `main.py`) completion test
/// builder.
///

View File

@@ -1656,4 +1656,218 @@ func<CURSOR>_alias()
|
");
}
#[test]
fn stub_target() {
let test = CursorTest::builder()
.source(
"path.pyi",
r#"
class Path:
def __init__(self, path: str): ...
"#,
)
.source(
"path.py",
r#"
class Path:
def __init__(self, path: str):
self.path = path
"#,
)
.source(
"importer.py",
r#"
from path import Path<CURSOR>
a: Path = Path("test")
"#,
)
.build();
assert_snapshot!(test.references(), @r###"
info[references]: Reference 1
--> path.pyi:2:7
|
2 | class Path:
| ^^^^
3 | def __init__(self, path: str): ...
|
info[references]: Reference 2
--> importer.py:2:18
|
2 | from path import Path
| ^^^^
3 |
4 | a: Path = Path("test")
|
info[references]: Reference 3
--> importer.py:4:4
|
2 | from path import Path
3 |
4 | a: Path = Path("test")
| ^^^^
|
info[references]: Reference 4
--> importer.py:4:11
|
2 | from path import Path
3 |
4 | a: Path = Path("test")
| ^^^^
|
"###);
}
#[test]
fn import_alias() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
import warnings
import warnings as <CURSOR>abc
x = abc
y = warnings
"#,
)
.build();
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:3:20
|
2 | import warnings
3 | import warnings as abc
| ^^^
4 |
5 | x = abc
|
info[references]: Reference 2
--> main.py:5:5
|
3 | import warnings as abc
4 |
5 | x = abc
| ^^^
6 | y = warnings
|
");
}
#[test]
fn import_alias_use() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
import warnings
import warnings as abc
x = abc<CURSOR>
y = warnings
"#,
)
.build();
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:3:20
|
2 | import warnings
3 | import warnings as abc
| ^^^
4 |
5 | x = abc
|
info[references]: Reference 2
--> main.py:5:5
|
3 | import warnings as abc
4 |
5 | x = abc
| ^^^
6 | y = warnings
|
");
}
#[test]
fn import_from_alias() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from warnings import deprecated as xyz<CURSOR>
from warnings import deprecated
y = xyz
z = deprecated
"#,
)
.build();
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:2:36
|
2 | from warnings import deprecated as xyz
| ^^^
3 | from warnings import deprecated
|
info[references]: Reference 2
--> main.py:5:5
|
3 | from warnings import deprecated
4 |
5 | y = xyz
| ^^^
6 | z = deprecated
|
");
}
#[test]
fn import_from_alias_use() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from warnings import deprecated as xyz
from warnings import deprecated
y = xyz<CURSOR>
z = deprecated
"#,
)
.build();
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:2:36
|
2 | from warnings import deprecated as xyz
| ^^^
3 | from warnings import deprecated
|
info[references]: Reference 2
--> main.py:5:5
|
3 | from warnings import deprecated
4 |
5 | y = xyz
| ^^^
6 | z = deprecated
|
");
}
}

View File

@@ -334,15 +334,24 @@ impl GotoTarget<'_> {
let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?;
ty
}
// TODO: Support identifier targets
GotoTarget::PatternMatchRest(_)
| GotoTarget::PatternKeywordArgument(_)
| GotoTarget::PatternMatchStarName(_)
| GotoTarget::PatternMatchAsName(_)
| GotoTarget::TypeParamParamSpecName(_)
| GotoTarget::TypeParamTypeVarTupleName(_)
| GotoTarget::NonLocal { .. }
| GotoTarget::Globals { .. } => return None,
GotoTarget::PatternMatchRest(pattern) => {
model.inferred_type_for_identifier(pattern.rest.as_ref()?)
}
GotoTarget::PatternKeywordArgument(pattern) => {
model.inferred_type_for_identifier(&pattern.attr)
}
GotoTarget::PatternMatchStarName(pattern) => {
model.inferred_type_for_identifier(pattern.name.as_ref()?)
}
GotoTarget::PatternMatchAsName(pattern) => {
model.inferred_type_for_identifier(pattern.name.as_ref()?)
}
GotoTarget::NonLocal { identifier } => model.inferred_type_for_identifier(identifier),
GotoTarget::Globals { identifier } => model.inferred_type_for_identifier(identifier),
// These don't really... *have* a type?
GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamTypeVarTupleName(_) => {
return None;
}
};
Some(ty)
@@ -396,13 +405,19 @@ impl GotoTarget<'_> {
GotoTarget::ImportSymbolAlias {
alias, import_from, ..
} => {
let symbol_name = alias.name.as_str();
Some(definitions_for_imported_symbol(
model,
import_from,
symbol_name,
alias_resolution,
))
if let Some(asname) = alias.asname.as_ref()
&& alias_resolution == ImportAliasResolution::PreserveAliases
{
Some(definitions_for_name(model, asname.as_str(), asname.into()))
} else {
let symbol_name = alias.name.as_str();
Some(definitions_for_imported_symbol(
model,
import_from,
symbol_name,
alias_resolution,
))
}
}
GotoTarget::ImportModuleComponent {
@@ -418,12 +433,12 @@ impl GotoTarget<'_> {
// Handle import aliases (offset within 'z' in "import x.y as z")
GotoTarget::ImportModuleAlias { alias } => {
if alias_resolution == ImportAliasResolution::ResolveAliases {
definitions_for_module(model, Some(alias.name.as_str()), 0)
if let Some(asname) = alias.asname.as_ref()
&& alias_resolution == ImportAliasResolution::PreserveAliases
{
Some(definitions_for_name(model, asname.as_str(), asname.into()))
} else {
alias.asname.as_ref().map(|name| {
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
})
definitions_for_module(model, Some(alias.name.as_str()), 0)
}
}

View File

@@ -975,7 +975,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^^
5 | x = ab
|
"#);
}
#[test]
@@ -1003,7 +1022,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^^
5 | x = ab
|
"#);
}
#[test]
@@ -1031,7 +1069,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^^
5 | x = ab
|
"#);
}
#[test]
@@ -1065,7 +1122,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^
11 | x = ab
|
");
}
#[test]
@@ -1143,7 +1219,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:10:23
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^^^^^
11 | x = ab
|
");
}
#[test]
@@ -1395,7 +1490,26 @@ def outer():
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:6:18
|
5 | def inner():
6 | nonlocal xy
| ^^
7 | xy = "modified"
8 | return x # Should find the nonlocal x declaration in outer scope
|
"#);
}
#[test]
@@ -1447,7 +1561,26 @@ def function():
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:5:12
|
4 | def function():
5 | global global_var
| ^^^^^^^^^^
6 | global_var = "modified"
7 | return global_var # Should find the global variable declaration
|
"#);
}
#[test]

View File

@@ -1704,7 +1704,26 @@ def outer():
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:6:18
|
5 | def inner():
6 | nonlocal xy
| ^-
| ||
| |Cursor offset
| source
7 | xy = "modified"
8 | return x # Should find the nonlocal x declaration in outer scope
|
"#);
}
#[test]
@@ -1756,7 +1775,26 @@ def function():
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:12
|
4 | def function():
5 | global global_var
| ^^^^^^^-^^
| | |
| | Cursor offset
| source
6 | global_var = "modified"
7 | return global_var # Should find the global variable declaration
|
"#);
}
#[test]
@@ -1770,7 +1808,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^-
| ||
| |Cursor offset
| source
5 | x = ab
|
"#);
}
#[test]
@@ -1816,7 +1873,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^-
| ||
| |Cursor offset
| source
5 | x = ab
|
"#);
}
#[test]
@@ -1862,7 +1938,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^-
| ||
| |Cursor offset
| source
5 | x = ab
|
"#);
}
#[test]
@@ -1914,7 +2009,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^-
| ||
| |Cursor offset
| source
11 | x = ab
|
");
}
#[test]
@@ -2011,7 +2125,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:10:23
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^^-^^
| | |
| | Cursor offset
| source
11 | x = ab
|
");
}
#[test]

View File

@@ -553,6 +553,16 @@ impl<'a> ImportRequest<'a> {
}
}
/// Causes this request to become a command. This will force the
/// requested import style, even if another style would be more
/// appropriate generally.
pub(crate) fn force(mut self) -> Self {
Self {
force_style: true,
..self
}
}
/// Attempts to change the import request style so that the chances
/// of an import conflict are minimized (although not always reduced
/// to zero).

View File

@@ -12,7 +12,7 @@
use crate::find_node::CoveringNode;
use crate::goto::GotoTarget;
use crate::{Db, NavigationTarget, ReferenceKind, ReferenceTarget};
use crate::{Db, NavigationTargets, ReferenceKind, ReferenceTarget};
use ruff_db::files::File;
use ruff_python_ast::{
self as ast, AnyNodeRef,
@@ -49,10 +49,9 @@ pub(crate) fn references(
// When finding references, do not resolve any local aliases.
let model = SemanticModel::new(db, file);
let target_definitions_nav = goto_target
let target_definitions = goto_target
.get_definition_targets(&model, ImportAliasResolution::PreserveAliases)?
.definition_targets(db)?;
let target_definitions: Vec<NavigationTarget> = target_definitions_nav.into_iter().collect();
.declaration_targets(db)?;
// Extract the target text from the goto target for fast comparison
let target_text = goto_target.to_string()?;
@@ -115,7 +114,7 @@ pub(crate) fn references(
fn references_for_file(
db: &dyn Db,
file: File,
target_definitions: &[NavigationTarget],
target_definitions: &NavigationTargets,
target_text: &str,
mode: ReferencesMode,
references: &mut Vec<ReferenceTarget>,
@@ -159,7 +158,7 @@ fn is_symbol_externally_visible(goto_target: &GotoTarget<'_>) -> bool {
struct LocalReferencesFinder<'a> {
model: &'a SemanticModel<'a>,
tokens: &'a Tokens,
target_definitions: &'a [NavigationTarget],
target_definitions: &'a NavigationTargets,
references: &'a mut Vec<ReferenceTarget>,
mode: ReferencesMode,
target_text: &'a str,
@@ -318,12 +317,10 @@ impl LocalReferencesFinder<'_> {
GotoTarget::from_covering_node(self.model, covering_node, offset, self.tokens)
{
// Get the definitions for this goto target
if let Some(current_definitions_nav) = goto_target
if let Some(current_definitions) = goto_target
.get_definition_targets(self.model, ImportAliasResolution::PreserveAliases)
.and_then(|definitions| definitions.declaration_targets(self.model.db()))
{
let current_definitions: Vec<NavigationTarget> =
current_definitions_nav.into_iter().collect();
// Check if any of the current definitions match our target definitions
if self.navigation_targets_match(&current_definitions) {
// Determine if this is a read or write reference
@@ -337,7 +334,7 @@ impl LocalReferencesFinder<'_> {
}
/// Check if `Vec<NavigationTarget>` match our target definitions
fn navigation_targets_match(&self, current_targets: &[NavigationTarget]) -> bool {
fn navigation_targets_match(&self, current_targets: &NavigationTargets) -> bool {
// Since we're comparing the same symbol, all definitions should be equivalent
// We only need to check against the first target definition
if let Some(first_target) = self.target_definitions.iter().next() {

View File

@@ -163,7 +163,7 @@ mod tests {
}
#[test]
fn test_prepare_rename_parameter() {
fn prepare_rename_parameter() {
let test = cursor_test(
"
def func(<CURSOR>value: int) -> int:
@@ -178,7 +178,7 @@ value = 0
}
#[test]
fn test_rename_parameter() {
fn rename_parameter() {
let test = cursor_test(
"
def func(<CURSOR>value: int) -> int:
@@ -207,7 +207,7 @@ func(value=42)
}
#[test]
fn test_rename_function() {
fn rename_function() {
let test = cursor_test(
"
def fu<CURSOR>nc():
@@ -235,7 +235,7 @@ x = func
}
#[test]
fn test_rename_class() {
fn rename_class() {
let test = cursor_test(
"
class My<CURSOR>Class:
@@ -265,7 +265,7 @@ cls = MyClass
}
#[test]
fn test_rename_invalid_name() {
fn rename_invalid_name() {
let test = cursor_test(
"
def fu<CURSOR>nc():
@@ -286,7 +286,7 @@ def fu<CURSOR>nc():
}
#[test]
fn test_multi_file_function_rename() {
fn multi_file_function_rename() {
let test = CursorTest::builder()
.source(
"utils.py",
@@ -312,7 +312,7 @@ from utils import helper_function
class DataProcessor:
def __init__(self):
self.multiplier = helper_function
def process(self, value):
return helper_function(value)
",
@@ -654,7 +654,7 @@ class DataProcessor:
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=a<CURSOR>b):
@@ -685,7 +685,7 @@ class DataProcessor:
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, button=ab):
@@ -716,7 +716,7 @@ class DataProcessor:
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Cl<CURSOR>ick(x, button=ab):
@@ -756,7 +756,7 @@ class DataProcessor:
def __init__(self, pos, btn):
self.position: int = pos
self.button: str = btn
def my_func(event: Click):
match event:
case Click(x, but<CURSOR>ton=ab):
@@ -880,7 +880,7 @@ class DataProcessor:
}
#[test]
fn test_cannot_rename_import_module_component() {
fn cannot_rename_import_module_component() {
// Test that we cannot rename parts of module names in import statements
let test = cursor_test(
"
@@ -893,7 +893,7 @@ x = os.path.join('a', 'b')
}
#[test]
fn test_cannot_rename_from_import_module_component() {
fn cannot_rename_from_import_module_component() {
// Test that we cannot rename parts of module names in from import statements
let test = cursor_test(
"
@@ -906,7 +906,7 @@ result = join('a', 'b')
}
#[test]
fn test_cannot_rename_external_file() {
fn cannot_rename_external_file() {
// This test verifies that we cannot rename a symbol when it's defined in a file
// that's outside the project (like a standard library function)
let test = cursor_test(
@@ -920,7 +920,7 @@ x = <CURSOR>os.path.join('a', 'b')
}
#[test]
fn test_rename_alias_at_import_statement() {
fn rename_alias_at_import_statement() {
let test = CursorTest::builder()
.source(
"utils.py",
@@ -931,8 +931,8 @@ def test(): pass
.source(
"main.py",
"
from utils import test as test_<CURSOR>alias
result = test_alias()
from utils import test as <CURSOR>alias
result = alias()
",
)
.build();
@@ -941,16 +941,16 @@ result = test_alias()
info[rename]: Rename symbol (found 2 locations)
--> main.py:2:27
|
2 | from utils import test as test_alias
| ^^^^^^^^^^
3 | result = test_alias()
| ----------
2 | from utils import test as alias
| ^^^^^
3 | result = alias()
| -----
|
");
}
#[test]
fn test_rename_alias_at_usage_site() {
fn rename_alias_at_usage_site() {
// Test renaming an alias when the cursor is on the alias in the usage statement
let test = CursorTest::builder()
.source(
@@ -962,8 +962,8 @@ def test(): pass
.source(
"main.py",
"
from utils import test as test_alias
result = test_<CURSOR>alias()
from utils import test as alias
result = <CURSOR>alias()
",
)
.build();
@@ -972,16 +972,16 @@ result = test_<CURSOR>alias()
info[rename]: Rename symbol (found 2 locations)
--> main.py:2:27
|
2 | from utils import test as test_alias
| ^^^^^^^^^^
3 | result = test_alias()
| ----------
2 | from utils import test as alias
| ^^^^^
3 | result = alias()
| -----
|
");
}
#[test]
fn test_rename_across_import_chain_with_mixed_aliases() {
fn rename_across_import_chain_with_mixed_aliases() {
// Test renaming a symbol that's imported across multiple files with mixed alias patterns
// File 1 (source.py): defines the original function
// File 2 (middle.py): imports without alias from source.py
@@ -1049,7 +1049,7 @@ value1 = func_alias()
}
#[test]
fn test_rename_alias_in_import_chain() {
fn rename_alias_in_import_chain() {
let test = CursorTest::builder()
.source(
"file1.py",
@@ -1101,7 +1101,7 @@ class App:
}
#[test]
fn test_cannot_rename_keyword() {
fn cannot_rename_keyword() {
// Test that we cannot rename Python keywords like "None"
let test = cursor_test(
"
@@ -1116,7 +1116,7 @@ def process_value(value):
}
#[test]
fn test_cannot_rename_builtin_type() {
fn cannot_rename_builtin_type() {
// Test that we cannot rename Python builtin types like "int"
let test = cursor_test(
"
@@ -1129,7 +1129,7 @@ def convert_to_number(value):
}
#[test]
fn test_rename_keyword_argument() {
fn rename_keyword_argument() {
// Test renaming a keyword argument and its corresponding parameter
let test = cursor_test(
"
@@ -1156,7 +1156,7 @@ result = func(10, <CURSOR>y=20)
}
#[test]
fn test_rename_parameter_with_keyword_argument() {
fn rename_parameter_with_keyword_argument() {
// Test renaming a parameter and its corresponding keyword argument
let test = cursor_test(
"
@@ -1181,4 +1181,64 @@ result = func(10, y=20)
|
");
}
#[test]
fn import_alias() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
import warnings
import warnings as <CURSOR>abc
x = abc
y = warnings
"#,
)
.build();
assert_snapshot!(test.rename("z"), @r"
info[rename]: Rename symbol (found 2 locations)
--> main.py:3:20
|
2 | import warnings
3 | import warnings as abc
| ^^^
4 |
5 | x = abc
| ---
6 | y = warnings
|
");
}
#[test]
fn import_alias_use() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
import warnings
import warnings as abc
x = abc<CURSOR>
y = warnings
"#,
)
.build();
assert_snapshot!(test.rename("z"), @r"
info[rename]: Rename symbol (found 2 locations)
--> main.py:3:20
|
2 | import warnings
3 | import warnings as abc
| ^^^
4 |
5 | x = abc
| ---
6 | y = warnings
|
");
}
}

View File

@@ -0,0 +1,34 @@
# GenericAlias in type expressions
We recognize if a `types.GenericAlias` instance is created by specializing a generic class. We don't
explicitly mention it in our type display, but `list[int]` in the example below is a `GenericAlias`
instance at runtime:
```py
Numbers = list[int]
# At runtime, `Numbers` is an instance of `types.GenericAlias`. Showing
# this as `list[int]` is more helpful, though:
reveal_type(Numbers) # revealed: <class 'list[int]'>
def _(numbers: Numbers) -> None:
reveal_type(numbers) # revealed: list[int]
```
It is also valid to create `GenericAlias` instances manually:
```py
from types import GenericAlias
Strings = GenericAlias(list, (str,))
reveal_type(Strings) # revealed: GenericAlias
```
However, using such a `GenericAlias` instance in a type expression is currently not supported:
```py
# error: [invalid-type-form] "Variable of type `GenericAlias` is not allowed in a type expression"
def _(strings: Strings) -> None:
reveal_type(strings) # revealed: Unknown
```

View File

@@ -1,24 +1,16 @@
# NewType
## Valid forms
## Basic usage
`NewType` can be used to create distinct types that are based on existing types:
```py
from typing_extensions import NewType
from types import GenericAlias
X = GenericAlias(type, ())
A = NewType("A", int)
# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
# to be compatible with `type`
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `<NewType pseudo-class 'A'>`"
B = GenericAlias(A, ())
UserId = NewType("UserId", int)
def _(
a: A,
b: B,
):
reveal_type(a) # revealed: A
reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
def _(user_id: UserId):
reveal_type(user_id) # revealed: UserId
```
## Subtyping

View File

@@ -52,6 +52,10 @@ def f(x: A):
JSONPrimitive = Union[str, int, float, bool, None]
JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]')
def _(x: JSONValue):
# TODO: should be `JSONValue`
reveal_type(x) # revealed: Divergent
```
## Self-referential legacy type variables

View File

@@ -284,10 +284,17 @@ python-version = "3.12"
```py
from typing import assert_never
class A[T]: ...
class A[T]:
value: T
class ASub[T](A[T]): ...
class B[T]: ...
class C[T]: ...
class B[T]:
value: T
class C[T]:
value: T
class D: ...
class E: ...
class F: ...

View File

@@ -51,6 +51,10 @@ class Parent:
@final
def my_property2(self) -> int: ...
@property
@final
def my_property3(self) -> int: ...
@final
@classmethod
def class_method1(cls) -> int: ...
@@ -86,6 +90,13 @@ class Child(Parent):
@property
def my_property2(self) -> int: ... # error: [override-of-final-method]
@my_property2.setter
def my_property2(self, x: int) -> None: ...
@property
def my_property3(self) -> int: ... # error: [override-of-final-method]
@my_property3.deleter
def my_proeprty3(self) -> None: ...
@classmethod
def class_method1(cls) -> int: ... # error: [override-of-final-method]
@@ -230,7 +241,7 @@ class ChildOfBad(Bad):
def bar(self, x: str) -> str: ...
@overload
def bar(self, x: int) -> int: ... # error: [override-of-final-method]
@overload
def baz(self, x: str) -> str: ...
@overload
@@ -461,14 +472,17 @@ class B(A):
def method1(self) -> None: ... # error: [override-of-final-method]
def method2(self) -> None: ... # error: [override-of-final-method]
def method3(self) -> None: ... # error: [override-of-final-method]
def method4(self) -> None: ... # error: [override-of-final-method]
# check that autofixes don't introduce invalid syntax
# if there are multiple statements on one line
#
# TODO: we should emit a Liskov violation here too
# error: [override-of-final-method]
method4 = 42; unrelated = 56 # fmt: skip
# Possible overrides of possibly `@final` methods...
class C(A):
if coinflip():
# TODO: the autofix here introduces invalid syntax because there are now no
# statements inside the `if:` branch
# (but it might still be a useful autofix in an IDE context?)
def method1(self) -> None: ... # error: [override-of-final-method]
else:
pass

View File

@@ -5,6 +5,11 @@
At its simplest, to define a generic class using the legacy syntax, you inherit from the
`typing.Generic` special form, which is "specialized" with the generic class's type variables.
```toml
[environment]
python-version = "3.11"
```
```py
from ty_extensions import generic_context
from typing_extensions import Generic, TypeVar, TypeVarTuple, ParamSpec, Unpack
@@ -19,7 +24,9 @@ class MultipleTypevars(Generic[T, S]): ...
class SingleParamSpec(Generic[P]): ...
class TypeVarAndParamSpec(Generic[P, T]): ...
class SingleTypeVarTuple(Generic[Unpack[Ts]]): ...
class StarredSingleTypeVarTuple(Generic[*Ts]): ...
class TypeVarAndTypeVarTuple(Generic[T, Unpack[Ts]]): ...
class StarredTypeVarAndTypeVarTuple(Generic[T, *Ts]): ...
# revealed: ty_extensions.GenericContext[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar))
@@ -34,6 +41,8 @@ reveal_type(generic_context(TypeVarAndParamSpec))
# TODO: support `TypeVarTuple` properly (these should not reveal `None`)
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: None
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: None
reveal_type(generic_context(StarredSingleTypeVarTuple)) # revealed: None
reveal_type(generic_context(StarredTypeVarAndTypeVarTuple)) # revealed: None
```
Inheriting from `Generic` multiple times yields a `duplicate-base` diagnostic, just like any other
@@ -210,6 +219,37 @@ reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str]
reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int]
```
## Diagnostics for bad specializations
We show the user where the type variable was defined if a specialization is given that doesn't
satisfy the type variable's upper bound or constraints:
<!-- snapshot-diagnostics -->
`library.py`:
```py
from typing import TypeVar, Generic
T = TypeVar("T", bound=str)
U = TypeVar("U", int, bytes)
class Bounded(Generic[T]):
x: T
class Constrained(Generic[U]):
x: U
```
`main.py`:
```py
from library import Bounded, Constrained
x: Bounded[int] # error: [invalid-type-arguments]
y: Constrained[str] # error: [invalid-type-arguments]
```
## Inferring generic class parameters
We can infer the type parameter from a type context:

View File

@@ -1,4 +1,4 @@
# `ParamSpec`
# Legacy `ParamSpec`
## Definition
@@ -115,59 +115,3 @@ P = ParamSpec("P", default=[A, B])
class A: ...
class B: ...
```
### PEP 695
```toml
[environment]
python-version = "3.12"
```
#### Valid
```py
def foo1[**P]() -> None:
reveal_type(P) # revealed: typing.ParamSpec
def foo2[**P = ...]() -> None:
reveal_type(P) # revealed: typing.ParamSpec
def foo3[**P = [int, str]]() -> None:
reveal_type(P) # revealed: typing.ParamSpec
def foo4[**P, **Q = P]():
reveal_type(P) # revealed: typing.ParamSpec
reveal_type(Q) # revealed: typing.ParamSpec
```
#### Invalid
ParamSpec, when defined using the new syntax, does not allow defining bounds or constraints.
This results in a lot of syntax errors mainly because the AST doesn't accept them in this position.
The parser could do a better job in recovering from these errors.
<!-- blacken-docs:off -->
```py
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
def foo[**P: int]() -> None:
# error: [invalid-syntax]
# error: [invalid-syntax]
pass
```
<!-- blacken-docs:on -->
#### Invalid default
```py
# error: [invalid-paramspec]
def foo[**P = int]() -> None:
pass
```

View File

@@ -191,6 +191,32 @@ reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str]
reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int]
```
## Diagnostics for bad specializations
We show the user where the type variable was defined if a specialization is given that doesn't
satisfy the type variable's upper bound or constraints:
<!-- snapshot-diagnostics -->
`library.py`:
```py
class Bounded[T: str]:
x: T
class Constrained[U: (int, bytes)]:
x: U
```
`main.py`:
```py
from library import Bounded, Constrained
x: Bounded[int] # error: [invalid-type-arguments]
y: Constrained[str] # error: [invalid-type-arguments]
```
## Inferring generic class parameters
We can infer the type parameter from a type context:

View File

@@ -0,0 +1,64 @@
# PEP 695 `ParamSpec`
`ParamSpec` was introduced in Python 3.12 while the support for specifying defaults was added in
Python 3.13.
```toml
[environment]
python-version = "3.13"
```
## Definition
```py
def foo1[**P]() -> None:
reveal_type(P) # revealed: typing.ParamSpec
```
## Bounds and constraints
`ParamSpec`, when defined using the new syntax, does not allow defining bounds or constraints.
TODO: This results in a lot of syntax errors mainly because the AST doesn't accept them in this
position. The parser could do a better job in recovering from these errors.
<!-- blacken-docs:off -->
```py
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
def foo[**P: int]() -> None:
# error: [invalid-syntax]
# error: [invalid-syntax]
pass
```
<!-- blacken-docs:on -->
## Default
The default value for a `ParamSpec` can be either a list of types, `...`, or another `ParamSpec`.
```py
def foo2[**P = ...]() -> None:
reveal_type(P) # revealed: typing.ParamSpec
def foo3[**P = [int, str]]() -> None:
reveal_type(P) # revealed: typing.ParamSpec
def foo4[**P, **Q = P]():
reveal_type(P) # revealed: typing.ParamSpec
reveal_type(Q) # revealed: typing.ParamSpec
```
Other values are invalid.
```py
# error: [invalid-paramspec]
def foo[**P = int]() -> None:
pass
```

View File

@@ -321,9 +321,8 @@ from typing import Never
from ty_extensions import ConstraintSet, generic_context
def mentions[T, U]():
# (T@mentions ≤ int) ∧ (U@mentions = list[T@mentions])
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(list[T], U, list[T])
# revealed: ty_extensions.ConstraintSet[((T@mentions ≤ int) ∧ (U@mentions = list[T@mentions]))]
reveal_type(constraints)
# revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = list[int]]
reveal_type(generic_context(mentions).specialize_constrained(constraints))
```
@@ -334,9 +333,8 @@ this case.
```py
def divergent[T, U]():
# (T@divergent = list[U@divergent]) ∧ (U@divergent = list[T@divergent]))
constraints = ConstraintSet.range(list[U], T, list[U]) & ConstraintSet.range(list[T], U, list[T])
# revealed: ty_extensions.ConstraintSet[((T@divergent = list[U@divergent]) ∧ (U@divergent = list[T@divergent]))]
reveal_type(constraints)
# revealed: None
reveal_type(generic_context(divergent).specialize_constrained(constraints))
```

View File

@@ -0,0 +1,660 @@
# Support for Resolving Imports In Workspaces
Python packages have fairly rigid structures that we rely on when resolving imports and merging
namespace packages or stub packages. These rules go out the window when analyzing some random local
python file in some random workspace, and so we need to be more tolerant of situations that wouldn't
fly in a published package, cases where we're not configured as well as we'd like, or cases where
two projects in a monorepo have conflicting definitions (but we want to analyze both at once).
## Invalid Names
While you can't syntactically refer to a module with an invalid name (i.e. one with a `-`, or that
has the same name as a keyword) there are plenty of situations where a module with an invalid name
can be run. For instance `python my-script.py` and `python my-proj/main.py` both work, even though
we might in the course of analyzing the code compute the module name `my-script` or `my-proj.main`.
Also, a sufficiently motivated programmer can technically use `importlib.import_module` which takes
strings and does in fact allow syntactically invalid module names.
### Current File Is Invalid Module Name
Relative and absolute imports should resolve fine in a file that isn't a valid module name.
`my-main.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .mod1 import x
# error: [unresolved-import]
from . import mod2
import mod3
reveal_type(x) # revealed: Unknown
reveal_type(mod2.y) # revealed: Unknown
reveal_type(mod3.z) # revealed: int
```
`mod1.py`:
```py
x: int = 1
```
`mod2.py`:
```py
y: int = 2
```
`mod3.py`:
```py
z: int = 2
```
### Current Directory Is Invalid Module Name
Relative and absolute imports should resolve fine in a dir that isn't a valid module name.
`my-tests/main.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .mod1 import x
# error: [unresolved-import]
from . import mod2
import mod3
reveal_type(x) # revealed: Unknown
reveal_type(mod2.y) # revealed: Unknown
reveal_type(mod3.z) # revealed: int
```
`my-tests/mod1.py`:
```py
x: int = 1
```
`my-tests/mod2.py`:
```py
y: int = 2
```
`mod3.py`:
```py
z: int = 2
```
### Current Directory Is Invalid Package Name
Relative and absolute imports should resolve fine in a dir that isn't a valid package name, even if
it contains an `__init__.py`:
`my-tests/__init__.py`:
```py
```
`my-tests/main.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .mod1 import x
# error: [unresolved-import]
from . import mod2
import mod3
reveal_type(x) # revealed: Unknown
reveal_type(mod2.y) # revealed: Unknown
reveal_type(mod3.z) # revealed: int
```
`my-tests/mod1.py`:
```py
x: int = 1
```
`my-tests/mod2.py`:
```py
y: int = 2
```
`mod3.py`:
```py
z: int = 2
```
## Multiple Projects
It's common for a monorepo to define many separate projects that may or may not depend on eachother
and are stitched together with a package manager like `uv` or `poetry`, often as editables. In this
case, especially when running as an LSP, we want to be able to analyze all of the projects at once,
allowing us to reuse results between projects, without getting confused about things that only make
sense when analyzing the project separately.
The following tests will feature two projects, `a` and `b` where the "real" packages are found under
`src/` subdirectories (and we've been configured to understand that), but each project also contains
other python files in their roots or subdirectories that contains python files which relatively
import eachother and also absolutely import the main package of the project. All of these imports
*should* resolve.
Often the fact that there is both an `a` and `b` project seemingly won't matter, but many possible
solutions will misbehave under these conditions, as e.g. if both define a `main.py` and test code
has `import main`, we need to resolve each project's main as appropriate.
One key hint we will have in these situations is the existence of a `pyproject.toml`, so the
following examples include them in case they help.
### Tests Directory With Overlapping Names
Here we have fairly typical situation where there are two projects `aproj` and `bproj` where the
"real" packages are found under `src/` subdirectories, but each project also contains a `tests/`
directory that contains python files which relatively import eachother and also absolutely import
the package they test. All of these imports *should* resolve.
```toml
[environment]
# This is similar to what we would compute for installed editables
extra-paths = ["aproj/src/", "bproj/src/"]
```
`aproj/tests/test1.py`:
```py
from .setup import x
from . import setup
from a import y
import a
reveal_type(x) # revealed: int
reveal_type(setup.x) # revealed: int
reveal_type(y) # revealed: int
reveal_type(a.y) # revealed: int
```
`aproj/tests/setup.py`:
```py
x: int = 1
```
`aproj/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`aproj/src/a/__init__.py`:
```py
y: int = 10
```
`bproj/tests/test1.py`:
```py
from .setup import x
from . import setup
from b import y
import b
reveal_type(x) # revealed: str
reveal_type(setup.x) # revealed: str
reveal_type(y) # revealed: str
reveal_type(b.y) # revealed: str
```
`bproj/tests/setup.py`:
```py
x: str = "2"
```
`bproj/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`bproj/src/b/__init__.py`:
```py
y: str = "20"
```
### Tests Directory With Ambiguous Project Directories
The same situation as the previous test but instead of the project `a` being in a directory `aproj`
to disambiguate, we now need to avoid getting confused about whether `a/` or `a/src/a/` is the
package `a` while still resolving imports.
```toml
[environment]
# This is similar to what we would compute for installed editables
extra-paths = ["a/src/", "b/src/"]
```
`a/tests/test1.py`:
```py
# TODO: there should be no errors in this file.
# error: [unresolved-import]
from .setup import x
# error: [unresolved-import]
from . import setup
from a import y
import a
reveal_type(x) # revealed: Unknown
reveal_type(setup.x) # revealed: Unknown
reveal_type(y) # revealed: int
reveal_type(a.y) # revealed: int
```
`a/tests/setup.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`a/src/a/__init__.py`:
```py
y: int = 10
```
`b/tests/test1.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .setup import x
# error: [unresolved-import]
from . import setup
from b import y
import b
reveal_type(x) # revealed: Unknown
reveal_type(setup.x) # revealed: Unknown
reveal_type(y) # revealed: str
reveal_type(b.y) # revealed: str
```
`b/tests/setup.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/src/b/__init__.py`:
```py
y: str = "20"
```
### Tests Package With Ambiguous Project Directories
The same situation as the previous test but `tests/__init__.py` is also defined, in case that
complicates the situation.
```toml
[environment]
extra-paths = ["a/src/", "b/src/"]
```
`a/tests/test1.py`:
```py
# TODO: there should be no errors in this file.
# error: [unresolved-import]
from .setup import x
# error: [unresolved-import]
from . import setup
from a import y
import a
reveal_type(x) # revealed: Unknown
reveal_type(setup.x) # revealed: Unknown
reveal_type(y) # revealed: int
reveal_type(a.y) # revealed: int
```
`a/tests/__init__.py`:
```py
```
`a/tests/setup.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`a/src/a/__init__.py`:
```py
y: int = 10
```
`b/tests/test1.py`:
```py
# TODO: there should be no errors in this file
# error: [unresolved-import]
from .setup import x
# error: [unresolved-import]
from . import setup
from b import y
import b
reveal_type(x) # revealed: Unknown
reveal_type(setup.x) # revealed: Unknown
reveal_type(y) # revealed: str
reveal_type(b.y) # revealed: str
```
`b/tests/__init__.py`:
```py
```
`b/tests/setup.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/src/b/__init__.py`:
```py
y: str = "20"
```
### Tests Directory Absolute Importing `main.py`
Here instead of defining packages we have a couple simple applications with a `main.py` and tests
that `import main` and expect that to work.
`a/tests/test1.py`:
```py
# TODO: there should be no errors in this file.
from .setup import x
from . import setup
# error: [unresolved-import]
from main import y
# error: [unresolved-import]
import main
reveal_type(x) # revealed: int
reveal_type(setup.x) # revealed: int
reveal_type(y) # revealed: Unknown
reveal_type(main.y) # revealed: Unknown
```
`a/tests/setup.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`a/main.py`:
```py
y: int = 10
```
`b/tests/test1.py`:
```py
# TODO: there should be no errors in this file
from .setup import x
from . import setup
# error: [unresolved-import]
from main import y
# error: [unresolved-import]
import main
reveal_type(x) # revealed: str
reveal_type(setup.x) # revealed: str
reveal_type(y) # revealed: Unknown
reveal_type(main.y) # revealed: Unknown
```
`b/tests/setup.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/main.py`:
```py
y: str = "20"
```
### Tests Package Absolute Importing `main.py`
The same as the previous case but `tests/__init__.py` exists in case that causes different issues.
`a/tests/test1.py`:
```py
# TODO: there should be no errors in this file.
from .setup import x
from . import setup
# error: [unresolved-import]
from main import y
# error: [unresolved-import]
import main
reveal_type(x) # revealed: int
reveal_type(setup.x) # revealed: int
reveal_type(y) # revealed: Unknown
reveal_type(main.y) # revealed: Unknown
```
`a/tests/__init__.py`:
```py
```
`a/tests/setup.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`a/main.py`:
```py
y: int = 10
```
`b/tests/test1.py`:
```py
# TODO: there should be no errors in this file
from .setup import x
from . import setup
# error: [unresolved-import]
from main import y
# error: [unresolved-import]
import main
reveal_type(x) # revealed: str
reveal_type(setup.x) # revealed: str
reveal_type(y) # revealed: Unknown
reveal_type(main.y) # revealed: Unknown
```
`b/tests/__init__.py`:
```py
```
`b/tests/setup.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/main.py`:
```py
y: str = "20"
```
### `main.py` absolute importing private package
In this case each project has a `main.py` that defines a "private" `utils` package and absolute
imports it.
`a/main.py`:
```py
# TODO: there should be no errors in this file.
# error: [unresolved-import]
from utils import x
# error: [unresolved-import]
import utils
reveal_type(x) # revealed: Unknown
reveal_type(utils.x) # revealed: Unknown
```
`a/utils/__init__.py`:
```py
x: int = 1
```
`a/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```
`b/main.py`:
```py
# TODO: there should be no errors in this file.
# error: [unresolved-import]
from utils import x
# error: [unresolved-import]
import utils
reveal_type(x) # revealed: Unknown
reveal_type(utils.x) # revealed: Unknown
```
`b/utils/__init__.py`:
```py
x: str = "2"
```
`b/pyproject.toml`:
```text
name = "a"
version = "0.1.0"
```

View File

@@ -408,3 +408,205 @@ class Vec2(NamedTuple):
Vec2(0.0, 0.0)
```
## `super()` is not supported in NamedTuple methods
Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime. In Python
3.14+, a `TypeError` is raised; in earlier versions, a confusing `RuntimeError` about
`__classcell__` is raised.
```py
from typing import NamedTuple
class F(NamedTuple):
x: int
def method(self):
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super()
def method_with_args(self):
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super(F, self)
def method_with_different_pivot(self):
# Even passing a different pivot class fails.
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super(tuple, self)
@classmethod
def class_method(cls):
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super()
@staticmethod
def static_method():
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super()
@property
def prop(self):
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
return super()
```
However, classes that **inherit from** a `NamedTuple` class (but don't directly inherit from
`NamedTuple`) can use `super()` normally:
```py
from typing import NamedTuple
class Base(NamedTuple):
x: int
class Child(Base):
def method(self):
super()
```
And regular classes that don't inherit from `NamedTuple` at all can use `super()` as normal:
```py
class Regular:
def method(self):
super() # fine
```
Using `super()` on a `NamedTuple` class also works fine if it occurs outside the class:
```py
from typing import NamedTuple
class F(NamedTuple):
x: int
super(F, F(42)) # fine
```
## NamedTuples cannot have field names starting with underscores
<!-- snapshot-diagnostics -->
```py
from typing import NamedTuple
class Foo(NamedTuple):
# error: [invalid-named-tuple] "NamedTuple field `_bar` cannot start with an underscore"
_bar: int
class Bar(NamedTuple):
x: int
class Baz(Bar):
_whatever: str # `Baz` is not a NamedTuple class, so this is fine
```
## Prohibited NamedTuple attributes
`NamedTuple` classes have certain synthesized attributes that cannot be overwritten. Attempting to
assign to these attributes (without type annotations) will raise an `AttributeError` at runtime.
```py
from typing import NamedTuple
class F(NamedTuple):
x: int
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
_asdict = 42
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_make`"
_make = "foo"
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_replace`"
_replace = lambda self: self
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_fields`"
_fields = ()
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_field_defaults`"
_field_defaults = {}
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `__new__`"
__new__ = None
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `__init__`"
__init__ = None
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `__getnewargs__`"
__getnewargs__ = None
```
However, other attributes (including those starting with underscores) can be assigned without error:
```py
from typing import NamedTuple
class G(NamedTuple):
x: int
# These are fine (not prohibited attributes)
_custom = 42
__custom__ = "ok"
regular_attr = "value"
```
Note that type-annotated attributes become NamedTuple fields, not attribute overrides. They are not
flagged as prohibited attribute overrides (though field names starting with `_` are caught by the
underscore field name check):
```py
from typing import NamedTuple
class H(NamedTuple):
x: int
# This is a field declaration, not an override. It's not flagged as an override,
# but is flagged because field names cannot start with underscores.
# error: [invalid-named-tuple] "NamedTuple field `_asdict` cannot start with an underscore"
_asdict: int = 0
```
The check also applies to assignments within conditional blocks:
```py
from typing import NamedTuple
class I(NamedTuple):
x: int
if True:
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
_asdict = 42
```
Method definitions with prohibited names are also flagged:
```py
from typing import NamedTuple
class J(NamedTuple):
x: int
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
def _asdict(self):
return {}
@classmethod
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_make`"
def _make(cls, iterable):
return cls(*iterable)
```
Classes that inherit from a `NamedTuple` class (but don't directly inherit from `NamedTuple`) are
not subject to these restrictions:
```py
from typing import NamedTuple
class Base(NamedTuple):
x: int
class Child(Base):
# This is fine - Child is not directly a NamedTuple
_asdict = 42
```

View File

@@ -283,8 +283,7 @@ class MyNamedTuple(NamedTuple):
x: int
@override
# TODO: this raises an exception at runtime (which we should emit a diagnostic for).
# It shouldn't be an `invalid-explicit-override` diagnostic, however.
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
def _asdict(self, /) -> dict[str, Any]: ...
class MyNamedTupleParent(NamedTuple):

View File

@@ -0,0 +1,78 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: classes.md - Generic classes: Legacy syntax - Diagnostics for bad specializations
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
---
# Python source files
## library.py
```
1 | from typing import TypeVar, Generic
2 |
3 | T = TypeVar("T", bound=str)
4 | U = TypeVar("U", int, bytes)
5 |
6 | class Bounded(Generic[T]):
7 | x: T
8 |
9 | class Constrained(Generic[U]):
10 | x: U
```
## main.py
```
1 | from library import Bounded, Constrained
2 |
3 | x: Bounded[int] # error: [invalid-type-arguments]
4 | y: Constrained[str] # error: [invalid-type-arguments]
```
# Diagnostics
```
error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` of type variable `T@Bounded`
--> src/main.py:3:12
|
1 | from library import Bounded, Constrained
2 |
3 | x: Bounded[int] # error: [invalid-type-arguments]
| ^^^
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
::: src/library.py:3:1
|
1 | from typing import TypeVar, Generic
2 |
3 | T = TypeVar("T", bound=str)
| - Type variable defined here
4 | U = TypeVar("U", int, bytes)
|
info: rule `invalid-type-arguments` is enabled by default
```
```
error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `bytes` of type variable `U@Constrained`
--> src/main.py:4:16
|
3 | x: Bounded[int] # error: [invalid-type-arguments]
4 | y: Constrained[str] # error: [invalid-type-arguments]
| ^^^
|
::: src/library.py:4:1
|
3 | T = TypeVar("T", bound=str)
4 | U = TypeVar("U", int, bytes)
| - Type variable defined here
5 |
6 | class Bounded(Generic[T]):
|
info: rule `invalid-type-arguments` is enabled by default
```

View File

@@ -0,0 +1,71 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: classes.md - Generic classes: PEP 695 syntax - Diagnostics for bad specializations
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
---
# Python source files
## library.py
```
1 | class Bounded[T: str]:
2 | x: T
3 |
4 | class Constrained[U: (int, bytes)]:
5 | x: U
```
## main.py
```
1 | from library import Bounded, Constrained
2 |
3 | x: Bounded[int] # error: [invalid-type-arguments]
4 | y: Constrained[str] # error: [invalid-type-arguments]
```
# Diagnostics
```
error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` of type variable `T@Bounded`
--> src/main.py:3:12
|
1 | from library import Bounded, Constrained
2 |
3 | x: Bounded[int] # error: [invalid-type-arguments]
| ^^^
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
::: src/library.py:1:15
|
1 | class Bounded[T: str]:
| - Type variable defined here
2 | x: T
|
info: rule `invalid-type-arguments` is enabled by default
```
```
error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `bytes` of type variable `U@Constrained`
--> src/main.py:4:16
|
3 | x: Bounded[int] # error: [invalid-type-arguments]
4 | y: Constrained[str] # error: [invalid-type-arguments]
| ^^^
|
::: src/library.py:4:19
|
2 | x: T
3 |
4 | class Constrained[U: (int, bytes)]:
| - Type variable defined here
5 | x: U
|
info: rule `invalid-type-arguments` is enabled by default
```

View File

@@ -49,26 +49,29 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
35 | def method1(self) -> None: ... # error: [override-of-final-method]
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
39 |
40 | # Possible overrides of possibly `@final` methods...
41 | class C(A):
42 | if coinflip():
43 | # TODO: the autofix here introduces invalid syntax because there are now no
44 | # statements inside the `if:` branch
45 | # (but it might still be a useful autofix in an IDE context?)
46 | def method1(self) -> None: ... # error: [override-of-final-method]
47 | else:
48 | pass
49 |
50 | if coinflip():
51 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
52 | else:
53 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
54 |
55 | if coinflip():
56 | def method3(self) -> None: ... # error: [override-of-final-method]
57 | def method4(self) -> None: ... # error: [override-of-final-method]
38 |
39 | # check that autofixes don't introduce invalid syntax
40 | # if there are multiple statements on one line
41 | #
42 | # TODO: we should emit a Liskov violation here too
43 | # error: [override-of-final-method]
44 | method4 = 42; unrelated = 56 # fmt: skip
45 |
46 | # Possible overrides of possibly `@final` methods...
47 | class C(A):
48 | if coinflip():
49 | def method1(self) -> None: ... # error: [override-of-final-method]
50 | else:
51 | pass
52 |
53 | if coinflip():
54 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
55 | else:
56 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
57 |
58 | if coinflip():
59 | def method3(self) -> None: ... # error: [override-of-final-method]
60 | def method4(self) -> None: ... # error: [override-of-final-method]
```
# Diagnostics
@@ -104,7 +107,7 @@ info: rule `override-of-final-method` is enabled by default
35 + # error: [override-of-final-method]
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
38 |
note: This is an unsafe fix and may change runtime behavior
```
@@ -118,7 +121,6 @@ error[override-of-final-method]: Cannot override `A.method2`
36 | def method2(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Overrides a definition from superclass `A`
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
info: `A.method2` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:16:9
@@ -140,8 +142,8 @@ info: rule `override-of-final-method` is enabled by default
- def method2(self) -> None: ... # error: [override-of-final-method]
36 + # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
39 |
38 |
39 | # check that autofixes don't introduce invalid syntax
note: This is an unsafe fix and may change runtime behavior
```
@@ -154,7 +156,8 @@ error[override-of-final-method]: Cannot override `A.method3`
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Overrides a definition from superclass `A`
38 | def method4(self) -> None: ... # error: [override-of-final-method]
38 |
39 | # check that autofixes don't introduce invalid syntax
|
info: `A.method3` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:20:9
@@ -174,23 +177,23 @@ info: rule `override-of-final-method` is enabled by default
36 | def method2(self) -> None: ... # error: [override-of-final-method]
- def method3(self) -> None: ... # error: [override-of-final-method]
37 + # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
39 |
40 | # Possible overrides of possibly `@final` methods...
38 |
39 | # check that autofixes don't introduce invalid syntax
40 | # if there are multiple statements on one line
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `A.method4`
--> src/mdtest_snippet.py:38:9
--> src/mdtest_snippet.py:44:5
|
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Overrides a definition from superclass `A`
39 |
40 | # Possible overrides of possibly `@final` methods...
42 | # TODO: we should emit a Liskov violation here too
43 | # error: [override-of-final-method]
44 | method4 = 42; unrelated = 56 # fmt: skip
| ^^^^^^^ Overrides a definition from superclass `A`
45 |
46 | # Possible overrides of possibly `@final` methods...
|
info: `A.method4` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:29:9
@@ -206,28 +209,19 @@ info: `A.method4` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `method4`
info: rule `override-of-final-method` is enabled by default
35 | def method1(self) -> None: ... # error: [override-of-final-method]
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
- def method4(self) -> None: ... # error: [override-of-final-method]
38 + # error: [override-of-final-method]
39 |
40 | # Possible overrides of possibly `@final` methods...
41 | class C(A):
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `A.method1`
--> src/mdtest_snippet.py:46:13
--> src/mdtest_snippet.py:49:13
|
44 | # statements inside the `if:` branch
45 | # (but it might still be a useful autofix in an IDE context?)
46 | def method1(self) -> None: ... # error: [override-of-final-method]
47 | class C(A):
48 | if coinflip():
49 | def method1(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Overrides a definition from superclass `A`
47 | else:
48 | pass
50 | else:
51 | pass
|
info: `A.method1` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:8:9
@@ -243,26 +237,17 @@ info: `A.method1` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `method1`
info: rule `override-of-final-method` is enabled by default
43 | # TODO: the autofix here introduces invalid syntax because there are now no
44 | # statements inside the `if:` branch
45 | # (but it might still be a useful autofix in an IDE context?)
- def method1(self) -> None: ... # error: [override-of-final-method]
46 + # error: [override-of-final-method]
47 | else:
48 | pass
49 |
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `A.method3`
--> src/mdtest_snippet.py:56:13
--> src/mdtest_snippet.py:59:13
|
55 | if coinflip():
56 | def method3(self) -> None: ... # error: [override-of-final-method]
58 | if coinflip():
59 | def method3(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Overrides a definition from superclass `A`
57 | def method4(self) -> None: ... # error: [override-of-final-method]
60 | def method4(self) -> None: ... # error: [override-of-final-method]
|
info: `A.method3` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:20:9
@@ -277,23 +262,16 @@ info: `A.method3` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `method3`
info: rule `override-of-final-method` is enabled by default
53 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
54 |
55 | if coinflip():
- def method3(self) -> None: ... # error: [override-of-final-method]
56 + # error: [override-of-final-method]
57 | def method4(self) -> None: ... # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `A.method4`
--> src/mdtest_snippet.py:57:13
--> src/mdtest_snippet.py:60:13
|
55 | if coinflip():
56 | def method3(self) -> None: ... # error: [override-of-final-method]
57 | def method4(self) -> None: ... # error: [override-of-final-method]
58 | if coinflip():
59 | def method3(self) -> None: ... # error: [override-of-final-method]
60 | def method4(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Overrides a definition from superclass `A`
|
info: `A.method4` is decorated with `@final`, forbidding overrides
@@ -310,11 +288,5 @@ info: `A.method4` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `method4`
info: rule `override-of-final-method` is enabled by default
54 |
55 | if coinflip():
56 | def method3(self) -> None: ... # error: [override-of-final-method]
- def method4(self) -> None: ... # error: [override-of-final-method]
57 + # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```

View File

@@ -28,93 +28,93 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
14 | @final
15 | def my_property2(self) -> int: ...
16 |
17 | @final
18 | @classmethod
19 | def class_method1(cls) -> int: ...
17 | @property
18 | @final
19 | def my_property3(self) -> int: ...
20 |
21 | @classmethod
22 | @final
23 | def class_method2(cls) -> int: ...
21 | @final
22 | @classmethod
23 | def class_method1(cls) -> int: ...
24 |
25 | @final
26 | @staticmethod
27 | def static_method1() -> int: ...
25 | @classmethod
26 | @final
27 | def class_method2(cls) -> int: ...
28 |
29 | @staticmethod
30 | @final
31 | def static_method2() -> int: ...
29 | @final
30 | @staticmethod
31 | def static_method1() -> int: ...
32 |
33 | @lossy_decorator
33 | @staticmethod
34 | @final
35 | def decorated_1(self): ...
35 | def static_method2() -> int: ...
36 |
37 | @final
38 | @lossy_decorator
39 | def decorated_2(self): ...
37 | @lossy_decorator
38 | @final
39 | def decorated_1(self): ...
40 |
41 | class Child(Parent):
42 | # explicitly test the concise diagnostic message,
43 | # which is different to the verbose diagnostic summary message:
44 | #
45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
46 | def foo(self): ...
47 | @property
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
49 |
50 | @property
51 | def my_property2(self) -> int: ... # error: [override-of-final-method]
52 |
53 | @classmethod
54 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
55 |
56 | @staticmethod
57 | def static_method1() -> int: ... # error: [override-of-final-method]
41 | @final
42 | @lossy_decorator
43 | def decorated_2(self): ...
44 |
45 | class Child(Parent):
46 | # explicitly test the concise diagnostic message,
47 | # which is different to the verbose diagnostic summary message:
48 | #
49 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
50 | def foo(self): ...
51 | @property
52 | def my_property1(self) -> int: ... # error: [override-of-final-method]
53 |
54 | @property
55 | def my_property2(self) -> int: ... # error: [override-of-final-method]
56 | @my_property2.setter
57 | def my_property2(self, x: int) -> None: ...
58 |
59 | @classmethod
60 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
61 |
62 | @staticmethod
63 | def static_method2() -> int: ... # error: [override-of-final-method]
64 |
65 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
59 | @property
60 | def my_property3(self) -> int: ... # error: [override-of-final-method]
61 | @my_property3.deleter
62 | def my_proeprty3(self) -> None: ...
63 |
64 | @classmethod
65 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
66 |
67 | @lossy_decorator
68 | def decorated_2(self): ... # TODO: should emit [override-of-final-method]
67 | @staticmethod
68 | def static_method1() -> int: ... # error: [override-of-final-method]
69 |
70 | class OtherChild(Parent): ...
71 |
72 | class Grandchild(OtherChild):
70 | @classmethod
71 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
72 |
73 | @staticmethod
74 | # TODO: we should emit a Liskov violation here too
75 | # error: [override-of-final-method]
76 | def foo(): ...
77 | @property
78 | # TODO: we should emit a Liskov violation here too
79 | # error: [override-of-final-method]
80 | def my_property1(self) -> str: ...
81 | # TODO: we should emit a Liskov violation here too
82 | # error: [override-of-final-method]
83 | class_method1 = None
84 |
85 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
86 |
87 | T = TypeVar("T")
88 |
89 | def identity(x: T) -> T: ...
90 |
91 | class Foo:
92 | @final
93 | @identity
94 | @identity
95 | @identity
96 | @identity
97 | @identity
98 | @identity
99 | @identity
100 | @identity
101 | @identity
102 | @identity
103 | @identity
74 | def static_method2() -> int: ... # error: [override-of-final-method]
75 |
76 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
77 |
78 | @lossy_decorator
79 | def decorated_2(self): ... # TODO: should emit [override-of-final-method]
80 |
81 | class OtherChild(Parent): ...
82 |
83 | class Grandchild(OtherChild):
84 | @staticmethod
85 | # TODO: we should emit a Liskov violation here too
86 | # error: [override-of-final-method]
87 | def foo(): ...
88 | @property
89 | # TODO: we should emit a Liskov violation here too
90 | # error: [override-of-final-method]
91 | def my_property1(self) -> str: ...
92 | # TODO: we should emit a Liskov violation here too
93 | # error: [override-of-final-method]
94 | class_method1 = None
95 |
96 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
97 |
98 | T = TypeVar("T")
99 |
100 | def identity(x: T) -> T: ...
101 |
102 | class Foo:
103 | @final
104 | @identity
105 | @identity
106 | @identity
@@ -122,24 +122,35 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
108 | @identity
109 | @identity
110 | @identity
111 | def bar(self): ...
112 |
113 | class Baz(Foo):
114 | def bar(self): ... # error: [override-of-final-method]
111 | @identity
112 | @identity
113 | @identity
114 | @identity
115 | @identity
116 | @identity
117 | @identity
118 | @identity
119 | @identity
120 | @identity
121 | @identity
122 | def bar(self): ...
123 |
124 | class Baz(Foo):
125 | def bar(self): ... # error: [override-of-final-method]
```
# Diagnostics
```
error[override-of-final-method]: Cannot override `Parent.foo`
--> src/mdtest_snippet.pyi:46:9
--> src/mdtest_snippet.pyi:50:9
|
44 | #
45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
46 | def foo(self): ...
48 | #
49 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
50 | def foo(self): ...
| ^^^ Overrides a definition from superclass `Parent`
47 | @property
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
51 | @property
52 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
info: `Parent.foo` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:6:5
@@ -154,28 +165,28 @@ info: `Parent.foo` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `foo`
info: rule `override-of-final-method` is enabled by default
43 | # which is different to the verbose diagnostic summary message:
44 | #
45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
47 | # which is different to the verbose diagnostic summary message:
48 | #
49 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
- def foo(self): ...
46 +
47 | @property
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
49 |
50 +
51 | @property
52 | def my_property1(self) -> int: ... # error: [override-of-final-method]
53 |
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Parent.my_property1`
--> src/mdtest_snippet.pyi:48:9
--> src/mdtest_snippet.pyi:52:9
|
46 | def foo(self): ...
47 | @property
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
50 | def foo(self): ...
51 | @property
52 | def my_property1(self) -> int: ... # error: [override-of-final-method]
| ^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
49 |
50 | @property
53 |
54 | @property
|
info: `Parent.my_property1` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:9:5
@@ -192,28 +203,18 @@ info: `Parent.my_property1` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `my_property1`
info: rule `override-of-final-method` is enabled by default
44 | #
45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
46 | def foo(self): ...
- @property
- def my_property1(self) -> int: ... # error: [override-of-final-method]
47 + # error: [override-of-final-method]
48 |
49 | @property
50 | def my_property2(self) -> int: ... # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Parent.my_property2`
--> src/mdtest_snippet.pyi:51:9
--> src/mdtest_snippet.pyi:55:9
|
50 | @property
51 | def my_property2(self) -> int: ... # error: [override-of-final-method]
54 | @property
55 | def my_property2(self) -> int: ... # error: [override-of-final-method]
| ^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
52 |
53 | @classmethod
56 | @my_property2.setter
57 | def my_property2(self, x: int) -> None: ...
|
info: `Parent.my_property2` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:14:5
@@ -224,181 +225,197 @@ info: `Parent.my_property2` is decorated with `@final`, forbidding overrides
15 | def my_property2(self) -> int: ...
| ------------ `Parent.my_property2` defined here
16 |
17 | @final
17 | @property
|
help: Remove the override of `my_property2`
help: Remove the getter and setter for `my_property2`
info: rule `override-of-final-method` is enabled by default
```
```
error[override-of-final-method]: Cannot override `Parent.my_property3`
--> src/mdtest_snippet.pyi:60:9
|
59 | @property
60 | def my_property3(self) -> int: ... # error: [override-of-final-method]
| ^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
61 | @my_property3.deleter
62 | def my_proeprty3(self) -> None: ...
|
info: `Parent.my_property3` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:18:5
|
17 | @property
18 | @final
| ------
19 | def my_property3(self) -> int: ...
| ------------ `Parent.my_property3` defined here
20 |
21 | @final
|
help: Remove the override of `my_property3`
info: rule `override-of-final-method` is enabled by default
47 | @property
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
49 |
- @property
- def my_property2(self) -> int: ... # error: [override-of-final-method]
50 + # error: [override-of-final-method]
51 |
52 | @classmethod
53 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Parent.class_method1`
--> src/mdtest_snippet.pyi:54:9
--> src/mdtest_snippet.pyi:65:9
|
53 | @classmethod
54 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
64 | @classmethod
65 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
| ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
55 |
56 | @staticmethod
66 |
67 | @staticmethod
|
info: `Parent.class_method1` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:17:5
--> src/mdtest_snippet.pyi:21:5
|
15 | def my_property2(self) -> int: ...
16 |
17 | @final
| ------
18 | @classmethod
19 | def class_method1(cls) -> int: ...
| ------------- `Parent.class_method1` defined here
19 | def my_property3(self) -> int: ...
20 |
21 | @classmethod
21 | @final
| ------
22 | @classmethod
23 | def class_method1(cls) -> int: ...
| ------------- `Parent.class_method1` defined here
24 |
25 | @classmethod
|
help: Remove the override of `class_method1`
info: rule `override-of-final-method` is enabled by default
50 | @property
51 | def my_property2(self) -> int: ... # error: [override-of-final-method]
52 |
61 | @my_property3.deleter
62 | def my_proeprty3(self) -> None: ...
63 |
- @classmethod
- def class_method1(cls) -> int: ... # error: [override-of-final-method]
53 + # error: [override-of-final-method]
54 |
55 | @staticmethod
56 | def static_method1() -> int: ... # error: [override-of-final-method]
64 + # error: [override-of-final-method]
65 |
66 | @staticmethod
67 | def static_method1() -> int: ... # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Parent.static_method1`
--> src/mdtest_snippet.pyi:57:9
--> src/mdtest_snippet.pyi:68:9
|
56 | @staticmethod
57 | def static_method1() -> int: ... # error: [override-of-final-method]
67 | @staticmethod
68 | def static_method1() -> int: ... # error: [override-of-final-method]
| ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
58 |
59 | @classmethod
69 |
70 | @classmethod
|
info: `Parent.static_method1` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:25:5
--> src/mdtest_snippet.pyi:29:5
|
23 | def class_method2(cls) -> int: ...
24 |
25 | @final
| ------
26 | @staticmethod
27 | def static_method1() -> int: ...
| -------------- `Parent.static_method1` defined here
27 | def class_method2(cls) -> int: ...
28 |
29 | @staticmethod
29 | @final
| ------
30 | @staticmethod
31 | def static_method1() -> int: ...
| -------------- `Parent.static_method1` defined here
32 |
33 | @staticmethod
|
help: Remove the override of `static_method1`
info: rule `override-of-final-method` is enabled by default
53 | @classmethod
54 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
55 |
64 | @classmethod
65 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
66 |
- @staticmethod
- def static_method1() -> int: ... # error: [override-of-final-method]
56 + # error: [override-of-final-method]
57 |
58 | @classmethod
59 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
67 + # error: [override-of-final-method]
68 |
69 | @classmethod
70 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Parent.class_method2`
--> src/mdtest_snippet.pyi:60:9
--> src/mdtest_snippet.pyi:71:9
|
59 | @classmethod
60 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
70 | @classmethod
71 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
| ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
61 |
62 | @staticmethod
72 |
73 | @staticmethod
|
info: `Parent.class_method2` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:22:5
--> src/mdtest_snippet.pyi:26:5
|
21 | @classmethod
22 | @final
25 | @classmethod
26 | @final
| ------
23 | def class_method2(cls) -> int: ...
27 | def class_method2(cls) -> int: ...
| ------------- `Parent.class_method2` defined here
24 |
25 | @final
28 |
29 | @final
|
help: Remove the override of `class_method2`
info: rule `override-of-final-method` is enabled by default
56 | @staticmethod
57 | def static_method1() -> int: ... # error: [override-of-final-method]
58 |
67 | @staticmethod
68 | def static_method1() -> int: ... # error: [override-of-final-method]
69 |
- @classmethod
- def class_method2(cls) -> int: ... # error: [override-of-final-method]
59 + # error: [override-of-final-method]
60 |
61 | @staticmethod
62 | def static_method2() -> int: ... # error: [override-of-final-method]
70 + # error: [override-of-final-method]
71 |
72 | @staticmethod
73 | def static_method2() -> int: ... # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Parent.static_method2`
--> src/mdtest_snippet.pyi:63:9
--> src/mdtest_snippet.pyi:74:9
|
62 | @staticmethod
63 | def static_method2() -> int: ... # error: [override-of-final-method]
73 | @staticmethod
74 | def static_method2() -> int: ... # error: [override-of-final-method]
| ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
64 |
65 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
75 |
76 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
|
info: `Parent.static_method2` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:30:5
--> src/mdtest_snippet.pyi:34:5
|
29 | @staticmethod
30 | @final
33 | @staticmethod
34 | @final
| ------
31 | def static_method2() -> int: ...
35 | def static_method2() -> int: ...
| -------------- `Parent.static_method2` defined here
32 |
33 | @lossy_decorator
36 |
37 | @lossy_decorator
|
help: Remove the override of `static_method2`
info: rule `override-of-final-method` is enabled by default
59 | @classmethod
60 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
61 |
70 | @classmethod
71 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
72 |
- @staticmethod
- def static_method2() -> int: ... # error: [override-of-final-method]
62 + # error: [override-of-final-method]
63 |
64 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
65 |
73 + # error: [override-of-final-method]
74 |
75 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
76 |
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Parent.foo`
--> src/mdtest_snippet.pyi:76:9
--> src/mdtest_snippet.pyi:87:9
|
74 | # TODO: we should emit a Liskov violation here too
75 | # error: [override-of-final-method]
76 | def foo(): ...
85 | # TODO: we should emit a Liskov violation here too
86 | # error: [override-of-final-method]
87 | def foo(): ...
| ^^^ Overrides a definition from superclass `Parent`
77 | @property
78 | # TODO: we should emit a Liskov violation here too
88 | @property
89 | # TODO: we should emit a Liskov violation here too
|
info: `Parent.foo` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:6:5
@@ -413,31 +430,31 @@ info: `Parent.foo` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `foo`
info: rule `override-of-final-method` is enabled by default
70 | class OtherChild(Parent): ...
71 |
72 | class Grandchild(OtherChild):
81 | class OtherChild(Parent): ...
82 |
83 | class Grandchild(OtherChild):
- @staticmethod
- # TODO: we should emit a Liskov violation here too
- # error: [override-of-final-method]
- def foo(): ...
73 +
74 | @property
75 | # TODO: we should emit a Liskov violation here too
76 | # error: [override-of-final-method]
84 +
85 | @property
86 | # TODO: we should emit a Liskov violation here too
87 | # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Parent.my_property1`
--> src/mdtest_snippet.pyi:80:9
--> src/mdtest_snippet.pyi:91:9
|
78 | # TODO: we should emit a Liskov violation here too
79 | # error: [override-of-final-method]
80 | def my_property1(self) -> str: ...
89 | # TODO: we should emit a Liskov violation here too
90 | # error: [override-of-final-method]
91 | def my_property1(self) -> str: ...
| ^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
81 | # TODO: we should emit a Liskov violation here too
82 | # error: [override-of-final-method]
92 | # TODO: we should emit a Liskov violation here too
93 | # error: [override-of-final-method]
|
info: `Parent.my_property1` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:9:5
@@ -454,92 +471,71 @@ info: `Parent.my_property1` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `my_property1`
info: rule `override-of-final-method` is enabled by default
74 | # TODO: we should emit a Liskov violation here too
75 | # error: [override-of-final-method]
76 | def foo(): ...
- @property
- # TODO: we should emit a Liskov violation here too
- # error: [override-of-final-method]
- def my_property1(self) -> str: ...
77 +
78 | # TODO: we should emit a Liskov violation here too
79 | # error: [override-of-final-method]
80 | class_method1 = None
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Parent.class_method1`
--> src/mdtest_snippet.pyi:83:5
--> src/mdtest_snippet.pyi:94:5
|
81 | # TODO: we should emit a Liskov violation here too
82 | # error: [override-of-final-method]
83 | class_method1 = None
92 | # TODO: we should emit a Liskov violation here too
93 | # error: [override-of-final-method]
94 | class_method1 = None
| ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
84 |
85 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
95 |
96 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
|
info: `Parent.class_method1` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:17:5
--> src/mdtest_snippet.pyi:21:5
|
15 | def my_property2(self) -> int: ...
16 |
17 | @final
| ------
18 | @classmethod
19 | def class_method1(cls) -> int: ...
| ------------- `Parent.class_method1` defined here
19 | def my_property3(self) -> int: ...
20 |
21 | @classmethod
21 | @final
| ------
22 | @classmethod
23 | def class_method1(cls) -> int: ...
| ------------- `Parent.class_method1` defined here
24 |
25 | @classmethod
|
help: Remove the override of `class_method1`
info: rule `override-of-final-method` is enabled by default
80 | def my_property1(self) -> str: ...
81 | # TODO: we should emit a Liskov violation here too
82 | # error: [override-of-final-method]
- class_method1 = None
83 +
84 |
85 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
86 |
note: This is an unsafe fix and may change runtime behavior
```
```
error[override-of-final-method]: Cannot override `Foo.bar`
--> src/mdtest_snippet.pyi:114:9
--> src/mdtest_snippet.pyi:125:9
|
113 | class Baz(Foo):
114 | def bar(self): ... # error: [override-of-final-method]
124 | class Baz(Foo):
125 | def bar(self): ... # error: [override-of-final-method]
| ^^^ Overrides a definition from superclass `Foo`
|
info: `Foo.bar` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.pyi:92:5
--> src/mdtest_snippet.pyi:103:5
|
91 | class Foo:
92 | @final
102 | class Foo:
103 | @final
| ------
93 | @identity
94 | @identity
104 | @identity
105 | @identity
|
::: src/mdtest_snippet.pyi:111:9
::: src/mdtest_snippet.pyi:122:9
|
109 | @identity
110 | @identity
111 | def bar(self): ...
120 | @identity
121 | @identity
122 | def bar(self): ...
| --- `Foo.bar` defined here
112 |
113 | class Baz(Foo):
123 |
124 | class Baz(Foo):
|
help: Remove the override of `bar`
info: rule `override-of-final-method` is enabled by default
111 | def bar(self): ...
112 |
113 | class Baz(Foo):
122 | def bar(self): ...
123 |
124 | class Baz(Foo):
- def bar(self): ... # error: [override-of-final-method]
114 + # error: [override-of-final-method]
125 + pass # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```

View File

@@ -53,7 +53,7 @@ info: rule `override-of-final-method` is enabled by default
2 |
3 | class Foo(module1.Foo):
- def f(self): ... # error: [override-of-final-method]
4 + # error: [override-of-final-method]
4 + pass # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```

View File

@@ -59,7 +59,7 @@ info: rule `override-of-final-method` is enabled by default
7 | class B(A):
- @final
- def f(self): ... # error: [override-of-final-method]
8 + # error: [override-of-final-method]
8 + pass # error: [override-of-final-method]
9 |
10 | class C(B):
11 | @final
@@ -95,7 +95,7 @@ info: rule `override-of-final-method` is enabled by default
- @final
- # we only emit one error here, not two
- def f(self): ... # error: [override-of-final-method]
12 + # error: [override-of-final-method]
12 + pass # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```

View File

@@ -58,7 +58,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
44 | def bar(self, x: str) -> str: ...
45 | @overload
46 | def bar(self, x: int) -> int: ... # error: [override-of-final-method]
47 |
47 |
48 | @overload
49 | def baz(self, x: str) -> str: ...
50 | @overload
@@ -265,7 +265,7 @@ error[override-of-final-method]: Cannot override `Bad.bar`
45 | @overload
46 | def bar(self, x: int) -> int: ... # error: [override-of-final-method]
| ^^^ Overrides a definition from superclass `Bad`
47 |
47 |
48 | @overload
|
info: `Bad.bar` is decorated with `@final`, forbidding overrides
@@ -287,12 +287,11 @@ info: rule `override-of-final-method` is enabled by default
- def bar(self, x: str) -> str: ...
- @overload
- def bar(self, x: int) -> int: ... # error: [override-of-final-method]
43 |
43 +
44 + # error: [override-of-final-method]
45 +
45 |
46 | @overload
47 | def baz(self, x: str) -> str: ...
48 | @overload
note: This is an unsafe fix and may change runtime behavior
```
@@ -319,7 +318,7 @@ help: Remove all overloads for `baz`
info: rule `override-of-final-method` is enabled by default
45 | @overload
46 | def bar(self, x: int) -> int: ... # error: [override-of-final-method]
47 |
47 |
- @overload
- def baz(self, x: str) -> str: ...
- @overload
@@ -360,12 +359,12 @@ info: rule `override-of-final-method` is enabled by default
- def f(self, x: str) -> str: ...
- @overload
- def f(self, x: int) -> int: ...
13 +
14 +
13 + pass
14 + pass
15 | # error: [override-of-final-method]
- def f(self, x: int | str) -> int | str:
- return x
16 +
16 + pass
17 |
18 | class Bad:
19 | @overload
@@ -459,15 +458,6 @@ info: `Bad.f` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `f`
info: rule `override-of-final-method` is enabled by default
57 |
58 | class ChildOfBad(Bad):
59 | # TODO: these should all cause us to emit Liskov violations as well
- f = None # error: [override-of-final-method]
60 + # error: [override-of-final-method]
61 | g = None # error: [override-of-final-method]
62 | h = None # error: [override-of-final-method]
63 | i = None # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
@@ -493,14 +483,6 @@ info: `Bad.g` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `g`
info: rule `override-of-final-method` is enabled by default
58 | class ChildOfBad(Bad):
59 | # TODO: these should all cause us to emit Liskov violations as well
60 | f = None # error: [override-of-final-method]
- g = None # error: [override-of-final-method]
61 + # error: [override-of-final-method]
62 | h = None # error: [override-of-final-method]
63 | i = None # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
@@ -525,13 +507,6 @@ info: `Bad.h` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `h`
info: rule `override-of-final-method` is enabled by default
59 | # TODO: these should all cause us to emit Liskov violations as well
60 | f = None # error: [override-of-final-method]
61 | g = None # error: [override-of-final-method]
- h = None # error: [override-of-final-method]
62 + # error: [override-of-final-method]
63 | i = None # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```
@@ -555,11 +530,5 @@ info: `Bad.i` is decorated with `@final`, forbidding overrides
|
help: Remove the override of `i`
info: rule `override-of-final-method` is enabled by default
60 | f = None # error: [override-of-final-method]
61 | g = None # error: [override-of-final-method]
62 | h = None # error: [override-of-final-method]
- i = None # error: [override-of-final-method]
63 + # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior
```

View File

@@ -0,0 +1,43 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: named_tuple.md - `NamedTuple` - NamedTuples cannot have field names starting with underscores
mdtest path: crates/ty_python_semantic/resources/mdtest/named_tuple.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import NamedTuple
2 |
3 | class Foo(NamedTuple):
4 | # error: [invalid-named-tuple] "NamedTuple field `_bar` cannot start with an underscore"
5 | _bar: int
6 |
7 | class Bar(NamedTuple):
8 | x: int
9 |
10 | class Baz(Bar):
11 | _whatever: str # `Baz` is not a NamedTuple class, so this is fine
```
# Diagnostics
```
error[invalid-named-tuple]: NamedTuple field name cannot start with an underscore
--> src/mdtest_snippet.py:5:5
|
3 | class Foo(NamedTuple):
4 | # error: [invalid-named-tuple] "NamedTuple field `_bar` cannot start with an underscore"
5 | _bar: int
| ^^^^^^^^^ Class definition will raise `TypeError` at runtime due to this field
6 |
7 | class Bar(NamedTuple):
|
info: rule `invalid-named-tuple` is enabled by default
```

View File

@@ -329,6 +329,16 @@ reveal_type(tuple[int, str]) # revealed: <class 'tuple[int, str]'>
reveal_type(tuple[int, ...]) # revealed: <class 'tuple[int, ...]'>
```
```py
from typing import Any
def _(a: type[tuple], b: type[tuple[int]], c: type[tuple[int, ...]], d: type[tuple[Any, ...]]) -> None:
reveal_type(a) # revealed: type[tuple[Unknown, ...]]
reveal_type(b) # revealed: type[tuple[int]]
reveal_type(c) # revealed: type[tuple[int, ...]]
reveal_type(d) # revealed: type[tuple[Any, ...]]
```
## Inheritance
```toml
@@ -392,7 +402,7 @@ class C(Tuple): ...
reveal_mro(C)
```
### Union subscript access
## Union subscript access
```py
def test(val: tuple[str] | tuple[int]):
@@ -402,7 +412,7 @@ def test2(val: tuple[str, None] | list[int | float]):
reveal_type(val[0]) # revealed: str | int | float
```
### Union subscript access with non-indexable type
## Union subscript access with non-indexable type
```py
def test3(val: tuple[str] | tuple[int] | int):
@@ -410,7 +420,7 @@ def test3(val: tuple[str] | tuple[int] | int):
reveal_type(val[0]) # revealed: str | int | Unknown
```
### Intersection subscript access
## Intersection subscript access
```py
from ty_extensions import Intersection

View File

@@ -85,6 +85,50 @@ a = test \
+ 2 # type: ignore
```
## Interpolated strings
```toml
[environment]
python-version = "3.14"
```
Suppressions for expressions within interpolated strings can be placed after the interpolated string
if it's a single-line interpolation.
```py
a = f"""
{test}
""" # type: ignore
```
For multiline-interpolation, put the ignore comment on the expression's start or end line:
```py
a = f"""
{
10 / # type: ignore
0
}
"""
a = f"""
{
10 /
0 # type: ignore
}
"""
```
But not at the end of the f-string:
```py
a = f"""
{
10 / 0 # error: [division-by-zero]
}
""" # error: [unused-ignore-comment] # type: ignore
```
## Codes
Mypy supports `type: ignore[code]`. ty doesn't understand mypy's rule names. Therefore, ignore the

View File

@@ -398,7 +398,7 @@ the expression `str`:
from ty_extensions import TypeOf, is_subtype_of, static_assert
# This is incorrect and therefore fails with ...
# error: "Static assertion error: argument of type `ty_extensions.ConstraintSet[never]` is statically known to be falsy"
# error: "Static assertion error: argument of type `ty_extensions.ConstraintSet` is statically known to be falsy"
static_assert(is_subtype_of(str, type[str]))
# Correct, returns True:

View File

@@ -123,11 +123,11 @@ class A:
A class `A` is a subtype of `type[T]` if any instance of `A` is a subtype of `T`.
```py
from typing import Callable, Protocol
from typing import Any, Callable, Protocol
from ty_extensions import is_assignable_to, is_subtype_of, is_disjoint_from, static_assert
class IntCallback(Protocol):
def __call__(self, *args, **kwargs) -> int: ...
class Callback[T](Protocol):
def __call__(self, *args, **kwargs) -> T: ...
def _[T](_: T):
static_assert(not is_subtype_of(type[T], T))
@@ -141,8 +141,11 @@ def _[T](_: T):
static_assert(is_assignable_to(type[T], Callable[..., T]))
static_assert(not is_disjoint_from(type[T], Callable[..., T]))
static_assert(not is_assignable_to(type[T], IntCallback))
static_assert(not is_disjoint_from(type[T], IntCallback))
static_assert(is_assignable_to(type[T], Callable[..., T] | Callable[..., Any]))
static_assert(not is_disjoint_from(type[T], Callable[..., T] | Callable[..., Any]))
static_assert(not is_assignable_to(type[T], Callback[int]))
static_assert(not is_disjoint_from(type[T], Callback[int]))
def _[T: int](_: T):
static_assert(not is_subtype_of(type[T], T))
@@ -157,14 +160,23 @@ def _[T: int](_: T):
static_assert(is_subtype_of(type[T], type[int]))
static_assert(not is_disjoint_from(type[T], type[int]))
static_assert(is_subtype_of(type[T], type[int] | None))
static_assert(not is_disjoint_from(type[T], type[int] | None))
static_assert(is_subtype_of(type[T], type[T]))
static_assert(not is_disjoint_from(type[T], type[T]))
static_assert(is_assignable_to(type[T], Callable[..., T]))
static_assert(not is_disjoint_from(type[T], Callable[..., T]))
static_assert(is_assignable_to(type[T], IntCallback))
static_assert(not is_disjoint_from(type[T], IntCallback))
static_assert(is_assignable_to(type[T], Callable[..., T] | Callable[..., Any]))
static_assert(not is_disjoint_from(type[T], Callable[..., T] | Callable[..., Any]))
static_assert(is_assignable_to(type[T], Callback[int]))
static_assert(not is_disjoint_from(type[T], Callback[int]))
static_assert(is_assignable_to(type[T], Callback[int] | Callback[Any]))
static_assert(not is_disjoint_from(type[T], Callback[int] | Callback[Any]))
static_assert(is_subtype_of(type[T], type[T] | None))
static_assert(not is_disjoint_from(type[T], type[T] | None))
@@ -183,8 +195,14 @@ def _[T: (int, str)](_: T):
static_assert(is_assignable_to(type[T], Callable[..., T]))
static_assert(not is_disjoint_from(type[T], Callable[..., T]))
static_assert(not is_assignable_to(type[T], IntCallback))
static_assert(not is_disjoint_from(type[T], IntCallback))
static_assert(is_assignable_to(type[T], Callable[..., T] | Callable[..., Any]))
static_assert(not is_disjoint_from(type[T], Callable[..., T] | Callable[..., Any]))
static_assert(not is_assignable_to(type[T], Callback[int]))
static_assert(not is_disjoint_from(type[T], Callback[int]))
static_assert(is_assignable_to(type[T], Callback[int | str]))
static_assert(not is_disjoint_from(type[T], Callback[int] | Callback[str]))
static_assert(is_subtype_of(type[T], type[T] | None))
static_assert(not is_disjoint_from(type[T], type[T] | None))

View File

@@ -34,7 +34,7 @@ upper bound.
```py
from typing import Any, final, Never, Sequence
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
@@ -44,8 +44,8 @@ class Sub(Base): ...
class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)]
reveal_type(ConstraintSet.range(Sub, T, Super))
# (Sub ≤ T@_ ≤ Super)
ConstraintSet.range(Sub, T, Super)
```
Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower
@@ -53,8 +53,8 @@ bound.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ ≤ Base)]
reveal_type(ConstraintSet.range(Never, T, Base))
# (T@_ ≤ Base)
ConstraintSet.range(Never, T, Base)
```
Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having
@@ -62,8 +62,8 @@ no upper bound.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)]
reveal_type(ConstraintSet.range(Base, T, object))
# (Base ≤ T@_)
ConstraintSet.range(Base, T, object)
```
And a range constraint with a lower bound of `Never` and an upper bound of `object` allows the
@@ -74,8 +74,8 @@ of `object`.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ = *)]
reveal_type(ConstraintSet.range(Never, T, object))
# (T@_ = *)
ConstraintSet.range(Never, T, object)
```
If the lower bound and upper bounds are "inverted" (the upper bound is a subtype of the lower bound)
@@ -83,10 +83,8 @@ or incomparable, then there is no type that can satisfy the constraint.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(ConstraintSet.range(Super, T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(ConstraintSet.range(Base, T, Unrelated))
static_assert(not ConstraintSet.range(Super, T, Sub))
static_assert(not ConstraintSet.range(Base, T, Unrelated))
```
The lower and upper bound can be the same type, in which case the typevar can only be specialized to
@@ -94,8 +92,8 @@ that specific type.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ = Base)]
reveal_type(ConstraintSet.range(Base, T, Base))
# (T@_ = Base)
ConstraintSet.range(Base, T, Base)
```
Constraints can only refer to fully static types, so the lower and upper bounds are transformed into
@@ -103,15 +101,21 @@ their bottom and top materializations, respectively.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)]
reveal_type(ConstraintSet.range(Base, T, Any))
# revealed: ty_extensions.ConstraintSet[(Sequence[Base] ≤ T@_ ≤ Sequence[object])]
reveal_type(ConstraintSet.range(Sequence[Base], T, Sequence[Any]))
constraints = ConstraintSet.range(Base, T, Any)
expected = ConstraintSet.range(Base, T, object)
static_assert(constraints == expected)
# revealed: ty_extensions.ConstraintSet[(T@_ ≤ Base)]
reveal_type(ConstraintSet.range(Any, T, Base))
# revealed: ty_extensions.ConstraintSet[(Sequence[Never] ≤ T@_ ≤ Sequence[Base])]
reveal_type(ConstraintSet.range(Sequence[Any], T, Sequence[Base]))
constraints = ConstraintSet.range(Sequence[Base], T, Sequence[Any])
expected = ConstraintSet.range(Sequence[Base], T, Sequence[object])
static_assert(constraints == expected)
constraints = ConstraintSet.range(Any, T, Base)
expected = ConstraintSet.range(Never, T, Base)
static_assert(constraints == expected)
constraints = ConstraintSet.range(Sequence[Any], T, Sequence[Base])
expected = ConstraintSet.range(Sequence[Never], T, Sequence[Base])
static_assert(constraints == expected)
```
### Negated range
@@ -122,7 +126,7 @@ strict subtype of the lower bound, a strict supertype of the upper bound, or inc
```py
from typing import Any, final, Never, Sequence
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
@@ -132,8 +136,8 @@ class Sub(Base): ...
class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)]
reveal_type(~ConstraintSet.range(Sub, T, Super))
# ¬(Sub ≤ T@_ ≤ Super)
~ConstraintSet.range(Sub, T, Super)
```
Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower
@@ -141,8 +145,8 @@ bound.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)]
reveal_type(~ConstraintSet.range(Never, T, Base))
# ¬(T@_ ≤ Base)
~ConstraintSet.range(Never, T, Base)
```
Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having
@@ -150,8 +154,8 @@ no upper bound.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)]
reveal_type(~ConstraintSet.range(Base, T, object))
# ¬(Base ≤ T@_)
~ConstraintSet.range(Base, T, object)
```
And a negated range constraint with _both_ a lower bound of `Never` and an upper bound of `object`
@@ -159,8 +163,8 @@ cannot be satisfied at all.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ *)]
reveal_type(~ConstraintSet.range(Never, T, object))
# (T@_ ≠ *)
~ConstraintSet.range(Never, T, object)
```
If the lower bound and upper bounds are "inverted" (the upper bound is a subtype of the lower bound)
@@ -168,10 +172,8 @@ or incomparable, then the negated range constraint can always be satisfied.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~ConstraintSet.range(Super, T, Sub))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~ConstraintSet.range(Base, T, Unrelated))
static_assert(~ConstraintSet.range(Super, T, Sub))
static_assert(~ConstraintSet.range(Base, T, Unrelated))
```
The lower and upper bound can be the same type, in which case the typevar can be specialized to any
@@ -179,8 +181,8 @@ type other than that specific type.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ Base)]
reveal_type(~ConstraintSet.range(Base, T, Base))
# (T@_ ≠ Base)
~ConstraintSet.range(Base, T, Base)
```
Constraints can only refer to fully static types, so the lower and upper bounds are transformed into
@@ -188,15 +190,21 @@ their bottom and top materializations, respectively.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)]
reveal_type(~ConstraintSet.range(Base, T, Any))
# revealed: ty_extensions.ConstraintSet[¬(Sequence[Base] ≤ T@_ ≤ Sequence[object])]
reveal_type(~ConstraintSet.range(Sequence[Base], T, Sequence[Any]))
constraints = ~ConstraintSet.range(Base, T, Any)
expected = ~ConstraintSet.range(Base, T, object)
static_assert(constraints == expected)
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)]
reveal_type(~ConstraintSet.range(Any, T, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sequence[Never] ≤ T@_ ≤ Sequence[Base])]
reveal_type(~ConstraintSet.range(Sequence[Any], T, Sequence[Base]))
constraints = ~ConstraintSet.range(Sequence[Base], T, Sequence[Any])
expected = ~ConstraintSet.range(Sequence[Base], T, Sequence[object])
static_assert(constraints == expected)
constraints = ~ConstraintSet.range(Any, T, Base)
expected = ~ConstraintSet.range(Never, T, Base)
static_assert(constraints == expected)
constraints = ~ConstraintSet.range(Sequence[Any], T, Sequence[Base])
expected = ~ConstraintSet.range(Sequence[Never], T, Sequence[Base])
static_assert(constraints == expected)
```
## Intersection
@@ -218,10 +226,10 @@ We cannot simplify the intersection of constraints that refer to different typev
```py
def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[((Sub ≤ T@_ ≤ Base) ∧ (Sub ≤ U@_ ≤ Base))]
reveal_type(ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Sub, U, Base))
# revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ U@_ ≤ Base))]
reveal_type(~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, U, Base))
# (Sub ≤ T@_ ≤ Base) ∧ (Sub ≤ U@_ ≤ Base)
ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Sub, U, Base)
# ¬(Sub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ U@_ ≤ Base)
~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, U, Base)
```
### Intersection of two ranges
@@ -230,7 +238,7 @@ The intersection of two ranges is where the ranges "overlap".
```py
from typing import final
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
@@ -241,24 +249,29 @@ class SubSub(Sub): ...
class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)]
reveal_type(ConstraintSet.range(SubSub, T, Base) & ConstraintSet.range(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)]
reveal_type(ConstraintSet.range(SubSub, T, Super) & ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[(T@_ = Base)]
reveal_type(ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)]
reveal_type(ConstraintSet.range(Sub, T, Super) & ConstraintSet.range(Sub, T, Super))
constraints = ConstraintSet.range(SubSub, T, Base) & ConstraintSet.range(Sub, T, Super)
expected = ConstraintSet.range(Sub, T, Base)
static_assert(constraints == expected)
constraints = ConstraintSet.range(SubSub, T, Super) & ConstraintSet.range(Sub, T, Base)
expected = ConstraintSet.range(Sub, T, Base)
static_assert(constraints == expected)
constraints = ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Base, T, Super)
expected = ConstraintSet.range(Base, T, Base)
static_assert(constraints == expected)
constraints = ConstraintSet.range(Sub, T, Super) & ConstraintSet.range(Sub, T, Super)
expected = ConstraintSet.range(Sub, T, Super)
static_assert(constraints == expected)
```
If they don't overlap, the intersection is empty.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object))
static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Base, T, Super))
static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object))
```
Expanding on this, when intersecting two upper bounds constraints (`(T ≤ Base) ∧ (T ≤ Other)`), we
@@ -267,23 +280,17 @@ satisfy their intersection `T ≤ Base & Other`, and vice versa.
```py
from typing import Never
from ty_extensions import Intersection, static_assert
from ty_extensions import Intersection
# This is not final, so it's possible for a subclass to inherit from both Base and Other.
class Other: ...
def upper_bounds[T]():
# (T@upper_bounds ≤ Base & Other)
intersection_type = ConstraintSet.range(Never, T, Intersection[Base, Other])
# revealed: ty_extensions.ConstraintSet[(T@upper_bounds ≤ Base & Other)]
reveal_type(intersection_type)
# (T@upper_bounds ≤ Base) ∧ (T@upper_bounds ≤ Other)
intersection_constraint = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, T, Other)
# revealed: ty_extensions.ConstraintSet[(T@upper_bounds ≤ Base & Other)]
reveal_type(intersection_constraint)
# The two constraint sets are equivalent; each satisfies the other.
static_assert(intersection_type.satisfies(intersection_constraint))
static_assert(intersection_constraint.satisfies(intersection_type))
static_assert(intersection_type == intersection_constraint)
```
For an intersection of two lower bounds constraints (`(Base ≤ T) ∧ (Other ≤ T)`), we union the lower
@@ -292,17 +299,11 @@ bounds. Any type that satisfies both `Base ≤ T` and `Other ≤ T` must necessa
```py
def lower_bounds[T]():
# (Base | Other ≤ T@lower_bounds)
union_type = ConstraintSet.range(Base | Other, T, object)
# revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@lower_bounds)]
reveal_type(union_type)
# (Base ≤ T@upper_bounds) ∧ (Other ≤ T@upper_bounds)
intersection_constraint = ConstraintSet.range(Base, T, object) & ConstraintSet.range(Other, T, object)
# revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@lower_bounds)]
reveal_type(intersection_constraint)
# The two constraint sets are equivalent; each satisfies the other.
static_assert(union_type.satisfies(intersection_constraint))
static_assert(intersection_constraint.satisfies(union_type))
static_assert(union_type == intersection_constraint)
```
### Intersection of a range and a negated range
@@ -313,7 +314,7 @@ the intersection as removing the hole from the range constraint.
```py
from typing import final, Never
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
@@ -328,10 +329,8 @@ If the negative range completely contains the positive range, then the intersect
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(SubSub, T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, T, Base))
static_assert(not ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(SubSub, T, Super))
static_assert(not ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, T, Base))
```
If the negative range is disjoint from the positive range, the negative range doesn't remove
@@ -339,12 +338,17 @@ anything; the intersection is the positive range.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)]
reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Never, T, Unrelated))
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub)]
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super)]
reveal_type(ConstraintSet.range(Base, T, Super) & ~ConstraintSet.range(SubSub, T, Sub))
constraints = ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Never, T, Unrelated)
expected = ConstraintSet.range(Sub, T, Base)
static_assert(constraints == expected)
constraints = ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super)
expected = ConstraintSet.range(SubSub, T, Sub)
static_assert(constraints == expected)
constraints = ConstraintSet.range(Base, T, Super) & ~ConstraintSet.range(SubSub, T, Sub)
expected = ConstraintSet.range(Base, T, Super)
static_assert(constraints == expected)
```
Otherwise we clip the negative constraint to the mininum range that overlaps with the positive
@@ -352,10 +356,9 @@ range.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ T@_ ≤ Base))]
reveal_type(ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))]
reveal_type(ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base))
constraints = ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super)
expected = ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Base)
static_assert(constraints == expected)
```
### Intersection of two negated ranges
@@ -365,7 +368,7 @@ smaller constraint. For negated ranges, the smaller constraint is the one with t
```py
from typing import final
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
@@ -376,22 +379,25 @@ class SubSub(Sub): ...
class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Super)]
reveal_type(~ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)]
reveal_type(~ConstraintSet.range(Sub, T, Super) & ~ConstraintSet.range(Sub, T, Super))
constraints = ~ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base)
expected = ~ConstraintSet.range(SubSub, T, Super)
static_assert(constraints == expected)
constraints = ~ConstraintSet.range(Sub, T, Super) & ~ConstraintSet.range(Sub, T, Super)
expected = ~ConstraintSet.range(Sub, T, Super)
static_assert(constraints == expected)
```
Otherwise, the intersection cannot be simplified.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))]
reveal_type(~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub))]
reveal_type(~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_))]
reveal_type(~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Unrelated, T, object))
# ¬(Base ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))
~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Base, T, Super)
# ¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub))
~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super)
# ¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_)
~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Unrelated, T, object)
```
In particular, the following does not simplify, even though it seems like it could simplify to
@@ -408,8 +414,8 @@ way.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Base))]
reveal_type(~ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super))
# (¬(Sub ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Base))
~ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super)
```
## Union
@@ -431,10 +437,10 @@ We cannot simplify the union of constraints that refer to different typevars.
```py
def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) (Sub ≤ U@_ ≤ Base)]
reveal_type(ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, U, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base) ¬(Sub ≤ U@_ ≤ Base)]
reveal_type(~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Sub, U, Base))
# (Sub ≤ T@_ ≤ Base) (Sub ≤ U@_ ≤ Base)
ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, U, Base)
# ¬(Sub ≤ T@_ ≤ Base) ¬(Sub ≤ U@_ ≤ Base)
~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Sub, U, Base)
```
### Union of two ranges
@@ -444,7 +450,7 @@ bounds.
```py
from typing import final
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
@@ -455,22 +461,25 @@ class SubSub(Sub): ...
class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Super)]
reveal_type(ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)]
reveal_type(ConstraintSet.range(Sub, T, Super) | ConstraintSet.range(Sub, T, Super))
constraints = ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base)
expected = ConstraintSet.range(SubSub, T, Super)
static_assert(constraints == expected)
constraints = ConstraintSet.range(Sub, T, Super) | ConstraintSet.range(Sub, T, Super)
expected = ConstraintSet.range(Sub, T, Super)
static_assert(constraints == expected)
```
Otherwise, the union cannot be simplified.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) (Sub ≤ T@_ ≤ Base)]
reveal_type(ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) (SubSub ≤ T@_ ≤ Sub)]
reveal_type(ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub) (Unrelated ≤ T@_)]
reveal_type(ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Unrelated, T, object))
# (Base ≤ T@_ ≤ Super) (Sub ≤ T@_ ≤ Base)
ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Base, T, Super)
# (Base ≤ T@_ ≤ Super) (SubSub ≤ T@_ ≤ Sub)
ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super)
# (SubSub ≤ T@_ ≤ Sub) (Unrelated ≤ T@_)
ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Unrelated, T, object)
```
In particular, the following does not simplify, even though it seems like it could simplify to
@@ -485,8 +494,8 @@ not include `Sub`. That means it should not be in the union. Since that type _is
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super) (SubSub ≤ T@_ ≤ Base)]
reveal_type(ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super))
# (Sub ≤ T@_ ≤ Super) (SubSub ≤ T@_ ≤ Base)
ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super)
```
The union of two upper bound constraints (`(T ≤ Base) (T ≤ Other)`) is different than the single
@@ -496,24 +505,18 @@ that satisfies the union constraint satisfies the union type.
```py
from typing import Never
from ty_extensions import static_assert
# This is not final, so it's possible for a subclass to inherit from both Base and Other.
class Other: ...
def union[T]():
# (T@union ≤ Base | Other)
union_type = ConstraintSet.range(Never, T, Base | Other)
# revealed: ty_extensions.ConstraintSet[(T@union ≤ Base | Other)]
reveal_type(union_type)
# (T@union ≤ Base) (T@union ≤ Other)
union_constraint = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Other)
# revealed: ty_extensions.ConstraintSet[(T@union ≤ Base) (T@union ≤ Other)]
reveal_type(union_constraint)
# (T = Base | Other) satisfies (T ≤ Base | Other) but not (T ≤ Base T ≤ Other)
specialization = ConstraintSet.range(Base | Other, T, Base | Other)
# revealed: ty_extensions.ConstraintSet[(T@union = Base | Other)]
reveal_type(specialization)
static_assert(specialization.satisfies(union_type))
static_assert(not specialization.satisfies(union_constraint))
@@ -528,18 +531,13 @@ satisfies the union constraint (`(Base ≤ T) (Other ≤ T)`) but not the un
```py
def union[T]():
# (Base | Other ≤ T@union)
union_type = ConstraintSet.range(Base | Other, T, object)
# revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@union)]
reveal_type(union_type)
# (Base ≤ T@union) (Other ≤ T@union)
union_constraint = ConstraintSet.range(Base, T, object) | ConstraintSet.range(Other, T, object)
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@union) (Other ≤ T@union)]
reveal_type(union_constraint)
# (T = Base) satisfies (Base ≤ T Other ≤ T) but not (Base | Other ≤ T)
specialization = ConstraintSet.range(Base, T, Base)
# revealed: ty_extensions.ConstraintSet[(T@union = Base)]
reveal_type(specialization)
static_assert(not specialization.satisfies(union_type))
static_assert(specialization.satisfies(union_constraint))
@@ -556,7 +554,7 @@ the union as filling part of the hole with the types from the range constraint.
```py
from typing import final, Never
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
@@ -571,10 +569,8 @@ If the positive range completely contains the negative range, then the union is
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(SubSub, T, Super))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, T, Base))
static_assert(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(SubSub, T, Super))
static_assert(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, T, Base))
```
If the negative range is disjoint from the positive range, the positive range doesn't add anything;
@@ -582,12 +578,17 @@ the union is the negative range.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Never, T, Unrelated))
# revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Sub)]
reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_ ≤ Super)]
reveal_type(~ConstraintSet.range(Base, T, Super) | ConstraintSet.range(SubSub, T, Sub))
constraints = ~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Never, T, Unrelated)
expected = ~ConstraintSet.range(Sub, T, Base)
static_assert(constraints == expected)
constraints = ~ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super)
expected = ~ConstraintSet.range(SubSub, T, Sub)
static_assert(constraints == expected)
constraints = ~ConstraintSet.range(Base, T, Super) | ConstraintSet.range(SubSub, T, Sub)
expected = ~ConstraintSet.range(Base, T, Super)
static_assert(constraints == expected)
```
Otherwise we clip the positive constraint to the mininum range that overlaps with the negative
@@ -595,10 +596,9 @@ range.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ¬(SubSub ≤ T@_ ≤ Base)]
reveal_type(~ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ¬(SubSub ≤ T@_ ≤ Super)]
reveal_type(~ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base))
constraints = ~ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super)
expected = ~ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Base)
static_assert(constraints == expected)
```
### Union of two negated ranges
@@ -607,7 +607,7 @@ The union of two negated ranges has a hole where the ranges "overlap".
```py
from typing import final
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
@@ -618,24 +618,29 @@ class SubSub(Sub): ...
class Unrelated: ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
reveal_type(~ConstraintSet.range(SubSub, T, Base) | ~ConstraintSet.range(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
reveal_type(~ConstraintSet.range(SubSub, T, Super) | ~ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ Base)]
reveal_type(~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)]
reveal_type(~ConstraintSet.range(Sub, T, Super) | ~ConstraintSet.range(Sub, T, Super))
constraints = ~ConstraintSet.range(SubSub, T, Base) | ~ConstraintSet.range(Sub, T, Super)
expected = ~ConstraintSet.range(Sub, T, Base)
static_assert(constraints == expected)
constraints = ~ConstraintSet.range(SubSub, T, Super) | ~ConstraintSet.range(Sub, T, Base)
expected = ~ConstraintSet.range(Sub, T, Base)
static_assert(constraints == expected)
constraints = ~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Base, T, Super)
expected = ~ConstraintSet.range(Base, T, Base)
static_assert(constraints == expected)
constraints = ~ConstraintSet.range(Sub, T, Super) | ~ConstraintSet.range(Sub, T, Super)
expected = ~ConstraintSet.range(Sub, T, Super)
static_assert(constraints == expected)
```
If the holes don't overlap, the union is always satisfied.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Unrelated, T, object))
static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Base, T, Super))
static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Unrelated, T, object))
```
## Negation
@@ -644,21 +649,21 @@ def _[T]() -> None:
```py
from typing import Never
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
class Sub(Base): ...
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
reveal_type(~ConstraintSet.range(Sub, T, Base))
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)]
reveal_type(~ConstraintSet.range(Never, T, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_)]
reveal_type(~ConstraintSet.range(Sub, T, object))
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ *)]
reveal_type(~ConstraintSet.range(Never, T, object))
# ¬(Sub ≤ T@_ ≤ Base)
~ConstraintSet.range(Sub, T, Base)
# ¬(T@_ ≤ Base)
~ConstraintSet.range(Never, T, Base)
# ¬(Sub ≤ T@_)
~ConstraintSet.range(Sub, T, object)
# (T@_ ≠ *)
~ConstraintSet.range(Never, T, object)
```
The union of a range constraint and its negation should always be satisfiable.
@@ -666,15 +671,14 @@ The union of a range constraint and its negation should always be satisfiable.
```py
def _[T]() -> None:
constraint = ConstraintSet.range(Sub, T, Base)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(constraint | ~constraint)
static_assert(constraint | ~constraint)
```
### Negation of constraints involving two variables
```py
from typing import final, Never
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
class Base: ...
@@ -682,8 +686,8 @@ class Base: ...
class Unrelated: ...
def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base) ¬(U@_ ≤ Base)]
reveal_type(~(ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base)))
# ¬(T@_ ≤ Base) ¬(U@_ ≤ Base)
~(ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base))
```
The union of a constraint and its negation should always be satisfiable.
@@ -691,150 +695,91 @@ The union of a constraint and its negation should always be satisfiable.
```py
def _[T, U]() -> None:
c1 = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(c1 | ~c1)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~c1 | c1)
static_assert(c1 | ~c1)
static_assert(~c1 | c1)
c2 = ConstraintSet.range(Unrelated, T, object) & ConstraintSet.range(Unrelated, U, object)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(c2 | ~c2)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~c2 | c2)
static_assert(c2 | ~c2)
static_assert(~c2 | c2)
union = c1 | c2
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(union | ~union)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~union | union)
static_assert(union | ~union)
static_assert(~union | union)
```
## Typevar ordering
Constraints can relate two typevars — i.e., `S ≤ T`. We could encode that in one of two ways:
`Never ≤ S ≤ T` or `S ≤ T ≤ object`. In other words, we can decide whether `S` or `T` is the typevar
being constrained. The other is then the lower or upper bound of the constraint.
To handle this, we enforce an arbitrary ordering on typevars, and always place the constraint on the
"earlier" typevar. For the example above, that does not change how the constraint is displayed,
since we always hide `Never` lower bounds and `object` upper bounds.
being constrained. The other is then the lower or upper bound of the constraint. To handle this, we
enforce an arbitrary ordering on typevars, and always place the constraint on the "earlier" typevar.
```py
from typing import Never
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
def f[S, T]():
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
reveal_type(ConstraintSet.range(Never, S, T))
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
reveal_type(ConstraintSet.range(S, T, object))
# (S@f ≤ T@f)
c1 = ConstraintSet.range(Never, S, T)
c2 = ConstraintSet.range(S, T, object)
static_assert(c1 == c2)
def f[T, S]():
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
reveal_type(ConstraintSet.range(Never, S, T))
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
reveal_type(ConstraintSet.range(S, T, object))
# (S@f ≤ T@f)
c1 = ConstraintSet.range(Never, S, T)
c2 = ConstraintSet.range(S, T, object)
static_assert(c1 == c2)
```
Equivalence constraints are similar; internally we arbitrarily choose the "earlier" typevar to be
the constraint, and the other the bound. But we display the result the same way no matter what.
the constraint, and the other the bound.
```py
def f[S, T]():
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
reveal_type(ConstraintSet.range(T, S, T))
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
reveal_type(ConstraintSet.range(S, T, S))
# (S@f = T@f)
c1 = ConstraintSet.range(T, S, T)
c2 = ConstraintSet.range(S, T, S)
static_assert(c1 == c2)
def f[T, S]():
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
reveal_type(ConstraintSet.range(T, S, T))
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
reveal_type(ConstraintSet.range(S, T, S))
# (S@f = T@f)
c1 = ConstraintSet.range(T, S, T)
c2 = ConstraintSet.range(S, T, S)
static_assert(c1 == c2)
```
But in the case of `S ≤ T ≤ U`, we end up with an ambiguity. Depending on the typevar ordering, that
might display as `S ≤ T ≤ U`, or as `(S ≤ T) ∧ (T ≤ U)`.
might represented internally as `S ≤ T ≤ U`, or as `(S ≤ T) ∧ (T ≤ U)`. However, this should not
affect any uses of the constraint set.
```py
def f[S, T, U]():
# Could be either of:
# ty_extensions.ConstraintSet[(S@f ≤ T@f ≤ U@f)]
# ty_extensions.ConstraintSet[(S@f ≤ T@f) ∧ (T@f ≤ U@f)]
# reveal_type(ConstraintSet.range(S, T, U))
# (S@f ≤ T@f ≤ U@f)
# (S@f ≤ T@f) ∧ (T@f ≤ U@f)
ConstraintSet.range(S, T, U)
...
```
## Other simplifications
### Displaying constraint sets
### Ordering of intersection and union elements
When displaying a constraint set, we transform the internal BDD representation into a DNF formula
(i.e., the logical OR of several clauses, each of which is the logical AND of several constraints).
This section contains several examples that show that we simplify the DNF formula as much as we can
before displaying it.
```py
from ty_extensions import ConstraintSet
def f[T, U]():
t1 = ConstraintSet.range(str, T, str)
t2 = ConstraintSet.range(bool, T, bool)
u1 = ConstraintSet.range(str, U, str)
u2 = ConstraintSet.range(bool, U, bool)
# revealed: ty_extensions.ConstraintSet[(T@f = bool) (T@f = str)]
reveal_type(t1 | t2)
# revealed: ty_extensions.ConstraintSet[(U@f = bool) (U@f = str)]
reveal_type(u1 | u2)
# revealed: ty_extensions.ConstraintSet[((T@f = bool) ∧ (U@f = bool)) ((T@f = bool) ∧ (U@f = str)) ((T@f = str) ∧ (U@f = bool)) ((T@f = str) ∧ (U@f = str))]
reveal_type((t1 | t2) & (u1 | u2))
```
We might simplify a BDD so much that we can no longer see the constraints that we used to construct
it!
The ordering of elements in a union or intersection do not affect what types satisfy a constraint
set.
```py
from typing import Never
from ty_extensions import static_assert
from ty_extensions import ConstraintSet, Intersection, static_assert
def f[T]():
t_int = ConstraintSet.range(Never, T, int)
t_bool = ConstraintSet.range(Never, T, bool)
c1 = ConstraintSet.range(Never, T, str | int)
c2 = ConstraintSet.range(Never, T, int | str)
static_assert(c1 == c2)
# `T ≤ bool` implies `T ≤ int`: if a type satisfies the former, it must always satisfy the
# latter. We can turn that into a constraint set, using the equivalence `p → q == ¬p q`:
implication = ~t_bool | t_int
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(implication)
static_assert(implication)
# However, because of that implication, some inputs aren't valid: it's not possible for
# `T ≤ bool` to be true and `T ≤ int` to be false. This is reflected in the constraint set's
# "domain", which maps valid inputs to `true` and invalid inputs to `false`. This means that two
# constraint sets that are both always satisfied will not be identical if they have different
# domains!
always = ConstraintSet.always()
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(always)
static_assert(always)
static_assert(implication != always)
```
### Normalized bounds
The lower and upper bounds of a constraint are normalized, so that we equate unions and
intersections whose elements appear in different orders.
```py
from typing import Never
from ty_extensions import ConstraintSet
def f[T]():
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
reveal_type(ConstraintSet.range(Never, T, str | int))
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
reveal_type(ConstraintSet.range(Never, T, int | str))
c1 = ConstraintSet.range(Never, T, Intersection[str, int])
c2 = ConstraintSet.range(Never, T, Intersection[int, str])
static_assert(c1 == c2)
```
### Constraints on the same typevar
@@ -846,15 +791,20 @@ static types.)
```py
from typing import Never
from ty_extensions import ConstraintSet
from ty_extensions import ConstraintSet, static_assert
def same_typevar[T]():
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(Never, T, T))
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(T, T, object))
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(T, T, T))
constraints = ConstraintSet.range(Never, T, T)
expected = ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
constraints = ConstraintSet.range(T, T, object)
expected = ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
constraints = ConstraintSet.range(T, T, T)
expected = ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
```
This is also true when the typevar appears in a union in the upper bound, or in an intersection in
@@ -865,12 +815,17 @@ as shown above.)
from ty_extensions import Intersection
def same_typevar[T]():
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(Never, T, T | None))
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(Intersection[T, None], T, object))
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(Intersection[T, None], T, T | None))
constraints = ConstraintSet.range(Never, T, T | None)
expected = ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
constraints = ConstraintSet.range(Intersection[T, None], T, object)
expected = ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
constraints = ConstraintSet.range(Intersection[T, None], T, T | None)
expected = ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
```
Similarly, if the lower bound is an intersection containing the _negation_ of the typevar, then the
@@ -880,8 +835,11 @@ constraint set can never be satisfied, since every type is disjoint with its neg
from ty_extensions import Not
def same_typevar[T]():
# revealed: ty_extensions.ConstraintSet[(T@same_typevar ≠ *)]
reveal_type(ConstraintSet.range(Intersection[Not[T], None], T, object))
# revealed: ty_extensions.ConstraintSet[(T@same_typevar ≠ *)]
reveal_type(ConstraintSet.range(Not[T], T, object))
constraints = ConstraintSet.range(Intersection[Not[T], None], T, object)
expected = ~ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
constraints = ConstraintSet.range(Not[T], T, object)
expected = ~ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
```

View File

@@ -50,27 +50,37 @@ question when considering a typevar, by translating the desired relationship int
```py
from typing import Any
from ty_extensions import is_assignable_to, is_subtype_of
from ty_extensions import ConstraintSet, is_assignable_to, is_subtype_of, static_assert
def assignability[T]():
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ bool]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, bool))
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ int]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, int))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, object))
constraints = is_assignable_to(T, bool)
# TODO: expected = ConstraintSet.range(Never, T, bool)
expected = ConstraintSet.never()
static_assert(constraints == expected)
constraints = is_assignable_to(T, int)
# TODO: expected = ConstraintSet.range(Never, T, int)
expected = ConstraintSet.never()
static_assert(constraints == expected)
constraints = is_assignable_to(T, object)
expected = ConstraintSet.always()
static_assert(constraints == expected)
def subtyping[T]():
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ bool]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, bool))
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ int]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, int))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, object))
constraints = is_subtype_of(T, bool)
# TODO: expected = ConstraintSet.range(Never, T, bool)
expected = ConstraintSet.never()
static_assert(constraints == expected)
constraints = is_subtype_of(T, int)
# TODO: expected = ConstraintSet.range(Never, T, int)
expected = ConstraintSet.never()
static_assert(constraints == expected)
constraints = is_subtype_of(T, object)
expected = ConstraintSet.always()
static_assert(constraints == expected)
```
When checking assignability with a dynamic type, we use the bottom and top materializations of the
@@ -88,50 +98,64 @@ class Contravariant[T]:
pass
def assignability[T]():
# aka [T@assignability ≤ object], which is always satisfiable
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
constraints = is_assignable_to(T, Any)
expected = ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
# aka [Never ≤ T@assignability], which is always satisfiable
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
constraints = is_assignable_to(Any, T)
expected = ConstraintSet.range(Never, T, object)
static_assert(constraints == expected)
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Covariant[object]]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Covariant[Any]))
# TODO: revealed: ty_extensions.ConstraintSet[Covariant[Never] ≤ T@assignability]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Covariant[Any], T))
constraints = is_assignable_to(T, Covariant[Any])
# TODO: expected = ConstraintSet.range(Never, T, Covariant[object])
expected = ConstraintSet.never()
static_assert(constraints == expected)
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Contravariant[Never]]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Contravariant[Any]))
# TODO: revealed: ty_extensions.ConstraintSet[Contravariant[object] ≤ T@assignability]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Contravariant[Any], T))
constraints = is_assignable_to(Covariant[Any], T)
# TODO: expected = ConstraintSet.range(Covariant[Never], T, object)
expected = ConstraintSet.never()
static_assert(constraints == expected)
constraints = is_assignable_to(T, Contravariant[Any])
# TODO: expected = ConstraintSet.range(Never, T, Contravariant[Never])
expected = ConstraintSet.never()
static_assert(constraints == expected)
constraints = is_assignable_to(Contravariant[Any], T)
# TODO: expected = ConstraintSet.range(Contravariant[object], T, object)
expected = ConstraintSet.never()
static_assert(constraints == expected)
def subtyping[T]():
# aka [T@assignability ≤ object], which is always satisfiable
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
constraints = is_subtype_of(T, Any)
# TODO: expected = ConstraintSet.range(Never, T, Never)
expected = ConstraintSet.never()
static_assert(constraints == expected)
# aka [Never ≤ T@assignability], which is always satisfiable
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
constraints = is_subtype_of(Any, T)
# TODO: expected = ConstraintSet.range(object, T, object)
expected = ConstraintSet.never()
static_assert(constraints == expected)
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Covariant[Never]]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Covariant[Any]))
# TODO: revealed: ty_extensions.ConstraintSet[Covariant[object] ≤ T@subtyping]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Covariant[Any], T))
constraints = is_subtype_of(T, Covariant[Any])
# TODO: expected = ConstraintSet.range(Never, T, Covariant[Never])
expected = ConstraintSet.never()
static_assert(constraints == expected)
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Contravariant[object]]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Contravariant[Any]))
# TODO: revealed: ty_extensions.ConstraintSet[Contravariant[Never] ≤ T@subtyping]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Contravariant[Any], T))
constraints = is_subtype_of(Covariant[Any], T)
# TODO: expected = ConstraintSet.range(Covariant[object], T, object)
expected = ConstraintSet.never()
static_assert(constraints == expected)
constraints = is_subtype_of(T, Contravariant[Any])
# TODO: expected = ConstraintSet.range(Never, T, Contravariant[object])
expected = ConstraintSet.never()
static_assert(constraints == expected)
constraints = is_subtype_of(Contravariant[Any], T)
# TODO: expected = ConstraintSet.range(Contravariant[Never], T, object)
expected = ConstraintSet.never()
static_assert(constraints == expected)
```
At some point, though, we need to resolve a constraint set; at that point, we can no longer punt on

View File

@@ -14,6 +14,7 @@ altair
antidote
anyio
apprise
archinstall
artigraph
arviz
async-utils
@@ -25,7 +26,9 @@ bidict
black
bokeh
boostedblob
build
check-jsonschema
cibuildwheel
cki-lib
cloud-init
colour
@@ -104,6 +107,7 @@ pylox
pyodide
pyp
pyppeteer
pyproject-metadata
pytest
pytest-robotframework
python-chess
@@ -118,6 +122,7 @@ schemathesis
scikit-build-core
scikit-learn
scipy
scipy-stubs
scrapy
setuptools
sockeye

View File

@@ -13,7 +13,8 @@ pub use diagnostic::add_inferred_python_version_hint_to_diagnostic;
pub use module_name::{ModuleName, ModuleNameResolutionError};
pub use module_resolver::{
KnownModule, Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules,
list_modules, resolve_module, resolve_real_module, system_module_search_paths,
list_modules, resolve_module, resolve_real_module, resolve_real_shadowable_module,
system_module_search_paths,
};
pub use program::{
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
@@ -25,6 +26,7 @@ pub use semantic_model::{
Completion, HasDefinition, HasType, MemberDefinition, NameKind, SemanticModel,
};
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
pub use suppression::create_suppression_fix;
pub use types::DisplaySettings;
pub use types::ide_support::{
ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, definitions_for_bin_op,

View File

@@ -8,9 +8,7 @@ use crate::program::Program;
use super::module::{Module, ModuleKind};
use super::path::{ModulePath, SearchPath, SystemOrVendoredPathRef};
use super::resolver::{
ModuleResolveMode, ResolverContext, is_non_shadowable, resolve_file_module, search_paths,
};
use super::resolver::{ModuleResolveMode, ResolverContext, resolve_file_module, search_paths};
/// List all available modules, including all sub-modules, sorted in lexicographic order.
pub fn all_modules(db: &dyn Db) -> Vec<Module<'_>> {
@@ -309,7 +307,8 @@ impl<'db> Lister<'db> {
/// Returns true if the given module name cannot be shadowable.
fn is_non_shadowable(&self, name: &ModuleName) -> bool {
is_non_shadowable(self.python_version().minor, name.as_str())
ModuleResolveMode::StubsAllowed
.is_non_shadowable(self.python_version().minor, name.as_str())
}
/// Returns the Python version we want to perform module resolution

View File

@@ -6,7 +6,7 @@ pub use module::Module;
pub use path::{SearchPath, SearchPathValidationError};
pub use resolver::SearchPaths;
pub(crate) use resolver::file_to_module;
pub use resolver::{resolve_module, resolve_real_module};
pub use resolver::{resolve_module, resolve_real_module, resolve_real_shadowable_module};
use ruff_db::system::SystemPath;
use crate::Db;

View File

@@ -47,8 +47,33 @@ pub fn resolve_real_module<'db>(db: &'db dyn Db, module_name: &ModuleName) -> Op
resolve_module_query(db, interned_name)
}
/// Resolves a module name to a module (stubs not allowed, some shadowing is
/// allowed).
///
/// In particular, this allows `typing_extensions` to be shadowed by a
/// non-standard library module. This is useful in the context of the LSP
/// where we don't want to pretend as if these modules are always available at
/// runtime.
///
/// This should generally only be used within the context of the LSP. Using it
/// within ty proper risks being unable to resolve builtin modules since they
/// are involved in an import cycle with `builtins`.
pub fn resolve_real_shadowable_module<'db>(
db: &'db dyn Db,
module_name: &ModuleName,
) -> Option<Module<'db>> {
let interned_name = ModuleNameIngredient::new(
db,
module_name,
ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed,
);
resolve_module_query(db, interned_name)
}
/// Which files should be visible when doing a module query
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum ModuleResolveMode {
/// Stubs are allowed to appear.
///
@@ -61,6 +86,13 @@ pub(crate) enum ModuleResolveMode {
/// implementations. When querying searchpaths this also notably replaces typeshed with
/// the "real" stdlib.
StubsNotAllowed,
/// Like `StubsNotAllowed`, but permits some modules to be shadowed.
///
/// In particular, this allows `typing_extensions` to be shadowed by a
/// non-standard library module. This is useful in the context of the LSP
/// where we don't want to pretend as if these modules are always available
/// at runtime.
StubsNotAllowedSomeShadowingAllowed,
}
#[salsa::interned(heap_size=ruff_memory_usage::heap_size)]
@@ -73,6 +105,39 @@ impl ModuleResolveMode {
fn stubs_allowed(self) -> bool {
matches!(self, Self::StubsAllowed)
}
/// 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. Additionally, certain other modules that are involved in an
/// import cycle with `builtins` (`types`, `typing_extensions`, etc.) are
/// also considered non-shadowable, unless the module resolution mode
/// specifically opts into allowing some of them to be shadowed. This
/// latter set of modules cannot be allowed to be shadowed by first-party
/// or "extra-path" modules in ty proper, or we risk panics in unexpected
/// places due to being unable to resolve builtin symbols. This is similar
/// behaviour to other type checkers such as mypy:
/// <https://github.com/python/mypy/blob/3807423e9d98e678bf16b13ec8b4f909fe181908/mypy/build.py#L104-L117>
pub(super) fn is_non_shadowable(self, minor_version: u8, module_name: &str) -> bool {
// Builtin modules are never shadowable, no matter what.
if ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name) {
return true;
}
// Similarly for `types`, which is always available at runtime.
if module_name == "types" {
return true;
}
// Otherwise, some modules should only be conditionally allowed
// to be shadowed, depending on the module resolution mode.
match self {
ModuleResolveMode::StubsAllowed | ModuleResolveMode::StubsNotAllowed => {
module_name == "typing_extensions"
}
ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed => false,
}
}
}
/// Salsa query that resolves an interned [`ModuleNameIngredient`] to a module.
@@ -386,7 +451,10 @@ impl SearchPaths {
pub(crate) fn stdlib(&self, mode: ModuleResolveMode) -> Option<&SearchPath> {
match mode {
ModuleResolveMode::StubsAllowed => self.stdlib_path.as_ref(),
ModuleResolveMode::StubsNotAllowed => self.real_stdlib_path.as_ref(),
ModuleResolveMode::StubsNotAllowed
| ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed => {
self.real_stdlib_path.as_ref()
}
}
}
@@ -439,7 +507,8 @@ pub(crate) fn dynamic_resolution_paths<'db>(
// Use the `ModuleResolveMode` to determine which stdlib (if any) to mark as existing
let stdlib = match mode.mode(db) {
ModuleResolveMode::StubsAllowed => stdlib_path,
ModuleResolveMode::StubsNotAllowed => real_stdlib_path,
ModuleResolveMode::StubsNotAllowed
| ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed => real_stdlib_path,
};
if let Some(path) = stdlib.as_ref().and_then(SearchPath::as_system_path) {
existing_paths.insert(Cow::Borrowed(path));
@@ -684,27 +753,13 @@ struct ModuleNameIngredient<'db> {
pub(super) mode: ModuleResolveMode,
}
/// 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
/// certain other modules that are involved in an import cycle with `builtins` (`types`,
/// `typing_extensions`, etc.). This latter set of modules cannot be allowed to be shadowed by
/// first-party or "extra-path" modules, or we risk panics in unexpected places due to being
/// unable to resolve builtin symbols. This is similar behaviour to other type checkers such
/// as mypy: <https://github.com/python/mypy/blob/3807423e9d98e678bf16b13ec8b4f909fe181908/mypy/build.py#L104-L117>
pub(super) fn is_non_shadowable(minor_version: u8, module_name: &str) -> bool {
matches!(module_name, "types" | "typing_extensions")
|| 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, mode: ModuleResolveMode) -> Option<ResolvedName> {
let program = Program::get(db);
let python_version = program.python_version(db);
let resolver_state = ResolverContext::new(db, python_version, mode);
let is_non_shadowable = is_non_shadowable(python_version.minor, name.as_str());
let is_non_shadowable = mode.is_non_shadowable(python_version.minor, name.as_str());
let name = RelaxedModuleName::new(name);
let stub_name = name.to_stub_package();

View File

@@ -1478,6 +1478,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
}
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
self.scopes_by_expression
.record_expression(asname, self.current_scope());
(asname.id.clone(), asname.id == alias.name.id)
} else {
(Name::new(alias.name.id.split('.').next().unwrap()), false)
@@ -1651,6 +1653,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
}
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
self.scopes_by_expression
.record_expression(asname, self.current_scope());
// It's re-exported if it's `from ... import x as x`
(&asname.id, asname.id == alias.name.id)
} else {

View File

@@ -336,6 +336,7 @@ fn pattern_kind_to_type<'db>(db: &'db dyn Db, kind: &PatternPredicateKind<'db>)
infer_expression_type(db, *class_expr, TypeContext::default())
.to_instance(db)
.unwrap_or(Type::Never)
.top_materialization(db)
} else {
Type::Never
}

View File

@@ -1,19 +1,19 @@
use ruff_db::files::{File, FilePath};
use ruff_db::source::{line_index, source_text};
use ruff_python_ast::{self as ast, ExprStringLiteral, ModExpression};
use ruff_python_ast::{self as ast, ExprStringLiteral, Identifier, ModExpression};
use ruff_python_ast::{Expr, ExprRef, HasNodeIndex, name::Name};
use ruff_python_parser::Parsed;
use ruff_source_file::LineIndex;
use rustc_hash::FxHashMap;
use crate::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::FileScopeId;
use crate::semantic_index::semantic_index;
use crate::types::ide_support::{Member, all_declarations_and_bindings, all_members};
use crate::types::list_members::{Member, all_members, all_members_of_scope};
use crate::types::{Type, binding_type, infer_scope_types};
use crate::{Db, resolve_real_shadowable_module};
/// The primary interface the LSP should use for querying semantic information about a [`File`].
///
@@ -76,7 +76,7 @@ impl<'db> SemanticModel<'db> {
for (file_scope, _) in index.ancestor_scopes(file_scope) {
for memberdef in
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file))
{
members.insert(
memberdef.member.name,
@@ -90,6 +90,19 @@ impl<'db> SemanticModel<'db> {
members
}
pub fn inferred_type_for_identifier(&self, identifier: &Identifier) -> Type<'db> {
// TODO(#1637): semantic tokens is making this crash even with
// `try_expr_ref_in_ast` guarding this, for now just use `try_expression_scope_id`.
// The problematic input is `x: "float` (with a dangling quote). I imagine the issue
// is we're too eagerly setting `is_string_annotation` in inference.
let Some(file_scope) = self.scope(identifier.into()) else {
return Type::unknown();
};
let scope = file_scope.to_scope_id(self.db, self.file);
infer_scope_types(self.db, scope).expression_type(identifier)
}
/// Resolve the given import made in this file to a Type
pub fn resolve_module_type(&self, module: Option<&str>, level: u32) -> Option<Type<'db>> {
let module = self.resolve_module(module, level)?;
@@ -105,8 +118,14 @@ impl<'db> SemanticModel<'db> {
/// Returns completions for symbols available in a `import <CURSOR>` context.
pub fn import_completions(&self) -> Vec<Completion<'db>> {
let typing_extensions = ModuleName::new("typing_extensions").unwrap();
let is_typing_extensions_available = self.file.is_stub(self.db)
|| resolve_real_shadowable_module(self.db, &typing_extensions).is_some();
list_modules(self.db)
.into_iter()
.filter(|module| {
is_typing_extensions_available || module.name(self.db) != &typing_extensions
})
.map(|module| {
let builtin = module.is_known(self.db, KnownModule::Builtins);
let ty = Type::module_literal(self.db, self.file, module);
@@ -215,12 +234,13 @@ impl<'db> SemanticModel<'db> {
let mut completions = vec![];
for (file_scope, _) in index.ancestor_scopes(file_scope) {
completions.extend(
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
.map(|memberdef| Completion {
all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file)).map(
|memberdef| Completion {
name: memberdef.member.name,
ty: Some(memberdef.member.ty),
builtin: false,
}),
},
),
);
}
// Builtins are available in all scopes.
@@ -397,7 +417,7 @@ pub trait HasType {
}
pub trait HasDefinition {
/// Returns the inferred type of `self`.
/// Returns the definition of `self`.
///
/// ## Panics
/// May panic if `self` is from another file than `model`.

View File

@@ -375,6 +375,77 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) {
}
}
/// Creates a fix for adding a suppression comment to suppress `lint` for `range`.
///
/// The fix prefers adding the code to an existing `ty: ignore[]` comment over
/// adding a new suppression comment.
pub fn create_suppression_fix(db: &dyn Db, file: File, id: LintId, range: TextRange) -> Fix {
let suppressions = suppressions(db, file);
let source = source_text(db, file);
let mut existing_suppressions = suppressions.line_suppressions(range).filter(|suppression| {
matches!(
suppression.target,
SuppressionTarget::Lint(_) | SuppressionTarget::Empty,
)
});
// If there's an existing `ty: ignore[]` comment, append the code to it instead of creating a new suppression comment.
if let Some(existing) = existing_suppressions.next() {
let comment_text = &source[existing.comment_range];
// Only add to the existing ignore comment if it has no reason.
if let Some(before_closing_paren) = comment_text.trim_end().strip_suffix(']') {
let up_to_last_code = before_closing_paren.trim_end();
let insertion = if up_to_last_code.ends_with(',') {
format!(" {id}", id = id.name())
} else {
format!(", {id}", id = id.name())
};
let relative_offset_from_end = comment_text.text_len() - up_to_last_code.text_len();
return Fix::safe_edit(Edit::insertion(
insertion,
existing.comment_range.end() - relative_offset_from_end,
));
}
}
// Always insert a new suppression at the end of the range to avoid having to deal with multiline strings
// etc.
let parsed = parsed_module(db, file).load(db);
let tokens_after = parsed.tokens().after(range.end());
// Same as for `line_end` when building up the `suppressions`: Ignore newlines
// in multiline-strings, inside f-strings, or after a line continuation because we can't
// place a comment on those lines.
let line_end = tokens_after
.iter()
.find(|token| {
matches!(
token.kind(),
TokenKind::Newline | TokenKind::NonLogicalNewline
)
})
.map(Ranged::start)
.unwrap_or(source.text_len());
let up_to_line_end = &source[..line_end.to_usize()];
let up_to_first_content = up_to_line_end.trim_end();
let trailing_whitespace_len = up_to_line_end.text_len() - up_to_first_content.text_len();
let insertion = format!(" # ty:ignore[{id}]", id = id.name());
Fix::safe_edit(if trailing_whitespace_len == TextSize::ZERO {
Edit::insertion(insertion, line_end)
} else {
// `expr # fmt: off<trailing_whitespace>`
// Trim the trailing whitespace
Edit::replacement(insertion, line_end - trailing_whitespace_len, line_end)
})
}
struct CheckSuppressionsContext<'a> {
db: &'a dyn Db,
file: File,

View File

@@ -96,11 +96,12 @@ mod generics;
pub mod ide_support;
mod infer;
mod instance;
mod liskov;
pub mod list_members;
mod member;
mod mro;
mod narrow;
mod newtype;
mod overrides;
mod protocol_class;
mod signatures;
mod special_form;
@@ -573,20 +574,19 @@ impl<'db> PropertyInstanceType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
let getter = match self.getter(db) {
Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true, visitor)?),
Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?),
Some(ty) => Some(
ty.recursive_type_normalized_impl(db, div, true, visitor)
ty.recursive_type_normalized_impl(db, div, true)
.unwrap_or(div),
),
None => None,
};
let setter = match self.setter(db) {
Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true, visitor)?),
Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?),
Some(ty) => Some(
ty.recursive_type_normalized_impl(db, div, true, visitor)
ty.recursive_type_normalized_impl(db, div, true)
.unwrap_or(div),
),
None => None,
@@ -957,8 +957,13 @@ impl<'db> Type<'db> {
self.is_instance_of(db, KnownClass::NotImplementedType)
}
pub(crate) const fn is_todo(&self) -> bool {
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
pub(crate) fn is_todo(&self) -> bool {
self.as_dynamic().is_some_and(|dynamic| match dynamic {
DynamicType::Any | DynamicType::Unknown | DynamicType::Divergent(_) => false,
DynamicType::Todo(_) | DynamicType::TodoStarredExpression | DynamicType::TodoUnpack => {
true
}
})
}
pub const fn is_generic_alias(&self) -> bool {
@@ -1553,15 +1558,14 @@ impl<'db> Type<'db> {
#[must_use]
pub(crate) fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self {
cycle.head_ids().fold(self, |ty, id| {
let visitor = NormalizedVisitor::new(Type::divergent(id));
ty.recursive_type_normalized_impl(db, Type::divergent(id), false, &visitor)
ty.recursive_type_normalized_impl(db, Type::divergent(id), false)
.unwrap_or(Type::divergent(id))
})
}
/// Normalizes types including divergent types (recursive types), which is necessary for convergence of fixed-point iteration.
/// When nested is true, propagate `None`. That is, if the type contains a `Divergent` type, the return value of this method is `None`.
/// When nested is false, create a type containing `Divergent` types instead of propagating `None`.
/// When `nested` is true, propagate `None`. That is, if the type contains a `Divergent` type, the return value of this method is `None` (so we can use the `?` operator).
/// When `nested` is false, create a type containing `Divergent` types instead of propagating `None` (we should use `unwrap_or(Divergent)`).
/// This is to preserve the structure of the non-divergent parts of the type instead of completely collapsing the type containing a `Divergent` type into a `Divergent` type.
/// ```python
/// tuple[tuple[Divergent, Literal[1]], Literal[1]].recursive_type_normalized(nested: false)
@@ -1580,102 +1584,73 @@ impl<'db> Type<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
if nested && self == div {
return None;
}
match self {
Type::Union(union) => visitor.try_visit(self, || {
union.recursive_type_normalized_impl(db, div, nested, visitor)
}),
Type::Intersection(intersection) => visitor.try_visit(self, || {
intersection
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::Intersection)
}),
Type::Callable(callable) => visitor.try_visit(self, || {
callable
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::Callable)
}),
Type::ProtocolInstance(protocol) => visitor.try_visit(self, || {
protocol
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::ProtocolInstance)
}),
Type::NominalInstance(instance) => visitor.try_visit(self, || {
instance
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::NominalInstance)
}),
Type::FunctionLiteral(function) => visitor.try_visit(self, || {
function
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::FunctionLiteral)
}),
Type::PropertyInstance(property) => visitor.try_visit(self, || {
property
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::PropertyInstance)
}),
Type::KnownBoundMethod(method_kind) => visitor.try_visit(self, || {
method_kind
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::KnownBoundMethod)
}),
Type::BoundMethod(method) => visitor.try_visit(self, || {
method
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::BoundMethod)
}),
Type::BoundSuper(bound_super) => visitor.try_visit(self, || {
bound_super
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::BoundSuper)
}),
Type::GenericAlias(generic) => visitor.try_visit(self, || {
generic
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::GenericAlias)
}),
Type::SubclassOf(subclass_of) => visitor.try_visit(self, || {
subclass_of
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::SubclassOf)
}),
Type::Union(union) => union.recursive_type_normalized_impl(db, div, nested),
Type::Intersection(intersection) => intersection
.recursive_type_normalized_impl(db, div, nested)
.map(Type::Intersection),
Type::Callable(callable) => callable
.recursive_type_normalized_impl(db, div, nested)
.map(Type::Callable),
Type::ProtocolInstance(protocol) => protocol
.recursive_type_normalized_impl(db, div, nested)
.map(Type::ProtocolInstance),
Type::NominalInstance(instance) => instance
.recursive_type_normalized_impl(db, div, nested)
.map(Type::NominalInstance),
Type::FunctionLiteral(function) => function
.recursive_type_normalized_impl(db, div, nested)
.map(Type::FunctionLiteral),
Type::PropertyInstance(property) => property
.recursive_type_normalized_impl(db, div, nested)
.map(Type::PropertyInstance),
Type::KnownBoundMethod(method_kind) => method_kind
.recursive_type_normalized_impl(db, div, nested)
.map(Type::KnownBoundMethod),
Type::BoundMethod(method) => method
.recursive_type_normalized_impl(db, div, nested)
.map(Type::BoundMethod),
Type::BoundSuper(bound_super) => bound_super
.recursive_type_normalized_impl(db, div, nested)
.map(Type::BoundSuper),
Type::GenericAlias(generic) => generic
.recursive_type_normalized_impl(db, div, nested)
.map(Type::GenericAlias),
Type::SubclassOf(subclass_of) => subclass_of
.recursive_type_normalized_impl(db, div, nested)
.map(Type::SubclassOf),
Type::TypeVar(_) => Some(self),
Type::KnownInstance(known_instance) => visitor.try_visit(self, || {
known_instance
.recursive_type_normalized_impl(db, div, nested, visitor)
.map(Type::KnownInstance)
}),
Type::TypeIs(type_is) => visitor.try_visit(self, || {
Type::KnownInstance(known_instance) => known_instance
.recursive_type_normalized_impl(db, div, nested)
.map(Type::KnownInstance),
Type::TypeIs(type_is) => {
let ty = if nested {
type_is
.return_type(db)
.recursive_type_normalized_impl(db, div, true, visitor)?
.recursive_type_normalized_impl(db, div, true)?
} else {
type_is
.return_type(db)
.recursive_type_normalized_impl(db, div, true, visitor)
.recursive_type_normalized_impl(db, div, true)
.unwrap_or(div)
};
Some(type_is.with_type(db, ty))
}),
}
Type::Dynamic(dynamic) => Some(Type::Dynamic(dynamic.recursive_type_normalized())),
Type::TypedDict(_) => {
// TODO: Normalize TypedDicts
Some(self)
}
Type::TypeAlias(_) => Some(self),
Type::NewTypeInstance(newtype) => visitor.try_visit(self, || {
newtype
.try_map_base_class_type(db, |class_type| {
class_type.recursive_type_normalized_impl(db, div, nested, visitor)
})
.map(Type::NewTypeInstance)
}),
Type::NewTypeInstance(newtype) => newtype
.try_map_base_class_type(db, |class_type| {
class_type.recursive_type_normalized_impl(db, div, nested)
})
.map(Type::NewTypeInstance),
Type::LiteralString
| Type::AlwaysFalsy
| Type::AlwaysTruthy
@@ -2115,18 +2090,25 @@ impl<'db> Type<'db> {
// `type[T]` is a subtype of the class object `A` if every instance of `T` is a subtype of an instance
// of `A`, and vice versa.
(Type::SubclassOf(subclass_of), _)
if subclass_of.is_type_var()
&& !matches!(target, Type::Callable(_) | Type::ProtocolInstance(_)) =>
if !subclass_of
.into_type_var()
.zip(target.to_instance(db))
.when_some_and(|(this_instance, other_instance)| {
Type::TypeVar(this_instance).has_relation_to_impl(
db,
other_instance,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
.is_never_satisfied(db) =>
{
// TODO: The repetition here isn't great, but we really need the fallthrough logic,
// where this arm only engages if it returns true.
let this_instance = Type::TypeVar(subclass_of.into_type_var().unwrap());
let other_instance = match target {
Type::Union(union) => Some(
union.map(db, |element| element.to_instance(db).unwrap_or(Type::Never)),
),
_ => target.to_instance(db),
};
other_instance.when_some_and(|other_instance| {
target.to_instance(db).when_some_and(|other_instance| {
this_instance.has_relation_to_impl(
db,
other_instance,
@@ -2137,6 +2119,7 @@ impl<'db> Type<'db> {
)
})
}
(_, Type::SubclassOf(subclass_of)) if subclass_of.is_type_var() => {
let other_instance = Type::TypeVar(subclass_of.into_type_var().unwrap());
self.to_instance(db).when_some_and(|this_instance| {
@@ -2673,6 +2656,10 @@ impl<'db> Type<'db> {
disjointness_visitor,
),
(Type::SubclassOf(subclass_of), _) if subclass_of.is_type_var() => {
ConstraintSet::from(false)
}
// `Literal[<class 'C'>]` is a subtype of `type[B]` if `C` is a subclass of `B`,
// since `type[B]` describes all possible runtime subclasses of the class object `B`.
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
@@ -3107,8 +3094,7 @@ impl<'db> Type<'db> {
ConstraintSet::from(false)
}
// `type[T]` is disjoint from a callable or protocol instance if its upper bound or
// constraints are.
// `type[T]` is disjoint from a callable or protocol instance if its upper bound or constraints are.
(Type::SubclassOf(subclass_of), Type::Callable(_) | Type::ProtocolInstance(_))
| (Type::Callable(_) | Type::ProtocolInstance(_), Type::SubclassOf(subclass_of))
if subclass_of.is_type_var() =>
@@ -3130,13 +3116,14 @@ impl<'db> Type<'db> {
// `type[T]` is disjoint from a class object `A` if every instance of `T` is disjoint from an instance of `A`.
(Type::SubclassOf(subclass_of), other) | (other, Type::SubclassOf(subclass_of))
if subclass_of.is_type_var() =>
if subclass_of.is_type_var()
&& (other.to_instance(db).is_some()
|| other.as_typevar().is_some_and(|type_var| {
type_var.typevar(db).bound_or_constraints(db).is_none()
})) =>
{
let this_instance = Type::TypeVar(subclass_of.into_type_var().unwrap());
let other_instance = match other {
Type::Union(union) => Some(
union.map(db, |element| element.to_instance(db).unwrap_or(Type::Never)),
),
// An unbounded typevar `U` may have instances of type `object` if specialized to
// an instance of `type`.
Type::TypeVar(typevar)
@@ -3490,6 +3477,12 @@ impl<'db> Type<'db> {
})
}
(Type::SubclassOf(subclass_of_ty), _) | (_, Type::SubclassOf(subclass_of_ty))
if subclass_of_ty.is_type_var() =>
{
ConstraintSet::from(true)
}
(Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b))
| (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => {
match subclass_of_ty.subclass_of() {
@@ -3519,31 +3512,27 @@ impl<'db> Type<'db> {
// 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))
if !subclass_of_ty.is_type_var() =>
{
match subclass_of_ty.subclass_of() {
SubclassOfInner::Dynamic(_) => {
KnownClass::Type.to_instance(db).is_disjoint_from_impl(
db,
other,
inferable,
disjointness_visitor,
relation_visitor,
)
}
SubclassOfInner::Class(class) => {
class.metaclass_instance_type(db).is_disjoint_from_impl(
db,
other,
inferable,
disjointness_visitor,
relation_visitor,
)
}
SubclassOfInner::TypeVar(_) => unreachable!(),
| (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Dynamic(_) => {
KnownClass::Type.to_instance(db).is_disjoint_from_impl(
db,
other,
inferable,
disjointness_visitor,
relation_visitor,
)
}
}
SubclassOfInner::Class(class) => {
class.metaclass_instance_type(db).is_disjoint_from_impl(
db,
other,
inferable,
disjointness_visitor,
relation_visitor,
)
}
SubclassOfInner::TypeVar(_) => unreachable!(),
},
(Type::SpecialForm(special_form), Type::NominalInstance(instance))
| (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => {
@@ -3805,11 +3794,6 @@ impl<'db> Type<'db> {
relation_visitor,
)
}
(Type::SubclassOf(_), _) | (_, Type::SubclassOf(_)) => {
// All cases should have been handled above.
unreachable!()
}
}
}
@@ -7403,9 +7387,6 @@ impl<'db> Type<'db> {
Some(KnownClass::TypeVarTuple) => Ok(todo_type!(
"Support for `typing.TypeVarTuple` instances in type expressions"
)),
Some(KnownClass::GenericAlias) => Ok(todo_type!(
"Support for `typing.GenericAlias` instances in type expressions"
)),
_ => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec_inline![
InvalidTypeExpression::InvalidType(*self, scope_id)
@@ -8167,7 +8148,7 @@ impl<'db> Type<'db> {
Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db),
// These types have no definition
Self::Dynamic(DynamicType::Divergent(_) | DynamicType::Todo(_) | DynamicType::TodoUnpack)
Self::Dynamic(DynamicType::Divergent(_) | DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression)
| Self::Callable(_)
| Self::TypeIs(_) => None,
}
@@ -8702,7 +8683,6 @@ impl<'db> KnownInstanceType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
match self {
// Nothing to normalize
@@ -8712,37 +8692,37 @@ impl<'db> KnownInstanceType<'db> {
Self::ConstraintSet(set) => Some(Self::ConstraintSet(set)),
Self::TypeVar(typevar) => Some(Self::TypeVar(typevar)),
Self::TypeAliasType(type_alias) => type_alias
.recursive_type_normalized_impl(db, div, visitor)
.recursive_type_normalized_impl(db, div)
.map(Self::TypeAliasType),
Self::Field(field) => field
.recursive_type_normalized_impl(db, div, nested, visitor)
.recursive_type_normalized_impl(db, div, nested)
.map(Self::Field),
Self::UnionType(union_type) => union_type
.recursive_type_normalized_impl(db, div, nested, visitor)
.recursive_type_normalized_impl(db, div, nested)
.map(Self::UnionType),
Self::Literal(ty) => ty
.recursive_type_normalized_impl(db, div, true, visitor)
.recursive_type_normalized_impl(db, div, true)
.map(Self::Literal),
Self::Annotated(ty) => ty
.recursive_type_normalized_impl(db, div, true, visitor)
.recursive_type_normalized_impl(db, div, true)
.map(Self::Annotated),
Self::TypeGenericAlias(ty) => ty
.recursive_type_normalized_impl(db, div, true, visitor)
.recursive_type_normalized_impl(db, div, true)
.map(Self::TypeGenericAlias),
Self::LiteralStringAlias(ty) => ty
.recursive_type_normalized_impl(db, div, true, visitor)
.recursive_type_normalized_impl(db, div, true)
.map(Self::LiteralStringAlias),
Self::Callable(callable) => callable
.recursive_type_normalized_impl(db, div, nested, visitor)
.recursive_type_normalized_impl(db, div, nested)
.map(Self::Callable),
Self::NewType(newtype) => newtype
.try_map_base_class_type(db, |class_type| {
class_type.recursive_type_normalized_impl(db, div, true, visitor)
class_type.recursive_type_normalized_impl(db, div, true)
})
.map(Self::NewType),
Self::GenericContext(generic) => Some(Self::GenericContext(generic)),
Self::Specialization(specialization) => specialization
.recursive_type_normalized_impl(db, div, true, visitor)
.recursive_type_normalized_impl(db, div, true)
.map(Self::Specialization),
}
}
@@ -8829,6 +8809,8 @@ pub enum DynamicType {
Todo(TodoType),
/// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]`
TodoUnpack,
/// A special Todo-variant for `*Ts`, so that we can treat it specially in `Generic[Unpack[Ts]]`
TodoStarredExpression,
/// A type that is determined to be divergent during recursive type inference.
Divergent(DivergentType),
}
@@ -8859,13 +8841,8 @@ impl std::fmt::Display for DynamicType {
// `DynamicType::Todo`'s display should be explicit that is not a valid display of
// any other type
DynamicType::Todo(todo) => write!(f, "@Todo{todo}"),
DynamicType::TodoUnpack => {
if cfg!(debug_assertions) {
f.write_str("@Todo(typing.Unpack)")
} else {
f.write_str("@Todo")
}
}
DynamicType::TodoUnpack => f.write_str("@Todo(typing.Unpack)"),
DynamicType::TodoStarredExpression => f.write_str("@Todo(StarredExpression)"),
DynamicType::Divergent(_) => f.write_str("Divergent"),
}
}
@@ -9231,15 +9208,12 @@ impl<'db> FieldInstance<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
let default_type = match self.default_type(db) {
Some(default) if nested => {
Some(default.recursive_type_normalized_impl(db, div, true, visitor)?)
}
Some(default) if nested => Some(default.recursive_type_normalized_impl(db, div, true)?),
Some(default) => Some(
default
.recursive_type_normalized_impl(db, div, true, visitor)
.recursive_type_normalized_impl(db, div, true)
.unwrap_or(div),
),
None => None,
@@ -10184,7 +10158,6 @@ impl<'db> UnionTypeInstance<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
// The `Divergent` elimination rules are different within union types.
// See `UnionType::recursive_type_normalized_impl` for details.
@@ -10192,14 +10165,14 @@ impl<'db> UnionTypeInstance<'db> {
Some(types) if nested => Some(
types
.iter()
.map(|ty| ty.recursive_type_normalized_impl(db, div, nested, visitor))
.map(|ty| ty.recursive_type_normalized_impl(db, div, nested))
.collect::<Option<Box<_>>>()?,
),
Some(types) => Some(
types
.iter()
.map(|ty| {
ty.recursive_type_normalized_impl(db, div, nested, visitor)
ty.recursive_type_normalized_impl(db, div, nested)
.unwrap_or(div)
})
.collect::<Box<_>>(),
@@ -10207,9 +10180,9 @@ impl<'db> UnionTypeInstance<'db> {
None => None,
};
let union_type = match self.union_type(db).clone() {
Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, div, nested, visitor)?),
Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, div, nested)?),
Ok(ty) => Ok(ty
.recursive_type_normalized_impl(db, div, nested, visitor)
.recursive_type_normalized_impl(db, div, nested)
.unwrap_or(div)),
Err(err) => Err(err),
};
@@ -10241,14 +10214,13 @@ impl<'db> InternedType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
let inner = if nested {
self.inner(db)
.recursive_type_normalized_impl(db, div, nested, visitor)?
.recursive_type_normalized_impl(db, div, nested)?
} else {
self.inner(db)
.recursive_type_normalized_impl(db, div, nested, visitor)
.recursive_type_normalized_impl(db, div, nested)
.unwrap_or(div)
};
Some(InternedType::new(db, inner))
@@ -11560,14 +11532,13 @@ impl<'db> BoundMethodType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
Some(Self::new(
db,
self.function(db)
.recursive_type_normalized_impl(db, div, nested, visitor)?,
.recursive_type_normalized_impl(db, div, nested)?,
self.self_instance(db)
.recursive_type_normalized_impl(db, div, true, visitor)?,
.recursive_type_normalized_impl(db, div, true)?,
))
}
@@ -11732,12 +11703,11 @@ impl<'db> CallableType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
Some(CallableType::new(
db,
self.signatures(db)
.recursive_type_normalized_impl(db, div, nested, visitor)?,
.recursive_type_normalized_impl(db, div, nested)?,
self.is_function_like(db),
))
}
@@ -12175,27 +12145,26 @@ impl<'db> KnownBoundMethodType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
match self {
KnownBoundMethodType::FunctionTypeDunderGet(function) => {
Some(KnownBoundMethodType::FunctionTypeDunderGet(
function.recursive_type_normalized_impl(db, div, nested, visitor)?,
function.recursive_type_normalized_impl(db, div, nested)?,
))
}
KnownBoundMethodType::FunctionTypeDunderCall(function) => {
Some(KnownBoundMethodType::FunctionTypeDunderCall(
function.recursive_type_normalized_impl(db, div, nested, visitor)?,
function.recursive_type_normalized_impl(db, div, nested)?,
))
}
KnownBoundMethodType::PropertyDunderGet(property) => {
Some(KnownBoundMethodType::PropertyDunderGet(
property.recursive_type_normalized_impl(db, div, nested, visitor)?,
property.recursive_type_normalized_impl(db, div, nested)?,
))
}
KnownBoundMethodType::PropertyDunderSet(property) => {
Some(KnownBoundMethodType::PropertyDunderSet(
property.recursive_type_normalized_impl(db, div, nested, visitor)?,
property.recursive_type_normalized_impl(db, div, nested)?,
))
}
KnownBoundMethodType::StrStartswith(_)
@@ -12858,18 +12827,14 @@ impl<'db> ManualPEP695TypeAliasType<'db> {
)
}
fn recursive_type_normalized_impl(
self,
db: &'db dyn Db,
div: Type<'db>,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
// TODO: with full support for manual PEP-695 style type aliases, this method should become unnecessary.
fn recursive_type_normalized_impl(self, db: &'db dyn Db, div: Type<'db>) -> Option<Self> {
Some(Self::new(
db,
self.name(db),
self.definition(db),
self.value(db)
.recursive_type_normalized_impl(db, div, true, visitor)?,
.recursive_type_normalized_impl(db, div, true)?,
))
}
}
@@ -12914,16 +12879,11 @@ impl<'db> TypeAliasType<'db> {
}
}
fn recursive_type_normalized_impl(
self,
db: &'db dyn Db,
div: Type<'db>,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
fn recursive_type_normalized_impl(self, db: &'db dyn Db, div: Type<'db>) -> Option<Self> {
match self {
TypeAliasType::PEP695(type_alias) => Some(TypeAliasType::PEP695(type_alias)),
TypeAliasType::ManualPEP695(type_alias) => Some(TypeAliasType::ManualPEP695(
type_alias.recursive_type_normalized_impl(db, div, visitor)?,
type_alias.recursive_type_normalized_impl(db, div)?,
)),
}
}
@@ -13248,7 +13208,6 @@ impl<'db> UnionType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Type<'db>> {
let mut builder = UnionBuilder::new(db)
.order_elements(false)
@@ -13258,7 +13217,7 @@ impl<'db> UnionType<'db> {
for ty in self.elements(db) {
if nested {
// list[T | Divergent] => list[Divergent]
let ty = ty.recursive_type_normalized_impl(db, div, nested, visitor)?;
let ty = ty.recursive_type_normalized_impl(db, div, nested)?;
if ty == div {
return Some(ty);
}
@@ -13271,7 +13230,7 @@ impl<'db> UnionType<'db> {
continue;
}
builder = builder.add(
ty.recursive_type_normalized_impl(db, div, nested, visitor)
ty.recursive_type_normalized_impl(db, div, nested)
.unwrap_or(div),
);
empty = false;
@@ -13389,18 +13348,16 @@ impl<'db> IntersectionType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
fn opt_normalized_set<'db>(
db: &'db dyn Db,
elements: &FxOrderSet<Type<'db>>,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<FxOrderSet<Type<'db>>> {
elements
.iter()
.map(|ty| ty.recursive_type_normalized_impl(db, div, nested, visitor))
.map(|ty| ty.recursive_type_normalized_impl(db, div, nested))
.collect()
}
@@ -13409,26 +13366,25 @@ impl<'db> IntersectionType<'db> {
elements: &FxOrderSet<Type<'db>>,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> FxOrderSet<Type<'db>> {
elements
.iter()
.map(|ty| {
ty.recursive_type_normalized_impl(db, div, nested, visitor)
ty.recursive_type_normalized_impl(db, div, nested)
.unwrap_or(div)
})
.collect()
}
let positive = if nested {
opt_normalized_set(db, self.positive(db), div, nested, visitor)?
opt_normalized_set(db, self.positive(db), div, nested)?
} else {
normalized_set(db, self.positive(db), div, nested, visitor)
normalized_set(db, self.positive(db), div, nested)
};
let negative = if nested {
opt_normalized_set(db, self.negative(db), div, nested, visitor)?
opt_normalized_set(db, self.negative(db), div, nested)?
} else {
normalized_set(db, self.negative(db), div, nested, visitor)
normalized_set(db, self.negative(db), div, nested)
};
Some(IntersectionType::new(db, positive, negative))
@@ -13911,16 +13867,15 @@ pub(crate) mod tests {
nested_rec.display(&db).to_string(),
"list[list[Divergent] | None]"
);
let visitor = NormalizedVisitor::default();
let normalized = nested_rec
.recursive_type_normalized_impl(&db, div, false, &visitor)
.recursive_type_normalized_impl(&db, div, false)
.unwrap();
assert_eq!(normalized.display(&db).to_string(), "list[Divergent]");
let union = UnionType::from_elements(&db, [div, KnownClass::Int.to_instance(&db)]);
assert_eq!(union.display(&db).to_string(), "Divergent | int");
let normalized = union
.recursive_type_normalized_impl(&db, div, false, &visitor)
.recursive_type_normalized_impl(&db, div, false)
.unwrap();
assert_eq!(normalized.display(&db).to_string(), "int");

View File

@@ -197,17 +197,16 @@ impl<'db> SuperOwnerKind<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
match self {
SuperOwnerKind::Dynamic(dynamic) => {
Some(SuperOwnerKind::Dynamic(dynamic.recursive_type_normalized()))
}
SuperOwnerKind::Class(class) => Some(SuperOwnerKind::Class(
class.recursive_type_normalized_impl(db, div, nested, visitor)?,
class.recursive_type_normalized_impl(db, div, nested)?,
)),
SuperOwnerKind::Instance(instance) => Some(SuperOwnerKind::Instance(
instance.recursive_type_normalized_impl(db, div, nested, visitor)?,
instance.recursive_type_normalized_impl(db, div, nested)?,
)),
}
}
@@ -620,14 +619,13 @@ impl<'db> BoundSuperType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
Some(Self::new(
db,
self.pivot_class(db)
.recursive_type_normalized_impl(db, div, nested, visitor)?,
.recursive_type_normalized_impl(db, div, nested)?,
self.owner(db)
.recursive_type_normalized_impl(db, div, nested, visitor)?,
.recursive_type_normalized_impl(db, div, nested)?,
))
}
}

View File

@@ -39,7 +39,7 @@ use crate::types::{
DataclassParams, FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType,
MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType,
TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType,
WrapperDescriptorKind, enums, ide_support, todo_type,
WrapperDescriptorKind, enums, list_members, todo_type,
};
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion};
@@ -888,7 +888,7 @@ impl<'db> Bindings<'db> {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(Type::heterogeneous_tuple(
db,
ide_support::all_members(db, *ty)
list_members::all_members(db, *ty)
.into_iter()
.sorted()
.map(|member| Type::string_literal(db, &member.name)),

View File

@@ -19,7 +19,7 @@ use crate::semantic_index::{
use crate::types::bound_super::BoundSuperError;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::context::InferContext;
use crate::types::diagnostic::INVALID_TYPE_ALIAS_TYPE;
use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD};
use crate::types::enums::enum_metadata;
use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{
@@ -284,13 +284,12 @@ impl<'db> GenericAlias<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
Some(Self::new(
db,
self.origin(db),
self.specialization(db)
.recursive_type_normalized_impl(db, div, nested, visitor)?,
.recursive_type_normalized_impl(db, div, nested)?,
))
}
@@ -443,12 +442,11 @@ impl<'db> ClassType<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
match self {
Self::NonGeneric(_) => Some(self),
Self::Generic(generic) => Some(Self::Generic(
generic.recursive_type_normalized_impl(db, div, nested, visitor)?,
generic.recursive_type_normalized_impl(db, div, nested)?,
)),
}
}
@@ -5546,6 +5544,20 @@ impl KnownClass {
return;
};
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
if let Some(builder) = context
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
{
builder.into_diagnostic(format_args!(
"Cannot use `super()` in a method of NamedTuple class `{}`",
enclosing_class.name(db)
));
}
overload.set_return_type(Type::unknown());
return;
}
// The type of the first parameter if the given scope is function-like (i.e. function or lambda).
// `None` if the scope is not function-like, or has no parameters.
let first_param = match scope.node(db) {
@@ -5585,6 +5597,22 @@ impl KnownClass {
overload.set_return_type(bound_super);
}
[Some(pivot_class_type), Some(owner_type)] => {
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) {
if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
if let Some(builder) = context
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
{
builder.into_diagnostic(format_args!(
"Cannot use `super()` in a method of NamedTuple class `{}`",
enclosing_class.name(db)
));
}
overload.set_return_type(Type::unknown());
return;
}
}
let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type)
.unwrap_or_else(|err| {
err.report_diagnostic(context, call_expression.into());

View File

@@ -48,12 +48,11 @@ impl<'db> ClassBase<'db> {
db: &'db dyn Db,
div: Type<'db>,
nested: bool,
visitor: &NormalizedVisitor<'db>,
) -> Option<Self> {
match self {
Self::Dynamic(dynamic) => Some(Self::Dynamic(dynamic.recursive_type_normalized())),
Self::Class(class) => Some(Self::Class(
class.recursive_type_normalized_impl(db, div, nested, visitor)?,
class.recursive_type_normalized_impl(db, div, nested)?,
)),
Self::Protocol | Self::Generic | Self::TypedDict => Some(self),
}
@@ -64,7 +63,9 @@ impl<'db> ClassBase<'db> {
ClassBase::Class(class) => class.name(db),
ClassBase::Dynamic(DynamicType::Any) => "Any",
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack) => "@Todo",
ClassBase::Dynamic(
DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression,
) => "@Todo",
ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent",
ClassBase::Protocol => "Protocol",
ClassBase::Generic => "Generic",

View File

@@ -418,6 +418,7 @@ impl<'db> ConstraintSet<'db> {
Self::constrain_typevar(db, typevar, lower, upper, TypeRelation::Assignability)
}
#[expect(dead_code)] // Keep this around for debugging purposes
pub(crate) fn display(self, db: &'db dyn Db) -> impl Display {
self.node.simplify_for_display(db).display(db)
}

View File

@@ -18,7 +18,7 @@ use crate::types::class::{
CodeGeneratorKind, DisjointBase, DisjointBaseKind, Field, MethodDecorator,
};
use crate::types::function::{FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral};
use crate::types::liskov::MethodKind;
use crate::types::overrides::MethodKind;
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
@@ -121,6 +121,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&MISSING_TYPED_DICT_KEY);
registry.register_lint(&INVALID_METHOD_OVERRIDE);
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -544,7 +545,8 @@ declare_lint! {
///
/// ## Why is this bad?
/// An invalidly defined `NamedTuple` class may lead to the type checker
/// drawing incorrect conclusions. It may also lead to `TypeError`s at runtime.
/// drawing incorrect conclusions. It may also lead to `TypeError`s or
/// `AttributeError`s at runtime.
///
/// ## Examples
/// A class definition cannot combine `NamedTuple` with other base classes
@@ -557,6 +559,27 @@ declare_lint! {
/// >>> class Foo(NamedTuple, object): ...
/// TypeError: can only inherit from a NamedTuple type and Generic
/// ```
///
/// Further, `NamedTuple` field names cannot start with an underscore:
///
/// ```pycon
/// >>> from typing import NamedTuple
/// >>> class Foo(NamedTuple):
/// ... _bar: int
/// ValueError: Field names cannot start with an underscore: '_bar'
/// ```
///
/// `NamedTuple` classes also have certain synthesized attributes (like `_asdict`, `_make`,
/// `_replace`, etc.) that cannot be overwritten. Attempting to assign to these attributes
/// without a type annotation will raise an `AttributeError` at runtime.
///
/// ```pycon
/// >>> from typing import NamedTuple
/// >>> class Foo(NamedTuple):
/// ... x: int
/// ... _asdict = 42
/// AttributeError: Cannot overwrite NamedTuple attribute _asdict
/// ```
pub(crate) static INVALID_NAMED_TUPLE = {
summary: "detects invalid `NamedTuple` class definitions",
status: LintStatus::stable("0.0.1-alpha.19"),
@@ -1760,6 +1783,33 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to `super()` inside methods of `NamedTuple` classes.
///
/// ## Why is this bad?
/// Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime.
///
/// ## Examples
/// ```python
/// from typing import NamedTuple
///
/// class F(NamedTuple):
/// x: int
///
/// def method(self):
/// super() # error: super() is not supported in methods of NamedTuple classes
/// ```
///
/// ## References
/// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)
pub(crate) static SUPER_CALL_IN_NAMED_TUPLE_METHOD = {
summary: "detects `super()` calls in methods of `NamedTuple` classes",
status: LintStatus::preview("0.0.1-alpha.30"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to `reveal_type` without importing it.
@@ -3501,7 +3551,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'db>(
context: &InferContext<'db, '_>,
class: ClassLiteral<'db>,
(field, field_def): &(Name, Option<Definition<'db>>),
(field, field_def): (&str, Option<Definition<'db>>),
(field_with_default, field_with_default_def): &(Name, Option<Definition<'db>>),
) {
let db = context.db();
@@ -3514,9 +3564,9 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'
let Some(builder) = context.report_lint(&INVALID_NAMED_TUPLE, diagnostic_range) else {
return;
};
let mut diagnostic = builder.into_diagnostic(format_args!(
let mut diagnostic = builder.into_diagnostic(
"NamedTuple field without default value cannot follow field(s) with default value(s)",
));
);
diagnostic.set_primary_message(format_args!(
"Field `{field}` defined here without a default value",
@@ -3547,6 +3597,40 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'
}
}
pub(super) fn report_named_tuple_field_with_leading_underscore<'db>(
context: &InferContext<'db, '_>,
class: ClassLiteral<'db>,
field_name: &str,
field_definition: Option<Definition<'db>>,
) {
let db = context.db();
let module = context.module();
let diagnostic_range = field_definition
.map(|definition| definition.kind(db).full_range(module))
.unwrap_or_else(|| class.header_range(db));
let Some(builder) = context.report_lint(&INVALID_NAMED_TUPLE, diagnostic_range) else {
return;
};
let mut diagnostic =
builder.into_diagnostic("NamedTuple field name cannot start with an underscore");
if field_definition.is_some() {
diagnostic.set_primary_message(
"Class definition will raise `TypeError` at runtime due to this field",
);
} else {
diagnostic.set_primary_message(format_args!(
"Class definition will raise `TypeError` at runtime due to field `{field_name}`",
));
}
diagnostic.set_concise_message(format_args!(
"NamedTuple field `{field_name}` cannot start with an underscore"
));
}
pub(crate) fn report_missing_typed_dict_key<'db>(
context: &InferContext<'db, '_>,
constructor_node: AnyNodeRef,
@@ -3804,6 +3888,7 @@ pub(super) fn report_overridden_final_method<'db>(
context: &InferContext<'db, '_>,
member: &str,
subclass_definition: Definition<'db>,
// N.B. the type of the *definition*, not the type on an instance of the subclass
subclass_type: Type<'db>,
superclass: ClassType<'db>,
subclass: ClassType<'db>,
@@ -3811,6 +3896,23 @@ pub(super) fn report_overridden_final_method<'db>(
) {
let db = context.db();
// Some hijinks so that we emit a diagnostic on the property getter rather than the property setter
let property_getter_definition = if subclass_definition.kind(db).is_function_def()
&& let Type::PropertyInstance(property) = subclass_type
&& let Some(Type::FunctionLiteral(getter)) = property.getter(db)
{
let getter_definition = getter.definition(db);
if getter_definition.scope(db) == subclass_definition.scope(db) {
Some(getter_definition)
} else {
None
}
} else {
None
};
let subclass_definition = property_getter_definition.unwrap_or(subclass_definition);
let Some(builder) = context.report_lint(
&OVERRIDE_OF_FINAL_METHOD,
subclass_definition.focus_range(db, context.module()),
@@ -3871,37 +3973,69 @@ pub(super) fn report_overridden_final_method<'db>(
diagnostic.sub(sub);
let underlying_function = match subclass_type {
Type::FunctionLiteral(function) => Some(function),
Type::BoundMethod(method) => Some(method.function(db)),
_ => None,
};
// It's tempting to autofix properties as well,
// but you'd want to delete the `@my_property.deleter` as well as the getter and the deleter,
// and we don't model property deleters at all right now.
if let Type::FunctionLiteral(function) = subclass_type {
let class_node = subclass
.class_literal(db)
.0
.body_scope(db)
.node(db)
.expect_class()
.node(context.module());
let (overloads, implementation) = function.overloads_and_implementation(db);
let overload_count = overloads.len() + usize::from(implementation.is_some());
let is_only = overload_count >= class_node.body.len();
if let Some(function) = underlying_function {
let overload_deletion = |overload: &OverloadLiteral<'db>| {
Edit::range_deletion(overload.node(db, context.file(), context.module()).range())
let range = overload.node(db, context.file(), context.module()).range();
if is_only {
Edit::range_replacement("pass".to_string(), range)
} else {
Edit::range_deletion(range)
}
};
let should_fix = overloads
.iter()
.copied()
.chain(implementation)
.all(|overload| {
class_node
.body
.iter()
.filter_map(ast::Stmt::as_function_def_stmt)
.contains(overload.node(db, context.file(), context.module()))
});
match function.overloads_and_implementation(db) {
([first_overload, rest @ ..], None) => {
diagnostic.help(format_args!("Remove all overloads for `{member}`"));
diagnostic.set_fix(Fix::unsafe_edits(
overload_deletion(first_overload),
rest.iter().map(overload_deletion),
));
diagnostic.set_optional_fix(should_fix.then(|| {
Fix::unsafe_edits(
overload_deletion(first_overload),
rest.iter().map(overload_deletion),
)
}));
}
([first_overload, rest @ ..], Some(implementation)) => {
diagnostic.help(format_args!(
"Remove all overloads and the implementation for `{member}`"
));
diagnostic.set_fix(Fix::unsafe_edits(
overload_deletion(first_overload),
rest.iter().chain([&implementation]).map(overload_deletion),
));
diagnostic.set_optional_fix(should_fix.then(|| {
Fix::unsafe_edits(
overload_deletion(first_overload),
rest.iter().chain([&implementation]).map(overload_deletion),
)
}));
}
([], Some(implementation)) => {
diagnostic.help(format_args!("Remove the override of `{member}`"));
diagnostic.set_fix(Fix::unsafe_edit(overload_deletion(&implementation)));
diagnostic.set_optional_fix(
should_fix.then(|| Fix::unsafe_edit(overload_deletion(&implementation))),
);
}
([], None) => {
// Should be impossible to get here: how would we even infer a function as a function
@@ -3911,11 +4045,12 @@ pub(super) fn report_overridden_final_method<'db>(
);
}
}
} else if let Type::PropertyInstance(property) = subclass_type
&& property.setter(db).is_some()
{
diagnostic.help(format_args!("Remove the getter and setter for `{member}`"));
} else {
diagnostic.help(format_args!("Remove the override of `{member}`"));
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion(
subclass_definition.full_range(db, context.module()).range(),
)));
}
}

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