Compare commits

...

60 Commits

Author SHA1 Message Date
Charlie Marsh
87eec9bb51 [ty] Show dynamic NamedTuple defaults in signature (#22574)
## Summary

Follow-up from https://github.com/astral-sh/ruff/pull/22327.
2026-01-14 18:28:23 +00:00
Micha Reiser
eaed0d9b5c [ty] Fix flaky completions (#22576) 2026-01-14 19:23:38 +01:00
Charlie Marsh
eb96456e1e [ty] Synthesize an empty __slots__ for named tuples (#22573)
## Summary

Closes https://github.com/astral-sh/ty/issues/2490.
2026-01-14 18:22:27 +00:00
Charlie Marsh
3e0299488e [ty] Add support for functional namedtuple creation (#22327)
## Summary

This PR is intended to demonstrate how the pattern established in
https://github.com/astral-sh/ruff/pull/22291 generalizes to other class
"kinds".

Closes https://github.com/astral-sh/ty/issues/1049.

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2026-01-14 17:41:04 +00:00
Alex Waygood
7f0ce3e88d [ty] Infer type[Unknown] for calls to type() when overload evaluation is ambiguous (#22569)
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2026-01-14 14:23:07 +00:00
Charlie Marsh
ba0736385d [ty] Add a diagnostic for non-decorator uses of final (#22555)
## Summary

See:
https://github.com/astral-sh/ruff/pull/22499#discussion_r2687263390.
2026-01-14 09:14:59 -05:00
Charlie Marsh
e41f045ec5 [ty] Emit diagnostics for invalid base classes in type(...) (#22499)
## Summary

Tackles a few TODOs from https://github.com/astral-sh/ruff/pull/22291.
2026-01-14 08:56:04 -05:00
Charlie Marsh
b24afb643c [ty] Support 'dangling' type(...) constructors (#22537)
## Summary

This PR adds support for 'dangling' `type(...)` constructors, e.g.:

```python
class Foo(type("Bar", ...)):
   ...
```

As opposed to:

```python
Bar = type("Bar", ...)
```

The former doesn't have a `Definition` since it doesn't get bound to a
place, so we instead need to store the `NodeIndex`. Per @MichaReiser's
suggestion, we can use a Salsa tracked struct for this.
2026-01-14 08:48:53 -05:00
Aryan Bagade
853bb00626 [ty] Improve rendering of ReST directives in docstrings (#22512) 2026-01-14 12:33:07 +00:00
Charlie Marsh
b5814b91c1 [ty] Add diagnostics to validate TypeIs and TypeGuard definitions (#22300)
## Summary

Closes https://github.com/astral-sh/ty/issues/2267.
2026-01-13 20:24:05 -05:00
Charlie Marsh
ea46426157 [ty] Apply narrowing to walrus targets (#22369)
## Summary

Closes https://github.com/astral-sh/ty/issues/2300.
2026-01-14 00:56:47 +00:00
Alex Waygood
ddd2fc7a90 [ty] Use "typeguard constraints" for two kinds of tuple narrowing (#22348)
## Summary

Since we've already filtered the union in these locations, it seems like
needless overhead to then intersect the previous union with the filtered
union. We know what that intersection will simplify to: it will simplify
to the filtered union. So rather than using a regular intersection-based
constraint, we can use a "typeguard constraint", which will just
directly replace the previous type with the new type instead of creating
an intersection.

## Test Plan

- Existing tests all pass
- The primer report should be clean
2026-01-13 23:37:09 +00:00
Will Duke
4ebf10cf1b [ty] Add a conformance script to compare ty diagnostics with expected errors (#22231) 2026-01-13 22:19:41 +00:00
Charlie Marsh
9a676bbeb7 [ty] Add diagnostic to catch generic enums (#22482)
## Summary

Closes https://github.com/astral-sh/ty/issues/2416.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2026-01-13 16:55:46 -05:00
Bhuminjay Soni
d9028a098b [isort] Insert imports in alphabetical order (I002) (#22493)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
This PR fixes #20811 , current approach reverses the order in `BtreeSet`
however as pointed in
https://github.com/astral-sh/ruff/issues/20811#issuecomment-3398958832
here we cannot use I`IndexSet` to preserve config order since Settings
derives `CacheKey` which isn't implemented for `IndexSet`, another
approach to preserve the original order might be to use `Vec` however
lookup time complexity might get affected as a result.

<!-- How was it tested? -->
I have tested it locally its working as expected ,
<img width="2200" height="1071" alt="image"
src="https://github.com/user-attachments/assets/7d97b488-1552-4a42-9d90-92acf55ec493"
/>

---------

Signed-off-by: Bhuminjay <bhuminjaysoni@gmail.com>
2026-01-13 21:21:18 +00:00
Alex Waygood
56077ee9a9 [ty] Fix @Todo type for starred expressions (#22503) 2026-01-13 21:09:29 +00:00
Alex Waygood
20c01d2553 [ty] Use the top materialization of classes for if type(x) is y narrowing (#22553) 2026-01-13 20:53:52 +00:00
Harutaka Kawamura
c98ea1bc24 [flake8-pytest-style] Add check parameter example to PT017 docs (#22546)
## Summary

- Adds an alternative example to the PT017 (`pytest-assert-in-except`)
rule documentation showing pytest's `check` parameter for validating
exceptions, available since pytest 8.4.0

Closes #22529

## Test plan

Documentation-only change. Verified with `uvx prek run -a`.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:45:53 -05:00
GiGaGon
9a2990b2a1 [ruff] Make example error out-of-the-box (RUF103) (#22558)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

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

Part of #18972

This PR makes [invalid-suppression-comment
(RUF103)](https://docs.astral.sh/ruff/rules/invalid-suppression-comment/#invalid-suppression-comment-ruf103)'s
example error out-of-the-box.

[Old example](https://play.ruff.rs/3ff757f3-04ae-4d27-986d-49972338fa24)
```py
ruff: disable # missing codes
```

[New example](https://play.ruff.rs/4a9970c4-3b33-4533-8ffa-f15d481b1e6f)
```py
# ruff: disable # missing codes
```

## Test Plan

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

N/A, no functionality/tests affected
2026-01-13 15:06:54 -05:00
Carl Meyer
a697050a83 [ty] Fix stack overflow with recursive type aliases containing tuple … (#22543)
This fixes issue #2470 where recursive type aliases like `type
RecursiveT = int | tuple[RecursiveT, ...]` caused a stack overflow when
used in return type checking with constructors like `list()`.

The fix moves all type mapping processing for `UniqueSpecialization`
(and other non-EagerExpansion mappings) inside the `visitor.visit()`
closure. This ensures that if we encounter the same TypeAlias
recursively during type mapping, the cycle detector will properly detect
it and return the fallback value instead of recursing infinitely.

The key insight is that the previous code called
`apply_function_specialization` followed by another
`apply_type_mapping_impl` AFTER the visitor closure returned. At that
point, the TypeAlias was no longer in the visitor's `seen` set, so
recursive references would not be detected as cycles.
2026-01-13 11:25:01 -08:00
Dex Devlon
2f64ef9c72 [ty] Include type parameters in generic callable display (#22435) 2026-01-13 17:29:08 +00:00
RasmusNygren
fde7d72fbb [ty] Add diagnostics for __init_subclass__ argument mismatch (#22185) 2026-01-13 16:15:51 +00:00
drbh
d13b5db066 [ty] narrow the right-hand side of ==, !=, is and is not conditions when the left-hand side is not narrowable (#22511)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2026-01-13 16:01:54 +00:00
Alex Waygood
c7b41060f4 [ty] Improve disambiguation of types (#22547) 2026-01-13 14:56:56 +00:00
Charlie Marsh
3878701265 [ty] Support own instance members for type(...) classes (#22480)
## Summary

Addresses
https://github.com/astral-sh/ruff/pull/22291#discussion_r2674467950.
2026-01-13 09:36:03 -05:00
Enric Calabuig
6e89e0abff [ty] Fix classmethod + contextmanager + Self (#22407)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

The test I've added illustrates the fix. Copying it here too:

```python
from contextlib import contextmanager
from typing import Iterator
from typing_extensions import Self

class Base:
    @classmethod
    @contextmanager
    def create(cls) -> Iterator[Self]:
        yield cls()

class Child(Base): ...

with Base.create() as base:
    reveal_type(base)  # revealed: Base (after the fix, None before)

with Child.create() as child:
    reveal_type(child)  # revealed: Child (after the fix, None before)
```

Full disclosure: I've used LLMs for this PR, but the result is
thoroughly reviewed by me before submitting. I'm excited about my first
Rust contribution to Astral tools and will address feedback quickly.

Related to https://github.com/astral-sh/ty/issues/2030, I am working on
a fix for the TypeVar case also reported in that issue (by me)

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

## Test Plan

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

Updated mdtests

---------

Co-authored-by: Douglas Creager <dcreager@dcreager.net>
2026-01-13 09:29:58 -05:00
Manuel Jacob
cb31883c5f Correct comment about public functions starting with an underscore. (#22550) 2026-01-13 14:04:19 +00:00
Charlie Marsh
6d8f2864c3 [ty] Rename MRO structs to match static nomenclature (#22549)
## Summary

I didn't want to make the "dynamic" `type(...)` PR any larger, but it
probably makes sense to rename these now that we have `Dynamic`
variants.
2026-01-13 08:53:49 -05:00
Dhruv Manilawala
990d0a8999 [ty] Improve log guidance message for Zed (#22530)
## Summary

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

## Test Plan


https://github.com/user-attachments/assets/5d8d6d03-3451-4403-a6cd-2deba9e796fc
2026-01-13 08:13:11 +00:00
Carl Meyer
99beabdde8 [ty] Fix false positive for bounded type parameters with NewType (#22542)
Fixes https://github.com/astral-sh/ty/issues/2467

When calling a method on an instance of a generic class with bounded
type parameters (e.g., `C[T: K]` where `K` is a NewType), ty was
incorrectly reporting: "Argument type `C[K]` does not satisfy upper
bound `C[T@C]` of type variable `Self`"

The issue was introduced by PR #22105, which moved the catch-all case
for NewType assignments that falls back to the concrete base type. This
case was moved before the TypeVar handling cases, so when checking `K <:
T@C` (where K is a NewType and T@C is a TypeVar with upper bound K):

1. The NewType fallback matched first
2. It delegated to `int` (K's concrete base type)
3. Then checked `int <: T@C`, which checks if `int` satisfies bound `K`
4. But `int` is not assignable to `K` (NewTypes are distinct from their
bases)

The fix moves the NewType fallback case after the TypeVar cases, so
TypeVar handling takes precedence. Now when checking `K <: T@C`, we use
the TypeVar case at line 828 which returns `false` for non-inferable
typevars - but this is correct because the *other* direction (`T@C <:
K`) passes, and for the overall specialization comparison both
directions are checked.
2026-01-12 17:23:31 -08:00
Ibraheem Ahmed
3ae4db3ccd [ty] Support assignment to unions of TypedDicts (#22294)
## Summary

Resolves https://github.com/astral-sh/ty/issues/2265.
2026-01-12 16:10:58 -05:00
Ibraheem Ahmed
8ac5f9d8bc [ty] Use key and value parameter types as type context for __setitem__ dunder calls (#22148)
## Summary

Resolves https://github.com/astral-sh/ty/issues/2136.
2026-01-12 16:05:05 -05:00
Charlie Marsh
4abc5fe2f1 [ty] Add support for dynamic type() classes (#22291)
## Summary

This PR adds support for dynamic classes created via `type()`. The core
of the change is that `ClassLiteral` is now an enum:

```rust
pub enum ClassLiteral<'db> {
    /// A class defined via a `class` statement.
    Stmt(StmtClassLiteral<'db>),
    /// A class created via the functional form `type(name, bases, dict)`.
    Functional(FunctionalClassLiteral<'db>),
}
```

And, in turn, various methods on `ClassLiteral` like `body_scope` now
return `Option` or similar (and callers must adjust to that change in
signature).

Over time, we can expand the enum to include functional namedtuples,
etc. (I already have this working in a separate branch, and I believe it
slots in well.)

(I'd love help with the names -- I think `StmtClassLiteral` is kind of
lame. Maybe `DeclarativeClassLiteral`?)

Closes https://github.com/astral-sh/ty/issues/740.

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2026-01-12 15:20:42 -05:00
renovate[bot]
78ef241200 Update actions/checkout digest to 0c366fd (#22513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2026-01-12 18:36:50 +01:00
Charlie Marsh
e4ba29392b [ty] Fix __file__ type in completions to show str instead of str | None (#22510)
## Summary

The type inference system already correctly special-cases `__file__` to
return `str` for the current module (since the code is executing from an
existing file). However, the completion system was bypassing this logic
and pulling `__file__: str | None` directly from `types.ModuleType` in
typeshed.

This PR adds implicit module globals (like `__file__`, `__name__`, etc.)
with their correctly-typed values to completions, reusing the existing
`module_type_implicit_global_symbol` function that already handles the
special-casing.

Closes https://github.com/astral-sh/ty/issues/2445.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2026-01-12 14:20:32 +00:00
Charlie Marsh
29064034ba Use rustfmt directly in prek (#22508)
## Summary

Apparently this is about ~18x faster (2.25s → 0.12s) for a single-file
change, and ~2x faster (2.36s → 1.25s) for `prek run -a`.
2026-01-12 08:59:20 -05:00
Charlie Marsh
f1db842821 [ty] Avoid panic for comparison on synthesized variants (#22509)
## Summary

Like `ProtocolInstance`, we now use `left.cmp(right)` by deriving
`PartialOrd` and `Ord`. IIUC, this uses Salsa ID for Salsa-interned
types, but avoids `None.cmp(None)` for synthesized variants.

Closes https://github.com/astral-sh/ty/issues/2451.
2026-01-12 13:35:56 +00:00
Alex Waygood
5a3deee353 [ty] Fix incorrect narrowing for if type(x) == y (#22531) 2026-01-12 12:26:47 +00:00
Dex Devlon
e15f88ff21 [ty] Fix contravariant type variable bound checking in specialization inference (#22488)
## Summary

Correctly handle upper bounds for contravariant type variables during
specialization inference. Previously, the type checker incorrectly
applied covariant subtyping rules, requiring the actual type to directly
satisfy the bound rather than checking for a valid intersection.

In contravariant positions, subtyping relationships are inverted. The
bug caused valid code like `f(x: Contra[str])` where `f` expects
`Contra[T: int]` to be incorrectly rejected, when it should solve `T` to
`Never` (the intersection of `int` and `str`).

Closes https://github.com/astral-sh/ty/issues/2427

## Details

- Added `is_contravariant()` helper to `TypeVarVariance` in
`variance.rs`
- Updated `SpecializationBuilder::infer_map_impl` in `generics.rs` to
treat bounds and constraints differently based on variance:
  * Skip immediate `ty <: bound` check for contravariant upper bounds
* Flip constraint check to `constraint <: ty` for contravariant
positions
- Added test case for bounded contravariant type variables in
`variance.md`
- All 308 mdtest cases pass & 150 ty_python_semantic unit tests pass

---------

Co-authored-by: Douglas Creager <dcreager@dcreager.net>
2026-01-12 05:00:35 -05:00
renovate[bot]
a559275c3e Update prek dependencies (#22526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2026-01-12 08:11:01 +00:00
renovate[bot]
95199c4217 Update astral-sh/setup-uv action to v7.2.0 (#22523)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | minor | `v7.1.6` → `v7.2.0` |

---

### Release Notes

<details>
<summary>astral-sh/setup-uv (astral-sh/setup-uv)</summary>

###
[`v7.2.0`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v7.2.0):
🌈 add outputs python-version and python-cache-hit

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v7.1.6...v7.2.0)

##### Changes

Among some minor typo fixes and quality of life features for developers
of actions the main feature of this release are new outputs:

- **python-version:** The Python version that was set (same content as
existing `UV_PYTHON`)
- **python-cache-hit:** A boolean value to indicate the Python cache
entry was found

While implementing this it became clear, that it is easier to handle the
Python binaries in a separate cache entry. The added benefit for users
is that the "normal" cache containing the dependencies can be used in
all runs no matter if these cache the Python binaries or not.

> \[!NOTE]\
> This release will invalidate caches that contain the Python binaries.
This happens a single time.

##### 🐛 Bug fixes

- chore: remove stray space from UV\_PYTHON\_INSTALL\_DIR message
[@&#8203;akx](https://redirect.github.com/akx)
([#&#8203;720](https://redirect.github.com/astral-sh/setup-uv/issues/720))

##### 🚀 Enhancements

- add outputs python-version and python-cache-hit
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;728](https://redirect.github.com/astral-sh/setup-uv/issues/728))
- Add action typings with validation
[@&#8203;krzema12](https://redirect.github.com/krzema12)
([#&#8203;721](https://redirect.github.com/astral-sh/setup-uv/issues/721))

##### 🧰 Maintenance

- fix: use uv\_build backend for old-python-constraint-project
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;729](https://redirect.github.com/astral-sh/setup-uv/issues/729))
- chore: update known checksums for 0.9.22
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;727](https://redirect.github.com/astral-sh/setup-uv/issues/727))
- chore: update known checksums for 0.9.21
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;726](https://redirect.github.com/astral-sh/setup-uv/issues/726))
- chore: update known checksums for 0.9.20
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;725](https://redirect.github.com/astral-sh/setup-uv/issues/725))
- chore: update known checksums for 0.9.18
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;718](https://redirect.github.com/astral-sh/setup-uv/issues/718))

##### ⬆️ Dependency updates

- Bump peter-evans/create-pull-request from 7.0.9 to 8.0.0
@&#8203;[dependabot\[bot\]](https://redirect.github.com/apps/dependabot)
([#&#8203;719](https://redirect.github.com/astral-sh/setup-uv/issues/719))
- Bump github/codeql-action from 4.31.6 to 4.31.9
@&#8203;[dependabot\[bot\]](https://redirect.github.com/apps/dependabot)
([#&#8203;723](https://redirect.github.com/astral-sh/setup-uv/issues/723))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:07:15 +00:00
renovate[bot]
f92a818fdd Update Rust crate insta to v1.46.0 (#22527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 07:56:12 +00:00
renovate[bot]
39ec97df79 Update taiki-e/install-action action to v2.65.13 (#22522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:54:28 +01:00
renovate[bot]
a21988d820 Update NPM Development dependencies (#22525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:53:56 +01:00
renovate[bot]
eab41d5a4c Update dependency ruff to v0.14.11 (#22517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:52:14 +01:00
renovate[bot]
52f4a529f7 Update CodSpeedHQ/action action to v4.5.2 (#22516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:49:54 +01:00
renovate[bot]
8fd142f4ef Update Rust crate libc to v0.2.179 (#22520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:48:43 +01:00
renovate[bot]
5dca6d22df Update Rust crate clap to v4.5.54 (#22518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:48:30 +01:00
renovate[bot]
4c5846c6fe Update Rust crate imperative to v1.0.7 (#22519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:48:06 +01:00
renovate[bot]
0289d1b163 Update Rust crate syn to v2.0.113 (#22521)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:47:25 +01:00
Charlie Marsh
09ff3e7056 Set priority for prek to enable concurrent execution (#22506)
## Summary

prek allows you to set priorities, and can run tasks of the same
priority concurrently (e.g., we can run Ruff's Python formatting and
`cargo fmt` at the same time). On my machine, this takes `uvx prek run
-a` from 19.4s to 5.0s (~a 4x speed-up).
2026-01-11 19:21:38 +00:00
Charlie Marsh
7bacca9b62 Use prek in documentation and CI (#22505)
## Summary

AFAIK, many of us on the team are using prek now. It seems appropriate
to modify the docs to formalize it.
2026-01-11 14:17:10 -05:00
Charlie Marsh
2c68057c4b [ty] Preserve argument signature in @total_ordering (#22496)
## Summary

Closes https://github.com/astral-sh/ty/issues/2435.
2026-01-10 14:35:58 -05:00
Charlie Marsh
8e29be9c1c Add let chains preference to development guidelines (#22497) 2026-01-10 12:29:07 -05:00
Dylan
880513a013 Respect fmt: skip for multiple statements on same logical line (#22119)
This PR adjusts the logic for skipping formatting so that a `fmt: skip`
can affect multiple statements if they lie on the same line.

Specifically, a `fmt: skip` comment will now suppress all the statements
in the suite in which it appears whose range intersects the line
containing the skip directive. For example:

```python
x=[
'1'
];x=2 # fmt: skip
```

remains unchanged after formatting.

(Note that compound statements are somewhat special and were handled in
a previous PR - see #20633).


Closes #17331 and #11430.

Simplest to review commit by commit - the key diffs of interest are the
commit introducing the core logic, and the diff between the snapshots
introduced in the last commit (compared to the second commit).

# Implementation

On `main` we format a suite of statements by iterating through them. If
we meet a statement with a leading or trailing (own-line)`fmt: off`
comment, then we suppress formatting until we meet a `fmt: on` comment.
Otherwise we format the statement using its own formatting rule.

How are `fmt: skip` comments handled then? They are handled internally
to the formatting of each statement. Specifically, calling `.fmt` on a
statement node will first check to see if there is a trailing,
end-of-line `fmt: skip` (or `fmt: off`/`yapf: off`), and if so then
write the node with suppressed formatting.

In this PR we move the responsibility for handling `fmt: skip` into the
formatting logic of the suite itself. This is done as follows:

- Before beginning to format the suite, we do a pass through the
statements and collect the data of ranges with skipped formatting. More
specifically, we create a map with key given by the _first_ skipped
statement in a block and value a pair consisting of the _last_ skipped
statement and the _range_ to write verbatim.
- We iterate as before, but if we meet a statement that is a key in the
map constructed above, we pause to write the associated range verbatim.
We then advance the iterator to the last statement in the block and
proceed as before.

## Addendum on range formatting

We also had to make some changes to range formatting in order to support
this new behavior. For example, we want to make sure that

```python
<RANGE_START>x=1<RANGE_END>;x=2 # fmt: skip
```

formats verbatim, rather than becoming 

```python
x = 1;x=2 # fmt: skip
```

Recall that range formatting proceeds in two steps:
1. Find the smallest enclosing node containing the range AND that has
enough info to format the range (so it may be larger than you think,
e.g. a docstring has enclosing node given by the suite, not the string
itself.)
2. Carve out the formatted range from the result of formatting that
enclosing node.

We had to modify (1), since the suite knows how to format skipped nodes,
but nodes may not "know" they are skipped. To do this we altered the
`visit_body` bethod of the `FindEnclosingNode` visitor: now we iterate
through the statements and check for skipped ranges intersecting the
format range. If we find them, we return without descending. The result
is to consider the statement containing the suite as the enclosing node
in this case.
2026-01-10 14:56:58 +00:00
Charlie Marsh
046c5a46d8 [ty] Support dataclass_transform as a function call (#22378)
## Summary

Instead of just as a decorator.

Closes https://github.com/astral-sh/ty/issues/2319.
2026-01-10 08:45:45 -05:00
Micha Reiser
cfed34334c Update mypy primer pin (#22490) 2026-01-10 14:00:00 +01:00
Charlie Marsh
11cc324449 [ty] Detect invalid @total_ordering applications in non-decorator contexts (#22486)
## Summary

E.g., `ValidOrderedClass = total_ordering(HasOrderingMethod)`.
2026-01-09 19:37:58 -05:00
Charlie Marsh
c88e1a0663 [ty] Avoid emitting Liskov repeated violations from grandparent to child (#22484)
## Summary

If parent violates LSP against grandparent, and child has the same
violation (but matches parent), we no longer flag the LSP violation on
child, since it can't be fixed without violating parent.

If parent violates LSP against grandparent, and child violates LSP
against both parent and grandparent, we emit two diagnostics (one for
each violation).

If parent violates LSP against grandparent, and child violates LSP
against parent (but not grandparent), we flag it.

Closes https://github.com/astral-sh/ty/issues/2000.
2026-01-09 19:27:57 -05:00
William Woodruff
2c7ac17b1e Remove two old zizmor ignore comments (#22485) 2026-01-09 18:02:44 -05:00
173 changed files with 11854 additions and 2462 deletions

View File

@@ -5,4 +5,4 @@ rustup component add clippy rustfmt
cargo install cargo-insta
cargo fetch
pip install maturin pre-commit
pip install maturin prek

View File

@@ -1,4 +1,4 @@
# Configuration for the actionlint tool, which we run via pre-commit
# Configuration for the actionlint tool, which we run via prek
# to verify the correctness of the syntax in our GitHub Actions workflows.
self-hosted-runner:
@@ -17,4 +17,4 @@ self-hosted-runner:
paths:
".github/workflows/mypy_primer.yaml":
ignore:
- 'condition "false" is always evaluated to false. remove the if: section'
- 'constant expression "false" in condition. remove the if: section'

View File

@@ -6,3 +6,4 @@
possibly-unresolved-reference = "warn"
possibly-missing-import = "warn"
division-by-zero = "warn"
unsupported-dynamic-base = "warn"

View File

@@ -76,9 +76,9 @@
enabled: false,
},
{
groupName: "pre-commit dependencies",
groupName: "prek dependencies",
matchManagers: ["pre-commit"],
description: "Weekly update of pre-commit dependencies",
description: "Weekly update of prek dependencies",
},
{
groupName: "NPM Development dependencies",

View File

@@ -281,15 +281,15 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
enable-cache: "true"
- name: ty mdtests (GitHub annotations)
@@ -343,11 +343,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
enable-cache: "true"
- name: "Run tests"
@@ -376,11 +376,11 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
enable-cache: "true"
- name: "Run tests"
@@ -486,7 +486,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ruff-linux-debug
@@ -521,7 +521,7 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -561,7 +561,7 @@ jobs:
ref: ${{ github.event.pull_request.base.ref }}
persist-credentials: false
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
activate-environment: true
@@ -667,7 +667,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -726,7 +726,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -769,32 +769,29 @@ jobs:
- name: "Remove wheels from cache"
run: rm -rf target/wheels
pre-commit:
name: "pre-commit"
prek:
name: "prek"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
timeout-minutes: 10
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 24
- name: "Cache pre-commit"
- name: "Cache prek"
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: "Run pre-commit"
path: ~/.cache/prek
key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
- name: "Run prek"
run: |
echo '```console' > "$GITHUB_STEP_SUMMARY"
# Enable color output for pre-commit and remove it for the summary
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
SKIP=cargo-fmt uvx --python="${PYTHON_VERSION}" pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
# Enable color output for prek and remove it for the summary
# Use --hook-stage=manual to enable slower hooks that are skipped by default
SKIP=rustfmt uvx prek run --all-files --show-diff-on-failure --color always --hook-stage manual | \
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
exit_code="${PIPESTATUS[0]}"
echo '```' >> "$GITHUB_STEP_SUMMARY"
@@ -814,7 +811,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
python-version: 3.13
activate-environment: true
@@ -966,13 +963,13 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
with:
tool: cargo-codspeed
@@ -980,7 +977,7 @@ jobs:
run: cargo codspeed build --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@972e3437949c89e1357ebd1a2dbc852fcbc57245 # v4.5.1
uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # v4.5.2
with:
mode: simulation
run: cargo codspeed run
@@ -1011,7 +1008,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
with:
tool: cargo-codspeed
@@ -1044,10 +1041,10 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- name: "Install codspeed"
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
with:
tool: cargo-codspeed
@@ -1061,7 +1058,7 @@ jobs:
run: chmod +x target/codspeed/simulation/ruff_benchmark/ty
- name: "Run benchmarks"
uses: CodSpeedHQ/action@972e3437949c89e1357ebd1a2dbc852fcbc57245 # v4.5.1
uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # v4.5.2
with:
mode: simulation
run: cargo codspeed run --bench ty "${{ matrix.benchmark }}"
@@ -1092,13 +1089,13 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
with:
tool: cargo-codspeed
@@ -1133,10 +1130,10 @@ jobs:
with:
persist-credentials: false
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- name: "Install codspeed"
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
with:
tool: cargo-codspeed
@@ -1150,7 +1147,7 @@ jobs:
run: chmod +x target/codspeed/walltime/ruff_benchmark/ty_walltime
- name: "Run benchmarks"
uses: CodSpeedHQ/action@972e3437949c89e1357ebd1a2dbc852fcbc57245 # v4.5.1
uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # v4.5.2
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

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"

View File

@@ -48,7 +48,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
@@ -87,7 +87,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
@@ -129,7 +129,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: wheels-*

View File

@@ -60,7 +60,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
submodules: recursive
@@ -133,7 +133,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
submodules: recursive
@@ -185,7 +185,7 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
submodules: recursive
@@ -261,7 +261,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
submodules: recursive

View File

@@ -76,7 +76,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- name: Sync typeshed stubs
run: |
rm -rf "ruff/${VENDORED_TYPESHED}"
@@ -130,7 +130,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -169,7 +169,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
- name: Setup git
run: |
git config --global user.name typeshedbot

View File

@@ -38,14 +38,14 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
enable-cache: true
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
lookup-only: false
- name: Install Rust toolchain
run: rustup show

View File

@@ -32,7 +32,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
enable-cache: true

View File

@@ -21,15 +21,61 @@ exclude: |
)$
repos:
# Priority 0: Read-only hooks; hooks that modify disjoint file types.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-merge-conflict
priority: 0
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
hooks:
- id: validate-pyproject
priority: 0
- repo: https://github.com/crate-ci/typos
rev: v1.41.0
hooks:
- id: typos
priority: 0
- repo: local
hooks:
- id: rustfmt
name: rustfmt
entry: rustfmt
language: system
types: [rust]
priority: 0
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.7.4
hooks:
- id: prettier
types: [yaml]
priority: 0
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.19.0
hooks:
- id: zizmor
priority: 0
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.36.0
hooks:
- id: check-github-workflows
priority: 0
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck
priority: 0
- repo: https://github.com/executablebooks/mdformat
rev: 1.0.0
@@ -44,7 +90,20 @@ repos:
docs/formatter/black\.md
| docs/\w+\.md
)$
priority: 0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
hooks:
- id: ruff-format
priority: 0
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, pyi]
require_serial: true
priority: 1
# Priority 1: Second-pass fixers (e.g., markdownlint-fix runs after mdformat).
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.47.0
hooks:
@@ -54,7 +113,9 @@ repos:
docs/formatter/black\.md
| docs/\w+\.md
)$
priority: 1
# Priority 2: blacken-docs runs after markdownlint-fix (both modify markdown).
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.20.0
hooks:
@@ -68,59 +129,18 @@ repos:
)$
additional_dependencies:
- black==25.12.0
- repo: https://github.com/crate-ci/typos
rev: v1.40.0
hooks:
- id: typos
- repo: local
hooks:
- id: cargo-fmt
name: cargo fmt
entry: cargo fmt --
language: system
types: [rust]
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
hooks:
- id: ruff-format
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, pyi]
require_serial: true
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.7.4
hooks:
- id: prettier
types: [yaml]
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.19.0
hooks:
- id: zizmor
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.36.0
hooks:
- id: check-github-workflows
priority: 2
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
- repo: https://github.com/rhysd/actionlint
rev: v1.7.9
rev: v1.7.10
hooks:
- id: actionlint
stages:
# This hook is disabled by default, since it's quite slow.
# To run all hooks *including* this hook, use `uvx pre-commit run -a --hook-stage=manual`.
# To run *just* this hook, use `uvx pre-commit run -a actionlint --hook-stage=manual`.
# To run all hooks *including* this hook, use `uvx prek run -a --hook-stage=manual`.
# To run *just* this hook, use `uvx prek run -a actionlint --hook-stage=manual`.
- manual
args:
- "-ignore=SC2129" # ignorable stylistic lint from shellcheck
@@ -131,8 +151,4 @@ repos:
# and checks these with shellcheck. This is arguably its most useful feature,
# but the integration only works if shellcheck is installed
- "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.11.1"
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck
priority: 0

View File

@@ -65,6 +65,7 @@ When working on ty, PR titles should start with `[ty]` and be tagged with the `t
- All changes must be tested. If you're not testing your changes, you're not done.
- Get your tests to pass. If you didn't run the tests, your code does not work.
- Follow existing code style. Check neighboring files for patterns.
- Always run `uvx pre-commit run -a` at the end of a task.
- Always run `uvx prek run -a` at the end of a task.
- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first.
- Avoid falling back to patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system.
- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability.

View File

@@ -53,12 +53,12 @@ cargo install cargo-insta
You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/) (or `pipx` and `pip`) to
run Python utility commands.
You can optionally install pre-commit hooks to automatically run the validation checks
You can optionally install hooks to automatically run the validation checks
when making a commit:
```shell
uv tool install pre-commit
pre-commit install
uv tool install prek
prek install
```
We recommend [nextest](https://nexte.st/) to run Ruff's test suite (via `cargo nextest run`),
@@ -85,7 +85,7 @@ and that it passes both the lint and test validation checks:
```shell
cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting
RUFF_UPDATE_SCHEMA=1 cargo test # Rust testing and updating ruff.schema.json
uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
uvx prek run -a # Rust and Python formatting, Markdown and Python linting, etc.
```
These checks will run on GitHub Actions when you open your pull request, but running them locally
@@ -381,7 +381,7 @@ Commit each step of this process separately for easier review.
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details
- Square brackets (eg, `[ruff]` project name) will be automatically escaped by `pre-commit`
- Square brackets (eg, `[ruff]` project name) will be automatically escaped by `prek`
Additionally, for minor releases:

56
Cargo.lock generated
View File

@@ -466,9 +466,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.53"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
"clap_derive",
@@ -476,9 +476,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.53"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [
"anstream",
"anstyle",
@@ -1583,11 +1583,11 @@ dependencies = [
[[package]]
name = "imperative"
version = "1.0.6"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29a1f6526af721f9aec9ceed7ab8ebfca47f3399d08b80056c2acca3fcb694a9"
checksum = "35e1d0bd9c575c52e59aad8e122a11786e852a154678d0c86e9e243d55273970"
dependencies = [
"phf",
"phf 0.13.1",
"rust-stemmers",
]
@@ -1648,9 +1648,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.45.1"
version = "1.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "983e3b24350c84ab8a65151f537d67afbbf7153bb9f1110e03e9fa9b07f67a5c"
checksum = "1b66886d14d18d420ab5052cbff544fc5d34d0b2cdd35eb5976aaa10a4a472e5"
dependencies = [
"console 0.15.11",
"once_cell",
@@ -1874,9 +1874,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.178"
version = "0.2.179"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
[[package]]
name = "libcst"
@@ -2488,7 +2488,17 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_shared",
"phf_shared 0.11.3",
]
[[package]]
name = "phf"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_shared 0.13.1",
"serde",
]
[[package]]
@@ -2498,7 +2508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator",
"phf_shared",
"phf_shared 0.11.3",
]
[[package]]
@@ -2507,7 +2517,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"phf_shared 0.11.3",
"rand 0.8.5",
]
@@ -2520,6 +2530,15 @@ dependencies = [
"siphasher",
]
[[package]]
name = "phf_shared"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -3286,7 +3305,6 @@ dependencies = [
"compact_str",
"get-size2",
"is-macro",
"itertools 0.14.0",
"memchr",
"ruff_cache",
"ruff_macros",
@@ -3994,9 +4012,9 @@ checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
[[package]]
name = "syn"
version = "2.0.111"
version = "2.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
dependencies = [
"proc-macro2",
"quote",
@@ -4060,7 +4078,7 @@ checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
dependencies = [
"fnv",
"nom",
"phf",
"phf 0.11.3",
"phf_codegen",
]
@@ -4808,7 +4826,7 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd"
dependencies = [
"phf",
"phf 0.11.3",
"unicode_names2_generator",
]

View File

@@ -231,7 +231,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
1657,
1700,
);
#[track_caller]

View File

@@ -13,7 +13,10 @@ impl<T, C> AsFormat<C> for &T
where
T: AsFormat<C>,
{
type Format<'a> = T::Format<'a> where Self: 'a;
type Format<'a>
= T::Format<'a>
where
Self: 'a;
fn format(&self) -> Self::Format<'_> {
AsFormat::format(&**self)

View File

@@ -106,6 +106,16 @@ impl Violation for PytestCompositeAssertion {
/// assert exc_info.value.args
/// ```
///
/// Or, for pytest 8.4.0 and later:
/// ```python
/// import pytest
///
///
/// def test_foo():
/// with pytest.raises(ZeroDivisionError, check=lambda e: e.args):
/// 1 / 0
/// ```
///
/// ## References
/// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises)
#[derive(ViolationMetadata)]

View File

@@ -101,7 +101,7 @@ pub(crate) fn private_member_access(checker: &Checker, expr: &Expr) {
}
}
// Allow some documented private methods, like `os._exit()`.
// Allow some public functions whose names start with an underscore, like `os._exit()`.
if let Some(qualified_name) = semantic.resolve_qualified_name(expr) {
if matches!(qualified_name.segments(), ["os", "_exit"]) {
return;

View File

@@ -140,7 +140,7 @@ pub(crate) fn add_required_imports(
source_type: PySourceType,
context: &LintContext,
) {
for required_import in &settings.isort.required_imports {
for required_import in settings.isort.required_imports.iter().rev() {
add_required_import(
required_import,
parsed,

View File

@@ -1,14 +1,6 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
I002 [*] Missing required import: `from __future__ import annotations`
--> docstring.py:1:1
help: Insert required import: `from __future__ import annotations`
1 | """Hello, world!"""
2 + from __future__ import annotations
3 |
4 | x = 1
I002 [*] Missing required import: `from __future__ import generator_stop`
--> docstring.py:1:1
help: Insert required import: `from __future__ import generator_stop`
@@ -16,3 +8,11 @@ help: Insert required import: `from __future__ import generator_stop`
2 + from __future__ import generator_stop
3 |
4 | x = 1
I002 [*] Missing required import: `from __future__ import annotations`
--> docstring.py:1:1
help: Insert required import: `from __future__ import annotations`
1 | """Hello, world!"""
2 + from __future__ import annotations
3 |
4 | x = 1

View File

@@ -1,15 +1,6 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
I002 [*] Missing required import: `from __future__ import annotations`
--> multiple_strings.py:1:1
help: Insert required import: `from __future__ import annotations`
1 | """This is a docstring."""
2 + from __future__ import annotations
3 | "This is not a docstring."
4 | "This is also not a docstring."
5 |
I002 [*] Missing required import: `from __future__ import generator_stop`
--> multiple_strings.py:1:1
help: Insert required import: `from __future__ import generator_stop`
@@ -17,4 +8,13 @@ help: Insert required import: `from __future__ import generator_stop`
2 + from __future__ import generator_stop
3 | "This is not a docstring."
4 | "This is also not a docstring."
5 |
I002 [*] Missing required import: `from __future__ import annotations`
--> multiple_strings.py:1:1
help: Insert required import: `from __future__ import annotations`
1 | """This is a docstring."""
2 + from __future__ import annotations
3 | "This is not a docstring."
4 | "This is also not a docstring."
5 |

View File

@@ -398,17 +398,17 @@ mod tests {
1 + from pipes import Template
2 + from shlex import quote
I002 [*] Missing required import: `from __future__ import generator_stop`
--> <filename>:1:1
help: Insert required import: `from __future__ import generator_stop`
1 + from __future__ import generator_stop
2 | from pipes import quote, Template
I002 [*] Missing required import: `from collections import Sequence`
--> <filename>:1:1
help: Insert required import: `from collections import Sequence`
1 + from collections import Sequence
2 | from pipes import quote, Template
I002 [*] Missing required import: `from __future__ import generator_stop`
--> <filename>:1:1
help: Insert required import: `from __future__ import generator_stop`
1 + from __future__ import generator_stop
2 | from pipes import quote, Template
");
}

View File

@@ -12,7 +12,7 @@ use crate::suppression::{InvalidSuppressionKind, ParseErrorKind};
///
/// ## Example
/// ```python
/// ruff: disable # missing codes
/// # ruff: disable # missing codes
/// ```
///
/// Use instead:

View File

@@ -28,7 +28,6 @@ bitflags = { workspace = true }
compact_str = { workspace = true }
get-size2 = { workspace = true, optional = true }
is-macro = { workspace = true }
itertools = { workspace = true }
memchr = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true, optional = true }

View File

@@ -14,7 +14,6 @@ use std::slice::{Iter, IterMut};
use std::sync::OnceLock;
use bitflags::bitflags;
use itertools::Itertools;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@@ -3380,10 +3379,13 @@ impl Arguments {
/// 2
/// {'4': 5}
/// ```
pub fn arguments_source_order(&self) -> impl Iterator<Item = ArgOrKeyword<'_>> {
let args = self.args.iter().map(ArgOrKeyword::Arg);
let keywords = self.keywords.iter().map(ArgOrKeyword::Keyword);
args.merge_by(keywords, |left, right| left.start() <= right.start())
pub fn arguments_source_order(&self) -> ArgumentsSourceOrder<'_> {
ArgumentsSourceOrder {
args: &self.args,
keywords: &self.keywords,
next_arg: 0,
next_keyword: 0,
}
}
pub fn inner_range(&self) -> TextRange {
@@ -3399,6 +3401,38 @@ impl Arguments {
}
}
/// The iterator returned by [`Arguments::arguments_source_order`].
#[derive(Clone)]
pub struct ArgumentsSourceOrder<'a> {
args: &'a [Expr],
keywords: &'a [Keyword],
next_arg: usize,
next_keyword: usize,
}
impl<'a> Iterator for ArgumentsSourceOrder<'a> {
type Item = ArgOrKeyword<'a>;
fn next(&mut self) -> Option<Self::Item> {
let arg = self.args.get(self.next_arg);
let keyword = self.keywords.get(self.next_keyword);
if let Some(arg) = arg
&& keyword.is_none_or(|keyword| arg.start() <= keyword.start())
{
self.next_arg += 1;
Some(ArgOrKeyword::Arg(arg))
} else if let Some(keyword) = keyword {
self.next_keyword += 1;
Some(ArgOrKeyword::Keyword(keyword))
} else {
None
}
}
}
impl FusedIterator for ArgumentsSourceOrder<'_> {}
/// An AST node used to represent a sequence of type parameters.
///
/// For example, given:

View File

@@ -0,0 +1,133 @@
class Simple:
x=1
x=2 # fmt: skip
x=3
class Semicolon:
x=1
x=2;x=3 # fmt: skip
x=4
class TrailingSemicolon:
x=1
x=2;x=3 ; # fmt: skip
x=4
class SemicolonNewLogicalLine:
x=1;
x=2;x=3 # fmt: skip
x=4
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
def foo(): y=1 # fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
x=1
x = [
'1',
'2',
] # fmt: skip
class MultiLineSemicolon:
x=1
x = [
'1',
'2',
]; x=2 # fmt: skip
class LineContinuationSemicolonAfter:
x=1
x = ['a']\
; y=1 # fmt: skip
class LineContinuationSemicolonBefore:
x=1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewline:
x=1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewlineAndComment:
x=1
x = ['a']; \
# 1
y=1 # fmt: skip
class RepeatedLineContinuation:
x=1
x = ['a']; \
\
\
y=1 # fmt: skip
class MultiLineSemicolonComments:
x=1
# 1
x = [ # 2
'1', # 3
'2',
# 4
]; x=2 # 5 # fmt: skip # 6
class DocstringSkipped:
'''This is a docstring''' # fmt: skip
x=1
class MultilineDocstringSkipped:
'''This is a docstring
''' # fmt: skip
x=1
class FirstStatementNewlines:
x=1 # fmt: skip
class ChainingSemicolons:
x=[
'1',
'2',
'3',
];x=1;x=[
'1',
'2',
'3'
];x=1;x=1 # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3
'2',
'3'
] ;x=2;x=3 # 4 # fmt: skip # 5
# 6
class MixingCompound:
def foo(): bar(); import zoo # fmt: skip
# https://github.com/astral-sh/ruff/issues/17331
def main() -> None:
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
# https://github.com/astral-sh/ruff/issues/11430
print(); print() # noqa # fmt: skip

View File

@@ -0,0 +1,22 @@
# 0
'''Module docstring
multiple lines''' # 1 # fmt: skip # 2
import a;import b; from c import (
x,y,z
); import f # fmt: skip
x=1;x=2;x=3;x=4 # fmt: skip
# 1
x=[ # 2
'1', # 3
'2',
'3'
];x=1;x=1 # 4 # fmt: skip # 5
# 6
def foo(): x=[
'1',
'2',
];x=1 # fmt: skip

View File

@@ -0,0 +1,68 @@
class Simple:
# Range comprises skip range
x=1
<RANGE_START>x=2 <RANGE_END># fmt: skip
x=3
class Semicolon:
# Range is part of skip range
x=1
x=2;<RANGE_START>x=3<RANGE_END> # fmt: skip
x=4
class FormatFirst:
x=1
<RANGE_START>x=2<RANGE_END>;x=3 # fmt: skip
x=4
class FormatMiddle:
x=1
x=2;<RANGE_START>x=3<RANGE_END>;x=4 # fmt: skip
x=5
class SemicolonNewLogicalLine:
# Range overlaps on right side
<RANGE_START>x=1;
x=2<RANGE_END>;x=3 # fmt: skip
x=4
class SemicolonNewLogicalLine:
# Range overlaps on left side
x=1;
x=2;<RANGE_START>x=3 # fmt: skip
x=4<RANGE_END>
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
<RANGE_START>def foo(): y=1 <RANGE_END># fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
# Range inside statement
x=1
x = <RANGE_START>[
'1',
'2',<RANGE_END>
] # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3<RANGE_START>
'2',
'3'
] ;x=2;x=3 # 4<RANGE_END> # fmt: skip # 5
# 6

View File

@@ -24,7 +24,6 @@ pub use crate::options::{
};
use crate::range::is_logical_line;
pub use crate::shared_traits::{AsFormat, FormattedIter, FormattedIterExt, IntoFormat};
use crate::verbatim::suppressed_node;
pub(crate) mod builders;
pub mod cli;
@@ -61,51 +60,39 @@ where
let node_ref = AnyNodeRef::from(node);
let node_comments = comments.leading_dangling_trailing(node_ref);
if self.is_suppressed(node_comments.trailing, f.context()) {
suppressed_node(node_ref).fmt(f)
} else {
leading_comments(node_comments.leading).fmt(f)?;
leading_comments(node_comments.leading).fmt(f)?;
// Emit source map information for nodes that are valid "narrowing" targets
// in range formatting. Never emit source map information if they're disabled
// for performance reasons.
let emit_source_position = (is_logical_line(node_ref) || node_ref.is_mod_module())
&& f.options().source_map_generation().is_enabled();
// Emit source map information for nodes that are valid "narrowing" targets
// in range formatting. Never emit source map information if they're disabled
// for performance reasons.
let emit_source_position = (is_logical_line(node_ref) || node_ref.is_mod_module())
&& f.options().source_map_generation().is_enabled();
emit_source_position
.then_some(source_position(node.start()))
.fmt(f)?;
emit_source_position
.then_some(source_position(node.start()))
.fmt(f)?;
self.fmt_fields(node, f)?;
self.fmt_fields(node, f)?;
debug_assert!(
node_comments
.dangling
.iter()
.all(SourceComment::is_formatted),
"The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`."
);
debug_assert!(
node_comments
.dangling
.iter()
.all(SourceComment::is_formatted),
"The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`."
);
write!(
f,
[
emit_source_position.then_some(source_position(node.end())),
trailing_comments(node_comments.trailing)
]
)
}
write!(
f,
[
emit_source_position.then_some(source_position(node.end())),
trailing_comments(node_comments.trailing)
]
)
}
/// Formats the node's fields.
fn fmt_fields(&self, item: &N, f: &mut PyFormatter) -> FormatResult<()>;
fn is_suppressed(
&self,
_trailing_comments: &[SourceComment],
_context: &PyFormatContext,
) -> bool {
false
}
}
#[derive(Error, Debug, salsa::Update, PartialEq, Eq)]

View File

@@ -1,9 +1,10 @@
use ruff_formatter::write;
use ruff_python_ast::Decorator;
use ruff_text_size::Ranged;
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::verbatim::verbatim_text;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
@@ -11,26 +12,27 @@ pub struct FormatDecorator;
impl FormatNodeRule<Decorator> for FormatDecorator {
fn fmt_fields(&self, item: &Decorator, f: &mut PyFormatter) -> FormatResult<()> {
let Decorator {
expression,
range: _,
node_index: _,
} = item;
let comments = f.context().comments();
let trailing = comments.trailing(item);
write!(
f,
[
token("@"),
maybe_parenthesize_expression(expression, item, Parenthesize::Optional)
]
)
}
if has_skip_comment(trailing, f.context().source()) {
comments.mark_verbatim_node_comments_formatted(item.into());
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
verbatim_text(item.range()).fmt(f)
} else {
let Decorator {
expression,
range: _,
node_index: _,
} = item;
write!(
f,
[
token("@"),
maybe_parenthesize_expression(expression, item, Parenthesize::Optional)
]
)
}
}
}

View File

@@ -15,7 +15,7 @@ use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::comments::Comments;
use crate::context::{IndentLevel, NodeLevel};
use crate::prelude::*;
use crate::statement::suite::DocstringStmt;
use crate::statement::suite::{DocstringStmt, skip_range};
use crate::verbatim::{ends_suppression, starts_suppression};
use crate::{FormatModuleError, PyFormatOptions, format_module_source};
@@ -251,7 +251,29 @@ impl<'ast> SourceOrderVisitor<'ast> for FindEnclosingNode<'_, 'ast> {
// We only visit statements that aren't suppressed that's why we don't need to track the suppression
// state in a stack. Assert that this assumption is safe.
debug_assert!(self.suppressed.is_no());
walk_body(self, body);
let mut iter = body.iter();
while let Some(stmt) = iter.next() {
// If the range intersects a skip range then we need to
// format the entire suite to properly handle the case
// where a `fmt: skip` affects multiple statements.
//
// For example, in the case
//
// ```
// <RANGE_START>x=1<RANGE_END>;x=2 # fmt: skip
// ```
//
// the statement `x=1` does not "know" that it is
// suppressed, but the suite does.
if let Some(verbatim_range) = skip_range(stmt, iter.as_slice(), self.context)
&& verbatim_range.intersect(self.range).is_some()
{
break;
}
self.visit_stmt(stmt);
}
self.suppressed = Suppressed::No;
}
}
@@ -561,7 +583,7 @@ impl NarrowRange<'_> {
}
pub(crate) const fn is_logical_line(node: AnyNodeRef) -> bool {
// Make sure to update [`FormatEnclosingLine`] when changing this.
// Make sure to update [`FormatEnclosingNode`] when changing this.
node.is_statement()
|| node.is_decorator()
|| node.is_except_handler()

View File

@@ -1,14 +1,13 @@
use ruff_formatter::write;
use ruff_python_ast::StmtAnnAssign;
use crate::comments::SourceComment;
use crate::expression::is_splittable_expression;
use crate::expression::parentheses::Parentheses;
use crate::prelude::*;
use crate::statement::stmt_assign::{
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
};
use crate::statement::trailing_semicolon;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtAnnAssign;
@@ -84,12 +83,4 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -2,10 +2,9 @@ use ruff_formatter::prelude::{space, token};
use ruff_formatter::write;
use ruff_python_ast::StmtAssert;
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::{has_skip_comment, prelude::*};
use crate::prelude::*;
#[derive(Default)]
pub struct FormatStmtAssert;
@@ -41,12 +40,4 @@ impl FormatNodeRule<StmtAssert> for FormatStmtAssert {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -19,6 +19,7 @@ use crate::expression::{
maybe_parenthesize_expression,
};
use crate::other::interpolated_string::InterpolatedStringLayout;
use crate::prelude::*;
use crate::preview::is_parenthesize_lambda_bodies_enabled;
use crate::statement::trailing_semicolon;
use crate::string::StringLikeExtensions;
@@ -26,7 +27,6 @@ use crate::string::implicit::{
FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat,
ImplicitConcatenatedLayout,
};
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtAssign;
@@ -104,14 +104,6 @@ impl FormatNodeRule<StmtAssign> for FormatStmtAssign {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}
/// Formats a single target with the equal operator.

View File

@@ -1,15 +1,14 @@
use ruff_formatter::write;
use ruff_python_ast::StmtAugAssign;
use crate::comments::SourceComment;
use crate::expression::parentheses::is_expression_parenthesized;
use crate::prelude::*;
use crate::statement::stmt_assign::{
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
has_target_own_parentheses,
};
use crate::statement::trailing_semicolon;
use crate::{AsFormat, FormatNodeRule};
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtAugAssign;
@@ -62,12 +61,4 @@ impl FormatNodeRule<StmtAugAssign> for FormatStmtAugAssign {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::StmtBreak;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
use crate::prelude::*;
#[derive(Default)]
pub struct FormatStmtBreak;
@@ -10,12 +9,4 @@ impl FormatNodeRule<StmtBreak> for FormatStmtBreak {
fn fmt_fields(&self, _item: &StmtBreak, f: &mut PyFormatter) -> FormatResult<()> {
token("break").fmt(f)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::StmtContinue;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
use crate::prelude::*;
#[derive(Default)]
pub struct FormatStmtContinue;
@@ -10,12 +9,4 @@ impl FormatNodeRule<StmtContinue> for FormatStmtContinue {
fn fmt_fields(&self, _item: &StmtContinue, f: &mut PyFormatter) -> FormatResult<()> {
token("continue").fmt(f)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -3,10 +3,10 @@ use ruff_python_ast::StmtDelete;
use ruff_text_size::Ranged;
use crate::builders::{PyFormatterExtensions, parenthesize_if_expands};
use crate::comments::{SourceComment, dangling_node_comments};
use crate::comments::dangling_node_comments;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::{has_skip_comment, prelude::*};
use crate::prelude::*;
#[derive(Default)]
pub struct FormatStmtDelete;
@@ -57,12 +57,4 @@ impl FormatNodeRule<StmtDelete> for FormatStmtDelete {
}
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,12 +1,10 @@
use ruff_python_ast as ast;
use ruff_python_ast::{Expr, Operator, StmtExpr};
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::trailing_semicolon;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtExpr;
@@ -30,14 +28,6 @@ impl FormatNodeRule<StmtExpr> for FormatStmtExpr {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}
const fn is_arithmetic_like(expression: &Expr) -> bool {

View File

@@ -1,8 +1,6 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::StmtGlobal;
use crate::comments::SourceComment;
use crate::has_skip_comment;
use crate::prelude::*;
#[derive(Default)]
@@ -47,12 +45,4 @@ impl FormatNodeRule<StmtGlobal> for FormatStmtGlobal {
)
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,8 +1,7 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::StmtImport;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
use crate::prelude::*;
#[derive(Default)]
pub struct FormatStmtImport;
@@ -21,12 +20,4 @@ impl FormatNodeRule<StmtImport> for FormatStmtImport {
});
write!(f, [token("import"), space(), names])
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -3,9 +3,7 @@ use ruff_python_ast::StmtImportFrom;
use ruff_text_size::Ranged;
use crate::builders::{PyFormatterExtensions, TrailingComma, parenthesize_if_expands};
use crate::comments::SourceComment;
use crate::expression::parentheses::parenthesized;
use crate::has_skip_comment;
use crate::other::identifier::DotDelimitedIdentifier;
use crate::prelude::*;
@@ -72,12 +70,4 @@ impl FormatNodeRule<StmtImportFrom> for FormatStmtImportFrom {
.fmt(f)
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,8 +1,7 @@
use ruff_python_ast::StmtIpyEscapeCommand;
use ruff_text_size::Ranged;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
use crate::prelude::*;
#[derive(Default)]
pub struct FormatStmtIpyEscapeCommand;
@@ -11,12 +10,4 @@ impl FormatNodeRule<StmtIpyEscapeCommand> for FormatStmtIpyEscapeCommand {
fn fmt_fields(&self, item: &StmtIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> {
source_text_slice(item.range()).fmt(f)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,8 +1,6 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::StmtNonlocal;
use crate::comments::SourceComment;
use crate::has_skip_comment;
use crate::prelude::*;
#[derive(Default)]
@@ -47,12 +45,4 @@ impl FormatNodeRule<StmtNonlocal> for FormatStmtNonlocal {
)
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::StmtPass;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
use crate::prelude::*;
#[derive(Default)]
pub struct FormatStmtPass;
@@ -10,12 +9,4 @@ impl FormatNodeRule<StmtPass> for FormatStmtPass {
fn fmt_fields(&self, _item: &StmtPass, f: &mut PyFormatter) -> FormatResult<()> {
token("pass").fmt(f)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,10 +1,9 @@
use ruff_formatter::write;
use ruff_python_ast::StmtRaise;
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::{has_skip_comment, prelude::*};
use crate::prelude::*;
#[derive(Default)]
pub struct FormatStmtRaise;
@@ -43,12 +42,4 @@ impl FormatNodeRule<StmtRaise> for FormatStmtRaise {
}
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,10 +1,9 @@
use ruff_formatter::write;
use ruff_python_ast::{Expr, StmtReturn};
use crate::comments::SourceComment;
use crate::expression::expr_tuple::TupleParentheses;
use crate::prelude::*;
use crate::statement::stmt_assign::FormatStatementsLastExpression;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtReturn;
@@ -43,12 +42,4 @@ impl FormatNodeRule<StmtReturn> for FormatStmtReturn {
None => Ok(()),
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,11 +1,10 @@
use ruff_formatter::write;
use ruff_python_ast::StmtTypeAlias;
use crate::comments::SourceComment;
use crate::prelude::*;
use crate::statement::stmt_assign::{
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
};
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtTypeAlias;
@@ -42,12 +41,4 @@ impl FormatNodeRule<StmtTypeAlias> for FormatStmtTypeAlias {
]
)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -4,11 +4,15 @@ use ruff_formatter::{
use ruff_python_ast::helpers::is_compound_statement;
use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt, Suite};
use ruff_python_ast::{AnyNodeRef, StmtExpr};
use ruff_python_trivia::{lines_after, lines_after_ignoring_end_of_line_trivia, lines_before};
use ruff_python_trivia::{
SimpleTokenKind, SimpleTokenizer, lines_after, lines_after_ignoring_end_of_line_trivia,
lines_before,
};
use ruff_text_size::{Ranged, TextRange};
use crate::comments::{
Comments, LeadingDanglingTrailingComments, leading_comments, trailing_comments,
Comments, LeadingDanglingTrailingComments, has_skip_comment, leading_comments,
trailing_comments,
};
use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel};
use crate::other::string_literal::StringLiteralKind;
@@ -16,9 +20,9 @@ use crate::prelude::*;
use crate::preview::{
is_allow_newline_after_block_open_enabled, is_blank_line_before_decorated_class_in_stub_enabled,
};
use crate::statement::stmt_expr::FormatStmtExpr;
use crate::statement::trailing_semicolon;
use crate::verbatim::{
suppressed_node, write_suppressed_statements_starting_with_leading_comment,
write_skipped_statements, write_suppressed_statements_starting_with_leading_comment,
write_suppressed_statements_starting_with_trailing_comment,
};
@@ -152,7 +156,21 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
let first_comments = comments.leading_dangling_trailing(first);
let (mut preceding, mut empty_line_after_docstring) = if first_comments
let (mut preceding, mut empty_line_after_docstring) = if let Some(verbatim_range) =
skip_range(first.statement(), iter.as_slice(), f.context())
{
let preceding =
write_skipped_statements(first.statement(), &mut iter, verbatim_range, f)?;
// Insert a newline after a module level docstring, but treat
// it as a docstring otherwise. See: https://github.com/psf/black/pull/3932.
let empty_line_after_docstring =
matches!(self.kind, SuiteKind::TopLevel | SuiteKind::Class)
&& DocstringStmt::try_from_statement(preceding, self.kind, f.context())
.is_some();
(preceding, empty_line_after_docstring)
} else if first_comments
.leading
.iter()
.any(|comment| comment.is_suppression_off_comment(source))
@@ -391,7 +409,10 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
}
}
if following_comments
if let Some(verbatim_range) = skip_range(following, iter.as_slice(), f.context()) {
preceding = write_skipped_statements(following, &mut iter, verbatim_range, f)?;
preceding_comments = comments.leading_dangling_trailing(preceding);
} else if following_comments
.leading
.iter()
.any(|comment| comment.is_suppression_off_comment(source))
@@ -840,61 +861,57 @@ impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
let comments = f.context().comments().clone();
let node_comments = comments.leading_dangling_trailing(self.docstring);
if FormatStmtExpr.is_suppressed(node_comments.trailing, f.context()) {
suppressed_node(self.docstring).fmt(f)
} else {
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ExprStringLiteral`.
let string_literal = self
.docstring
.as_expr_stmt()
.unwrap()
.value
.as_string_literal_expr()
.unwrap();
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ExprStringLiteral`.
let string_literal = self
.docstring
.as_expr_stmt()
.unwrap()
.value
.as_string_literal_expr()
.unwrap();
// We format the expression, but the statement carries the comments
write!(
f,
[
leading_comments(node_comments.leading),
f.options()
.source_map_generation()
.is_enabled()
.then_some(source_position(self.docstring.start())),
string_literal
.format()
.with_options(StringLiteralKind::Docstring),
f.options()
.source_map_generation()
.is_enabled()
.then_some(source_position(self.docstring.end())),
]
)?;
// We format the expression, but the statement carries the comments
write!(
f,
[
leading_comments(node_comments.leading),
f.options()
.source_map_generation()
.is_enabled()
.then_some(source_position(self.docstring.start())),
string_literal
.format()
.with_options(StringLiteralKind::Docstring),
f.options()
.source_map_generation()
.is_enabled()
.then_some(source_position(self.docstring.end())),
]
)?;
if self.suite_kind == SuiteKind::Class {
// Comments after class docstrings need a newline between the docstring and the
// comment (https://github.com/astral-sh/ruff/issues/7948).
// ```python
// class ModuleBrowser:
// """Browse module classes and functions in IDLE."""
// # ^ Insert a newline above here
//
// def __init__(self, master, path, *, _htest=False, _utest=False):
// pass
// ```
if let Some(own_line) = node_comments
.trailing
.iter()
.find(|comment| comment.line_position().is_own_line())
{
if lines_before(own_line.start(), f.context().source()) < 2 {
empty_line().fmt(f)?;
}
if self.suite_kind == SuiteKind::Class {
// Comments after class docstrings need a newline between the docstring and the
// comment (https://github.com/astral-sh/ruff/issues/7948).
// ```python
// class ModuleBrowser:
// """Browse module classes and functions in IDLE."""
// # ^ Insert a newline above here
//
// def __init__(self, master, path, *, _htest=False, _utest=False):
// pass
// ```
if let Some(own_line) = node_comments
.trailing
.iter()
.find(|comment| comment.line_position().is_own_line())
{
if lines_before(own_line.start(), f.context().source()) < 2 {
empty_line().fmt(f)?;
}
}
trailing_comments(node_comments.trailing).fmt(f)
}
trailing_comments(node_comments.trailing).fmt(f)
}
}
@@ -938,6 +955,58 @@ impl Format<PyFormatContext<'_>> for SuiteChildStatement<'_> {
}
}
pub(crate) fn skip_range(
first: &Stmt,
statements: &[Stmt],
context: &PyFormatContext,
) -> Option<TextRange> {
let start = first.start();
let mut last_statement = first;
let comments = context.comments();
let source = context.source();
for statement in statements {
if new_logical_line_between_statements(
source,
TextRange::new(last_statement.end(), statement.start()),
) {
break;
}
last_statement = statement;
}
if has_skip_comment(comments.trailing(last_statement), source) {
Some(TextRange::new(
start,
trailing_semicolon(last_statement.into(), source)
.map_or_else(|| last_statement.end(), ruff_text_size::TextRange::end),
))
} else {
None
}
}
fn new_logical_line_between_statements(source: &str, between_statement_range: TextRange) -> bool {
let mut tokenizer = SimpleTokenizer::new(source, between_statement_range).map(|tok| tok.kind());
while let Some(token) = tokenizer.next() {
match token {
SimpleTokenKind::Continuation => {
tokenizer.next();
}
SimpleTokenKind::Newline => {
return true;
}
// Since we are between statements, there are
// no non-trivia tokens, so there is no need to check
// for these and do an early return.
_ => {}
}
}
false
}
#[cfg(test)]
mod tests {
use ruff_formatter::format;

View File

@@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::iter::FusedIterator;
use std::slice::Iter;
use itertools::PeekingNext;
use ruff_formatter::{FormatError, write};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::Stmt;
@@ -451,6 +452,40 @@ fn write_suppressed_statements<'a>(
}
}
#[cold]
pub(crate) fn write_skipped_statements<'a>(
first_skipped: &'a Stmt,
statements: &mut std::slice::Iter<'a, Stmt>,
verbatim_range: TextRange,
f: &mut PyFormatter,
) -> FormatResult<&'a Stmt> {
let comments = f.context().comments().clone();
comments.mark_verbatim_node_comments_formatted(first_skipped.into());
let mut preceding = first_skipped;
while let Some(prec) = statements.peeking_next(|next| next.end() <= verbatim_range.end()) {
comments.mark_verbatim_node_comments_formatted(prec.into());
preceding = prec;
}
let first_leading = comments.leading(first_skipped);
let preceding_trailing = comments.trailing(preceding);
// Write the outer comments and format the node as verbatim
write!(
f,
[
leading_comments(first_leading),
source_position(verbatim_range.start()),
verbatim_text(verbatim_range),
source_position(verbatim_range.end()),
trailing_comments(preceding_trailing)
]
)?;
Ok(preceding)
}
#[derive(Copy, Clone, Debug)]
enum InSuppression {
No,
@@ -893,65 +928,6 @@ impl Format<PyFormatContext<'_>> for VerbatimText {
}
}
/// Disables formatting for `node` and instead uses the same formatting as the node has in source.
///
/// The `node` gets indented as any formatted node to avoid syntax errors when the indentation string changes (e.g. from 2 spaces to 4).
/// The `node`s leading and trailing comments are formatted as usual, except if they fall into the suppressed node's range.
#[cold]
pub(crate) fn suppressed_node<'a, N>(node: N) -> FormatSuppressedNode<'a>
where
N: Into<AnyNodeRef<'a>>,
{
FormatSuppressedNode { node: node.into() }
}
pub(crate) struct FormatSuppressedNode<'a> {
node: AnyNodeRef<'a>,
}
impl Format<PyFormatContext<'_>> for FormatSuppressedNode<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
let comments = f.context().comments().clone();
let node_comments = comments.leading_dangling_trailing(self.node);
// Mark all comments as formatted that fall into the node range
for comment in node_comments.leading {
if comment.start() > self.node.start() {
comment.mark_formatted();
}
}
for comment in node_comments.trailing {
if comment.start() < self.node.end() {
comment.mark_formatted();
}
}
// Some statements may end with a semicolon. Preserve the semicolon
let semicolon_range = self
.node
.is_statement()
.then(|| trailing_semicolon(self.node, f.context().source()))
.flatten();
let verbatim_range = semicolon_range.map_or(self.node.range(), |semicolon| {
TextRange::new(self.node.start(), semicolon.end())
});
comments.mark_verbatim_node_comments_formatted(self.node);
// Write the outer comments and format the node as verbatim
write!(
f,
[
leading_comments(node_comments.leading),
source_position(verbatim_range.start()),
verbatim_text(verbatim_range),
source_position(verbatim_range.end()),
trailing_comments(node_comments.trailing)
]
)
}
}
#[cold]
pub(crate) fn write_suppressed_clause_header(
header: ClauseHeader,

View File

@@ -0,0 +1,299 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
class Simple:
x=1
x=2 # fmt: skip
x=3
class Semicolon:
x=1
x=2;x=3 # fmt: skip
x=4
class TrailingSemicolon:
x=1
x=2;x=3 ; # fmt: skip
x=4
class SemicolonNewLogicalLine:
x=1;
x=2;x=3 # fmt: skip
x=4
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
def foo(): y=1 # fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
x=1
x = [
'1',
'2',
] # fmt: skip
class MultiLineSemicolon:
x=1
x = [
'1',
'2',
]; x=2 # fmt: skip
class LineContinuationSemicolonAfter:
x=1
x = ['a']\
; y=1 # fmt: skip
class LineContinuationSemicolonBefore:
x=1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewline:
x=1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewlineAndComment:
x=1
x = ['a']; \
# 1
y=1 # fmt: skip
class RepeatedLineContinuation:
x=1
x = ['a']; \
\
\
y=1 # fmt: skip
class MultiLineSemicolonComments:
x=1
# 1
x = [ # 2
'1', # 3
'2',
# 4
]; x=2 # 5 # fmt: skip # 6
class DocstringSkipped:
'''This is a docstring''' # fmt: skip
x=1
class MultilineDocstringSkipped:
'''This is a docstring
''' # fmt: skip
x=1
class FirstStatementNewlines:
x=1 # fmt: skip
class ChainingSemicolons:
x=[
'1',
'2',
'3',
];x=1;x=[
'1',
'2',
'3'
];x=1;x=1 # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3
'2',
'3'
] ;x=2;x=3 # 4 # fmt: skip # 5
# 6
class MixingCompound:
def foo(): bar(); import zoo # fmt: skip
# https://github.com/astral-sh/ruff/issues/17331
def main() -> None:
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
# https://github.com/astral-sh/ruff/issues/11430
print(); print() # noqa # fmt: skip
```
## Output
```python
class Simple:
x = 1
x=2 # fmt: skip
x = 3
class Semicolon:
x = 1
x=2;x=3 # fmt: skip
x = 4
class TrailingSemicolon:
x = 1
x=2;x=3 ; # fmt: skip
x = 4
class SemicolonNewLogicalLine:
x = 1
x=2;x=3 # fmt: skip
x = 4
class ManySemicolonOneLine:
x = 1
x=2;x=3;x=4 # fmt: skip
x = 5
class CompoundInSuite:
x = 1
def foo(): y=1 # fmt: skip
x = 2
class CompoundInSuiteNewline:
x = 1
def foo():
y=1 # fmt: skip
x = 2
class MultiLineSkip:
x = 1
x = [
'1',
'2',
] # fmt: skip
class MultiLineSemicolon:
x = 1
x = [
'1',
'2',
]; x=2 # fmt: skip
class LineContinuationSemicolonAfter:
x = 1
x = ['a']\
; y=1 # fmt: skip
class LineContinuationSemicolonBefore:
x = 1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewline:
x = 1
x = ["a"]
y=1 # fmt: skip
class LineContinuationSemicolonAndNewlineAndComment:
x = 1
x = ["a"]
# 1
y=1 # fmt: skip
class RepeatedLineContinuation:
x = 1
x = ['a']; \
\
\
y=1 # fmt: skip
class MultiLineSemicolonComments:
x = 1
# 1
x = [ # 2
'1', # 3
'2',
# 4
]; x=2 # 5 # fmt: skip # 6
class DocstringSkipped:
'''This is a docstring''' # fmt: skip
x = 1
class MultilineDocstringSkipped:
'''This is a docstring
''' # fmt: skip
x = 1
class FirstStatementNewlines:
x=1 # fmt: skip
class ChainingSemicolons:
x=[
'1',
'2',
'3',
];x=1;x=[
'1',
'2',
'3'
];x=1;x=1 # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3
'2',
'3'
] ;x=2;x=3 # 4 # fmt: skip # 5
# 6
class MixingCompound:
def foo(): bar(); import zoo # fmt: skip
# https://github.com/astral-sh/ruff/issues/17331
def main() -> None:
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
# https://github.com/astral-sh/ruff/issues/11430
print(); print() # noqa # fmt: skip
```

View File

@@ -0,0 +1,56 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
# 0
'''Module docstring
multiple lines''' # 1 # fmt: skip # 2
import a;import b; from c import (
x,y,z
); import f # fmt: skip
x=1;x=2;x=3;x=4 # fmt: skip
# 1
x=[ # 2
'1', # 3
'2',
'3'
];x=1;x=1 # 4 # fmt: skip # 5
# 6
def foo(): x=[
'1',
'2',
];x=1 # fmt: skip
```
## Output
```python
# 0
'''Module docstring
multiple lines''' # 1 # fmt: skip # 2
import a;import b; from c import (
x,y,z
); import f # fmt: skip
x=1;x=2;x=3;x=4 # fmt: skip
# 1
x=[ # 2
'1', # 3
'2',
'3'
];x=1;x=1 # 4 # fmt: skip # 5
# 6
def foo():
x=[
'1',
'2',
];x=1 # fmt: skip
```

View File

@@ -0,0 +1,147 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
class Simple:
# Range comprises skip range
x=1
<RANGE_START>x=2 <RANGE_END># fmt: skip
x=3
class Semicolon:
# Range is part of skip range
x=1
x=2;<RANGE_START>x=3<RANGE_END> # fmt: skip
x=4
class FormatFirst:
x=1
<RANGE_START>x=2<RANGE_END>;x=3 # fmt: skip
x=4
class FormatMiddle:
x=1
x=2;<RANGE_START>x=3<RANGE_END>;x=4 # fmt: skip
x=5
class SemicolonNewLogicalLine:
# Range overlaps on right side
<RANGE_START>x=1;
x=2<RANGE_END>;x=3 # fmt: skip
x=4
class SemicolonNewLogicalLine:
# Range overlaps on left side
x=1;
x=2;<RANGE_START>x=3 # fmt: skip
x=4<RANGE_END>
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
<RANGE_START>def foo(): y=1 <RANGE_END># fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
# Range inside statement
x=1
x = <RANGE_START>[
'1',
'2',<RANGE_END>
] # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3<RANGE_START>
'2',
'3'
] ;x=2;x=3 # 4<RANGE_END> # fmt: skip # 5
# 6
```
## Output
```python
class Simple:
# Range comprises skip range
x=1
x=2 # fmt: skip
x=3
class Semicolon:
# Range is part of skip range
x=1
x=2;x=3 # fmt: skip
x=4
class FormatFirst:
x=1
x=2;x=3 # fmt: skip
x=4
class FormatMiddle:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class SemicolonNewLogicalLine:
# Range overlaps on right side
x = 1
x=2;x=3 # fmt: skip
x=4
class SemicolonNewLogicalLine:
# Range overlaps on left side
x=1;
x=2;x=3 # fmt: skip
x = 4
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
def foo(): y=1 # fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
# Range inside statement
x=1
x = [
'1',
'2',
] # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3
'2',
'3'
] ;x=2;x=3 # 4 # fmt: skip # 5
# 6
```

View File

@@ -34,12 +34,12 @@ cargo install cargo-insta
You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/) (or `pipx` and `pip`) to
run Python utility commands.
You can optionally install pre-commit hooks to automatically run the validation checks
You can optionally install hooks to automatically run the validation checks
when making a commit:
```shell
uv tool install pre-commit
pre-commit install
uv tool install prek
prek install
```
We recommend [nextest](https://nexte.st/) to run ty's test suite (via `cargo nextest run`),
@@ -66,7 +66,7 @@ and that it passes both the lint and test validation checks:
```shell
cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting
cargo test # Rust testing
uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
uvx prek run -a # Rust and Python formatting, Markdown and Python linting, etc.
```
These checks will run on GitHub Actions when you open your pull request, but running them locally

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

@@ -8,7 +8,7 @@
Default level: <a href="../../rules#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#L540" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L543" target="_blank">View source</a>
</small>
@@ -80,7 +80,7 @@ def test(): -> "int":
Default level: <a href="../../rules#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#L139" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L142" target="_blank">View source</a>
</small>
@@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: <a href="../../rules#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.7">0.0.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-top-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L160" target="_blank">View source</a>
</small>
@@ -135,7 +135,7 @@ def f(x: object):
Default level: <a href="../../rules#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#L208" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L211" target="_blank">View source</a>
</small>
@@ -167,7 +167,7 @@ f(int) # error
Default level: <a href="../../rules#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#L234" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L237" target="_blank">View source</a>
</small>
@@ -198,7 +198,7 @@ a = 1
Default level: <a href="../../rules#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#L259" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L262" target="_blank">View source</a>
</small>
@@ -230,7 +230,7 @@ class C(A, B): ...
Default level: <a href="../../rules#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#L285" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L288" target="_blank">View source</a>
</small>
@@ -262,7 +262,7 @@ class B(A): ...
Default level: <a href="../../rules#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#L311" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L314" target="_blank">View source</a>
</small>
@@ -290,7 +290,7 @@ type B = A
Default level: <a href="../../rules#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#L355" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L358" target="_blank">View source</a>
</small>
@@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic
Default level: <a href="../../rules#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#L333" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L336" target="_blank">View source</a>
</small>
@@ -346,7 +346,7 @@ false positives it can produce.
Default level: <a href="../../rules#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#L376" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L379" target="_blank">View source</a>
</small>
@@ -373,7 +373,7 @@ class B(A, A): ...
Default level: <a href="../../rules#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#L397" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L400" target="_blank">View source</a>
</small>
@@ -529,7 +529,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../../rules#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#L623" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626" target="_blank">View source</a>
</small>
@@ -559,7 +559,7 @@ class C(A, B): ...
Default level: <a href="../../rules#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#L647" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650" target="_blank">View source</a>
</small>
@@ -579,13 +579,47 @@ t = (0, 1, 2)
t[3] # IndexError: tuple index out of range
```
## `ineffective-final`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.33">0.0.1-alpha.33</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ineffective-final" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1793" target="_blank">View source</a>
</small>
**What it does**
Checks for calls to `final()` that type checkers cannot interpret.
**Why is this bad?**
The `final()` function is designed to be used as a decorator. When called directly
as a function (e.g., `final(type(...))`), type checkers will not understand the
application of `final` and will not prevent subclassing.
**Example**
```python
from typing import final
# Incorrect: type checkers will not prevent subclassing
MyClass = final(type("MyClass", (), {}))
# Correct: use `final` as a decorator
@final
class MyClass: ...
```
## `instance-layout-conflict`
<small>
Default level: <a href="../../rules#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#L429" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L432" target="_blank">View source</a>
</small>
@@ -674,7 +708,7 @@ an atypical memory layout.
Default level: <a href="../../rules#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#L701" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L704" target="_blank">View source</a>
</small>
@@ -701,7 +735,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../../rules#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#L741" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L744" target="_blank">View source</a>
</small>
@@ -729,7 +763,7 @@ a: int = ''
Default level: <a href="../../rules#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#L2044" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2151" target="_blank">View source</a>
</small>
@@ -763,7 +797,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../../rules#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#L763" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766" target="_blank">View source</a>
</small>
@@ -799,7 +833,7 @@ asyncio.run(main())
Default level: <a href="../../rules#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#L793" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L796" target="_blank">View source</a>
</small>
@@ -823,7 +857,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../../rules#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#L844" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L881" target="_blank">View source</a>
</small>
@@ -850,7 +884,7 @@ with 1:
Default level: <a href="../../rules#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#L865" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L902" target="_blank">View source</a>
</small>
@@ -879,7 +913,7 @@ a: str
Default level: <a href="../../rules#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#L888" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L925" target="_blank">View source</a>
</small>
@@ -923,7 +957,7 @@ except ZeroDivisionError:
Default level: <a href="../../rules#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#L1714" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1821" target="_blank">View source</a>
</small>
@@ -965,7 +999,7 @@ class D(A):
Default level: <a href="../../rules#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.35">0.0.1-alpha.35</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-frozen-dataclass-subclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2295" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2402" target="_blank">View source</a>
</small>
@@ -1009,7 +1043,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: <a href="../../rules#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#L924" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1003" target="_blank">View source</a>
</small>
@@ -1041,6 +1075,55 @@ class D(Generic[U, T]): ...
- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)
## `invalid-generic-enum`
<small>
Default level: <a href="../../rules#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.12">0.0.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-enum" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L961" target="_blank">View source</a>
</small>
**What it does**
Checks for enum classes that are also generic.
**Why is this bad?**
Enum classes cannot be generic. Python does not support generic enums:
attempting to create one will either result in an immediate `TypeError`
at runtime, or will create a class that cannot be specialized in the way
that a normal generic class can.
**Examples**
```python
from enum import Enum
from typing import Generic, TypeVar
T = TypeVar("T")
# error: enum class cannot be generic (class creation fails with `TypeError`)
class E[T](Enum):
A = 1
# error: enum class cannot be generic (class creation fails with `TypeError`)
class F(Enum, Generic[T]):
A = 1
# error: enum class cannot be generic -- the class creation does not immediately fail...
class G(Generic[T], Enum):
A = 1
# ...but this raises `KeyError`:
x: G[int]
```
**References**
- [Python documentation: Enum](https://docs.python.org/3/library/enum.html)
## `invalid-ignore-comment`
<small>
@@ -1077,7 +1160,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../../rules#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#L668" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L671" target="_blank">View source</a>
</small>
@@ -1116,7 +1199,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: <a href="../../rules#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#L955" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1034" target="_blank">View source</a>
</small>
@@ -1151,7 +1234,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../../rules#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#L1052" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1131" target="_blank">View source</a>
</small>
@@ -1185,7 +1268,7 @@ class B(metaclass=f): ...
Default level: <a href="../../rules#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#L2197" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2304" target="_blank">View source</a>
</small>
@@ -1292,7 +1375,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: <a href="../../rules#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#L575" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L578" target="_blank">View source</a>
</small>
@@ -1346,7 +1429,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: <a href="../../rules#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#L1028" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107" target="_blank">View source</a>
</small>
@@ -1376,7 +1459,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: <a href="../../rules#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#L1079" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1158" target="_blank">View source</a>
</small>
@@ -1426,7 +1509,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../../rules#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#L1178" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1257" target="_blank">View source</a>
</small>
@@ -1452,7 +1535,7 @@ def f(a: int = ''): ...
Default level: <a href="../../rules#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#L983" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062" target="_blank">View source</a>
</small>
@@ -1483,7 +1566,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: <a href="../../rules#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#L511" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L514" target="_blank">View source</a>
</small>
@@ -1517,7 +1600,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../../rules#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#L1198" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1277" target="_blank">View source</a>
</small>
@@ -1566,7 +1649,7 @@ def g():
Default level: <a href="../../rules#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#L722" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L725" target="_blank">View source</a>
</small>
@@ -1591,7 +1674,7 @@ def func() -> int:
Default level: <a href="../../rules#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#L1241" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1320" target="_blank">View source</a>
</small>
@@ -1687,7 +1770,7 @@ class C: ...
Default level: <a href="../../rules#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.10">0.0.10</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-total-ordering" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2333" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2440" target="_blank">View source</a>
</small>
@@ -1733,7 +1816,7 @@ class MyClass:
Default level: <a href="../../rules#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#L1007" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086" target="_blank">View source</a>
</small>
@@ -1760,7 +1843,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../../rules#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#L1473" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1552" target="_blank">View source</a>
</small>
@@ -1807,7 +1890,7 @@ Bar[int] # error: too few arguments
Default level: <a href="../../rules#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#L1280" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1359" target="_blank">View source</a>
</small>
@@ -1837,7 +1920,7 @@ TYPE_CHECKING = ''
Default level: <a href="../../rules#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#L1304" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1383" target="_blank">View source</a>
</small>
@@ -1867,7 +1950,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../../rules#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#L1356" 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>
@@ -1901,7 +1984,7 @@ f(10) # Error
Default level: <a href="../../rules#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#L1328" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1407" target="_blank">View source</a>
</small>
@@ -1935,7 +2018,7 @@ class C:
Default level: <a href="../../rules#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#L1384" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463" target="_blank">View source</a>
</small>
@@ -1970,7 +2053,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../../rules#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.9">0.0.9</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-typed-dict-statement" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2172" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2279" target="_blank">View source</a>
</small>
@@ -2001,7 +2084,7 @@ class Foo(TypedDict):
Default level: <a href="../../rules#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#L1413" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1492" target="_blank">View source</a>
</small>
@@ -2026,7 +2109,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../../rules#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#L2145" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2252" target="_blank">View source</a>
</small>
@@ -2059,7 +2142,7 @@ alice["age"] # KeyError
Default level: <a href="../../rules#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#L1432" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1511" target="_blank">View source</a>
</small>
@@ -2088,7 +2171,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../../rules#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#L1514" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1593" target="_blank">View source</a>
</small>
@@ -2114,7 +2197,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../../rules#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-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1455" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1534" target="_blank">View source</a>
</small>
@@ -2138,7 +2221,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../../rules#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#L1687" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1766" target="_blank">View source</a>
</small>
@@ -2171,7 +2254,7 @@ class B(A):
Default level: <a href="../../rules#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#L1565" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1644" target="_blank">View source</a>
</small>
@@ -2198,7 +2281,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../../rules#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#L1898" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2005" target="_blank">View source</a>
</small>
@@ -2225,7 +2308,7 @@ f(x=1) # Error raised here
Default level: <a href="../../rules#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#L1586" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1665" target="_blank">View source</a>
</small>
@@ -2253,7 +2336,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../../rules#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#L182" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L185" target="_blank">View source</a>
</small>
@@ -2285,7 +2368,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../../rules#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.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#L1608" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1687" target="_blank">View source</a>
</small>
@@ -2322,7 +2405,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../../rules#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#L1638" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1717" target="_blank">View source</a>
</small>
@@ -2386,7 +2469,7 @@ def test(): -> "int":
Default level: <a href="../../rules#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#L2072" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2179" target="_blank">View source</a>
</small>
@@ -2413,7 +2496,7 @@ cast(int, f()) # Redundant
Default level: <a href="../../rules#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#L2020" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2127" target="_blank">View source</a>
</small>
@@ -2443,7 +2526,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../../rules#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#L1664" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1743" target="_blank">View source</a>
</small>
@@ -2472,7 +2555,7 @@ class B(A): ... # Error raised here
Default level: <a href="../../rules#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#L1832" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1939" target="_blank">View source</a>
</small>
@@ -2506,7 +2589,7 @@ class F(NamedTuple):
Default level: <a href="../../rules#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#L1772" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1879" target="_blank">View source</a>
</small>
@@ -2533,7 +2616,7 @@ f("foo") # Error raised here
Default level: <a href="../../rules#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#L1750" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1857" target="_blank">View source</a>
</small>
@@ -2561,7 +2644,7 @@ def _(x: int):
Default level: <a href="../../rules#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#L1793" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1900" target="_blank">View source</a>
</small>
@@ -2607,7 +2690,7 @@ class A:
Default level: <a href="../../rules#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#L1859" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1966" target="_blank">View source</a>
</small>
@@ -2631,7 +2714,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../../rules#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#L1877" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1984" target="_blank">View source</a>
</small>
@@ -2658,7 +2741,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../../rules#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#L1919" 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>
@@ -2686,7 +2769,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../../rules#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#L2093" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2200" target="_blank">View source</a>
</small>
@@ -2744,7 +2827,7 @@ def g():
Default level: <a href="../../rules#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#L1941" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2048" target="_blank">View source</a>
</small>
@@ -2769,7 +2852,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../../rules#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#L1960" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2067" target="_blank">View source</a>
</small>
@@ -2794,7 +2877,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../../rules#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#L811" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L814" target="_blank">View source</a>
</small>
@@ -2833,7 +2916,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../../rules#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#L1534" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1613" target="_blank">View source</a>
</small>
@@ -2864,13 +2947,54 @@ not b1 # exception raised here
b1 < b2 < b1 # exception raised here
```
## `unsupported-dynamic-base`
<small>
Default level: <a href="../../rules#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/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-dynamic-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L847" target="_blank">View source</a>
</small>
**What it does**
Checks for dynamic class definitions (using `type()`) that have bases
which are unsupported by ty.
This is equivalent to [`unsupported-base`] but applies to classes created
via `type()` rather than `class` statements.
**Why is this bad?**
If a dynamically created class has a base that is an unsupported type
such as `type[T]`, ty will not be able to resolve the
[method resolution order] (MRO) for the class. This may lead to an inferior
understanding of your codebase and unpredictable type-checking behavior.
**Default level**
This rule is disabled by default because it will not cause a runtime error,
and may be noisy on codebases that use `type()` in highly dynamic ways.
**Examples**
```python
def factory(base: type[Base]) -> type:
# `base` has type `type[Base]`, not `type[Base]` itself
return type("Dynamic", (base,), {}) # error: [unsupported-dynamic-base]
```
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
[`unsupported-base`]: https://docs.astral.sh/ty/rules/unsupported-base
## `unsupported-operator`
<small>
Default level: <a href="../../rules#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#L1979" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2086" target="_blank">View source</a>
</small>
@@ -2934,7 +3058,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments.
Default level: <a href="../../rules#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#L1122" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1201" target="_blank">View source</a>
</small>
@@ -2997,7 +3121,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../../rules#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#L2001" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2108" target="_blank">View source</a>
</small>

View File

@@ -2585,7 +2585,7 @@ type<CURSOR>
assert_snapshot!(
test.type_signatures().skip_auto_import().build().snapshot(),
@r"
@"
type :: <class 'type'>
TypeError :: <class 'TypeError'>
",
@@ -3586,7 +3586,7 @@ quux.<CURSOR>
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: def __new__(cls) -> Self@__new__
__new__ :: def __new__[Self](cls) -> Self
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method Quux.__repr__() -> str
@@ -3667,19 +3667,19 @@ C.<CURSOR>
__mro__ :: tuple[type, ...]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls) -> Self@__new__
__or__ :: bound method <class 'C'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
__new__ :: def __new__[Self](cls) -> Self
__or__ :: bound method <class 'C'>.__or__[Self](value: Any, /) -> UnionType | Self
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: def __repr__(self) -> str
__ror__ :: bound method <class 'C'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
__ror__ :: bound method <class 'C'>.__ror__[Self](value: Any, /) -> UnionType | Self
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: bound method <class 'C'>.__subclasscheck__(subclass: type, /) -> bool
__subclasses__ :: bound method <class 'C'>.__subclasses__[Self]() -> list[Self@__subclasses__]
__subclasses__ :: bound method <class 'C'>.__subclasses__[Self]() -> list[Self]
__subclasshook__ :: bound method <class 'C'>.__subclasshook__(subclass: type, /) -> bool
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
@@ -3737,18 +3737,18 @@ Meta.<CURSOR>
__mro__ :: tuple[type, ...]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__or__ :: def __or__[Self](self: Self@__or__, value: Any, /) -> UnionType | Self@__or__
__or__ :: def __or__[Self](self: Self, value: Any, /) -> UnionType | Self
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: def __repr__(self) -> str
__ror__ :: def __ror__[Self](self: Self@__ror__, value: Any, /) -> UnionType | Self@__ror__
__ror__ :: def __ror__[Self](self: Self, value: Any, /) -> UnionType | Self
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool
__subclasses__ :: def __subclasses__[Self](self: Self@__subclasses__) -> list[Self@__subclasses__]
__subclasses__ :: def __subclasses__[Self](self: Self) -> list[Self]
__subclasshook__ :: bound method <class 'Meta'>.__subclasshook__(subclass: type, /) -> bool
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
@@ -3866,19 +3866,19 @@ Quux.<CURSOR>
__mro__ :: tuple[type, ...]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls) -> Self@__new__
__or__ :: bound method <class 'Quux'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
__new__ :: def __new__[Self](cls) -> Self
__or__ :: bound method <class 'Quux'>.__or__[Self](value: Any, /) -> UnionType | Self
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: def __repr__(self) -> str
__ror__ :: bound method <class 'Quux'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
__ror__ :: bound method <class 'Quux'>.__ror__[Self](value: Any, /) -> UnionType | Self
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: bound method <class 'Quux'>.__subclasscheck__(subclass: type, /) -> bool
__subclasses__ :: bound method <class 'Quux'>.__subclasses__[Self]() -> list[Self@__subclasses__]
__subclasses__ :: bound method <class 'Quux'>.__subclasses__[Self]() -> list[Self]
__subclasshook__ :: bound method <class 'Quux'>.__subclasshook__(subclass: type, /) -> bool
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
@@ -3919,8 +3919,8 @@ Answer.<CURSOR>
__bool__ :: bound method <class 'Answer'>.__bool__() -> Literal[True]
__class__ :: <class 'EnumMeta'>
__contains__ :: bound method <class 'Answer'>.__contains__(value: object) -> bool
__copy__ :: def __copy__(self) -> Self@__copy__
__deepcopy__ :: def __deepcopy__(self, memo: Any) -> Self@__deepcopy__
__copy__ :: def __copy__[Self](self) -> Self
__deepcopy__ :: def __deepcopy__[Self](self, memo: Any) -> Self
__delattr__ :: def __delattr__(self, name: str, /) -> None
__dict__ :: dict[str, Any]
__dictoffset__ :: int
@@ -3930,34 +3930,34 @@ Answer.<CURSOR>
__flags__ :: int
__format__ :: def __format__(self, format_spec: str) -> str
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
__getitem__ :: bound method <class 'Answer'>.__getitem__[_EnumMemberT](name: str) -> _EnumMemberT@__getitem__
__getitem__ :: bound method <class 'Answer'>.__getitem__[_EnumMemberT](name: str) -> _EnumMemberT
__getstate__ :: def __getstate__(self) -> object
__hash__ :: def __hash__(self) -> int
__init__ :: def __init__(self) -> None
__init_subclass__ :: bound method <class 'Answer'>.__init_subclass__() -> None
__instancecheck__ :: bound method <class 'Answer'>.__instancecheck__(instance: Any, /) -> bool
__itemsize__ :: int
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT@__iter__]
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT]
__len__ :: bound method <class 'Answer'>.__len__() -> int
__members__ :: MappingProxyType[str, Answer]
__module__ :: str
__mro__ :: tuple[type, ...]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls, value: object) -> Self@__new__
__or__ :: bound method <class 'Answer'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
__new__ :: def __new__[Self](cls, value: object) -> Self
__or__ :: bound method <class 'Answer'>.__or__[Self](value: Any, /) -> UnionType | Self
__order__ :: str
__prepare__ :: bound method <class 'EnumMeta'>.__prepare__(cls: str, bases: tuple[type, ...], **kwds: Any) -> _EnumDict
__qualname__ :: str
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
__repr__ :: def __repr__(self) -> str
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__]
__ror__ :: bound method <class 'Answer'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT]
__ror__ :: bound method <class 'Answer'>.__ror__[Self](value: Any, /) -> UnionType | Self
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: bound method <class 'Answer'>.__subclasscheck__(subclass: type, /) -> bool
__subclasses__ :: bound method <class 'Answer'>.__subclasses__[Self]() -> list[Self@__subclasses__]
__subclasses__ :: bound method <class 'Answer'>.__subclasses__[Self]() -> list[Self]
__subclasshook__ :: bound method <class 'Answer'>.__subclasshook__(subclass: type, /) -> bool
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
@@ -3999,7 +3999,7 @@ quux.<CURSOR>
index :: bound method Quux.index(value: Any, start: SupportsIndex = 0, stop: SupportsIndex = ..., /) -> int
x :: int
y :: str
__add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], (value: tuple[_T@__add__, ...], /) -> tuple[int | str | _T@__add__, ...]]
__add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], [_T](value: tuple[_T, ...], /) -> tuple[int | str | _T, ...]]
__annotations__ :: dict[str, Any]
__class__ :: type[Quux]
__class_getitem__ :: bound method type[Quux].__class_getitem__(item: Any, /) -> GenericAlias
@@ -4025,7 +4025,7 @@ quux.<CURSOR>
__module__ :: str
__mul__ :: bound method Quux.__mul__(value: SupportsIndex, /) -> tuple[int | str, ...]
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: (x: int, y: str) -> None
__new__ :: (x: int, y: str) -> Quux
__orig_bases__ :: tuple[Any, ...]
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
@@ -8009,7 +8009,7 @@ my_list[0].remove<CURSOR>
);
assert_snapshot!(
builder.build().snapshot(),
@r"
@"
removeprefix
removesuffix
",
@@ -8030,7 +8030,7 @@ def f(x: Any | str):
);
assert_snapshot!(
builder.build().snapshot(),
@r"
@"
removeprefix
removesuffix
",
@@ -8052,13 +8052,24 @@ def f(x: Intersection[int, Any] | str):
);
assert_snapshot!(
builder.build().snapshot(),
@r"
@"
removeprefix
removesuffix
",
);
}
#[test]
fn dunder_file_completion() {
let builder = completion_test_builder("__fil<CURSOR>");
// __file__ should be `str` when accessed within a module, not `str | None`
assert_snapshot!(
builder.skip_keywords().skip_auto_import().type_signatures().build().snapshot(),
@"__file__ :: str",
);
}
/// A way to create a simple single-file (named `main.py`) completion test
/// builder.
///

View File

@@ -9,6 +9,7 @@
use regex::Regex;
use ruff_python_trivia::{PythonWhitespace, leading_indentation};
use ruff_source_file::UniversalNewlines;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::LazyLock;
@@ -349,16 +350,37 @@ fn render_markdown(docstring: &str) -> String {
| "versionchanged" | "version-changed" | "version-deprecated" | "deprecated"
| "version-removed" | "versionremoved",
) => {
// Map version directives to human-readable phrases (matching Sphinx output)
let pretty_directive = match directive.unwrap() {
"versionadded" | "version-added" => Cow::Borrowed("Added in version"),
"versionchanged" | "version-changed" => Cow::Borrowed("Changed in version"),
"deprecated" | "version-deprecated" => {
Cow::Borrowed("Deprecated since version")
}
"versionremoved" | "version-removed" => Cow::Borrowed("Removed in version"),
other => Cow::Owned(
other
.char_indices()
.map(|(index, c)| {
if index == 0 {
c.to_ascii_uppercase()
} else {
c
}
})
.collect(),
),
};
// Render the argument of things like `.. version-added:: 4.0`
let suffix = if let Some(lang) = lang {
format!(" *{lang}*")
format!(" {lang}")
} else {
String::new()
};
// We prepend without_directive here out of caution for preserving input.
// This is probably gibberish/invalid syntax? But it's a no-op in normal cases.
temp_owned_line =
format!("**{without_directive}{}:**{suffix}", directive.unwrap());
temp_owned_line = format!("**{without_directive}{pretty_directive}{suffix}:**");
line = temp_owned_line.as_str();
None
@@ -1076,7 +1098,7 @@ mod tests {
assert_snapshot!(docstring.render_markdown(), @r#"
The thing you need to understand is that computers are hard.
**warning:**
**Warning:**
&nbsp;&nbsp;&nbsp;&nbsp;Now listen here buckaroo you might have seen me say computers are hard,
&nbsp;&nbsp;&nbsp;&nbsp;and though "yeah I know computers are hard but NO you DON'T KNOW.
@@ -1115,12 +1137,12 @@ mod tests {
assert_snapshot!(docstring.render_markdown(), @"
Some much-updated docs
**version-added:** *3.0*
**Added in version 3.0:**
&nbsp;&nbsp;&nbsp;Function added
**version-changed:** *4.0*
**Changed in version 4.0:**
&nbsp;&nbsp;&nbsp;The `spam` argument was added
**version-changed:** *4.1*
**Changed in version 4.1:**
&nbsp;&nbsp;&nbsp;The `spam` argument is considered evil now.
&nbsp;&nbsp;&nbsp;You really shouldnt use it
@@ -1141,7 +1163,7 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @"
**wow this is some changes deprecated:** *1.2.3*
**wow this is some changes Deprecated since version 1.2.3:**
&nbsp;&nbsp;&nbsp;&nbsp;x = 2
");
}
@@ -1151,7 +1173,7 @@ mod tests {
fn explicit_markdown_block_with_ps1_contents() {
let docstring = r#"
My cool func:
```python
>>> thing.do_thing()
wow it did the thing
@@ -1162,7 +1184,7 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
assert_snapshot!(docstring.render_markdown(), @"
My cool func:
```python
@@ -1179,7 +1201,7 @@ mod tests {
fn explicit_markdown_block_with_underscore_contents_tick() {
let docstring = r#"
My cool func:
`````python
x_y = thing_do();
``` # this should't close the fence!
@@ -1189,7 +1211,7 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
assert_snapshot!(docstring.render_markdown(), @"
My cool func:
`````python
@@ -1205,7 +1227,7 @@ mod tests {
fn explicit_markdown_block_with_underscore_contents_tilde() {
let docstring = r#"
My cool func:
~~~~~python
x_y = thing_do();
~~~ # this should't close the fence!
@@ -1215,7 +1237,7 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
assert_snapshot!(docstring.render_markdown(), @"
My cool func:
~~~~~python
@@ -1233,7 +1255,7 @@ mod tests {
fn explicit_markdown_block_with_indent_tick() {
let docstring = r#"
My cool func...
Returns:
Some details
`````python
@@ -1246,7 +1268,7 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
assert_snapshot!(docstring.render_markdown(), @"
My cool func...
Returns:
@@ -1267,7 +1289,7 @@ mod tests {
fn explicit_markdown_block_with_indent_tilde() {
let docstring = r#"
My cool func...
Returns:
Some details
~~~~~~python
@@ -1280,7 +1302,7 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
assert_snapshot!(docstring.render_markdown(), @"
My cool func...
Returns:
@@ -1299,14 +1321,14 @@ mod tests {
fn explicit_markdown_block_with_unclosed_fence_tick() {
let docstring = r#"
My cool func:
````python
x_y = thing_do();
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
assert_snapshot!(docstring.render_markdown(), @"
My cool func:
````python
@@ -1320,14 +1342,14 @@ mod tests {
fn explicit_markdown_block_with_unclosed_fence_tilde() {
let docstring = r#"
My cool func:
~~~~~python
x_y = thing_do();
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
assert_snapshot!(docstring.render_markdown(), @"
My cool func:
~~~~~python
@@ -1342,7 +1364,7 @@ mod tests {
fn explicit_markdown_block_messy_corners_tick() {
let docstring = r#"
My cool func:
``````we still think this is a codefence```
x_y = thing_do();
```````````` and are sloppy as heck with indentation and closing shrugggg
@@ -1350,7 +1372,7 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
assert_snapshot!(docstring.render_markdown(), @"
My cool func:
``````we still think this is a codefence```
@@ -1365,7 +1387,7 @@ mod tests {
fn explicit_markdown_block_messy_corners_tilde() {
let docstring = r#"
My cool func:
~~~~~~we still think this is a codefence~~~
x_y = thing_do();
~~~~~~~~~~~~~ and are sloppy as heck with indentation and closing shrugggg
@@ -1373,7 +1395,7 @@ mod tests {
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
assert_snapshot!(docstring.render_markdown(), @"
My cool func:
~~~~~~we still think this is a codefence~~~

View File

@@ -231,7 +231,8 @@ impl<'db> Definitions<'db> {
ty_python_semantic::types::TypeDefinition::Module(module) => {
ResolvedDefinition::Module(module.file(db)?)
}
ty_python_semantic::types::TypeDefinition::Class(definition)
ty_python_semantic::types::TypeDefinition::StaticClass(definition)
| ty_python_semantic::types::TypeDefinition::DynamicClass(definition)
| ty_python_semantic::types::TypeDefinition::Function(definition)
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)

View File

@@ -1649,6 +1649,103 @@ Traceb<CURSOR>ackType
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
/// goto-definition on a dynamic class literal (created via `type()`)
#[test]
fn goto_definition_dynamic_class_literal() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
DynClass = type("DynClass", (), {})
x = DynCla<CURSOR>ss()
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Go to definition
--> main.py:4:5
|
2 | DynClass = type("DynClass", (), {})
3 |
4 | x = DynClass()
| ^^^^^^^^ Clicking here
|
info: Found 2 definitions
--> main.py:2:1
|
2 | DynClass = type("DynClass", (), {})
| --------
3 |
4 | x = DynClass()
|
::: stdlib/builtins.pyi:137:9
|
135 | def __class__(self, type: type[Self], /) -> None: ...
136 | def __init__(self) -> None: ...
137 | def __new__(cls) -> Self: ...
| -------
138 | # N.B. `object.__setattr__` and `object.__delattr__` are heavily special-cased by type checkers.
139 | # Overriding them in subclasses has different semantics, even if the override has an identical signature.
|
"#);
}
/// goto-definition on a dangling dynamic class literal (not assigned to a variable)
#[test]
fn goto_definition_dangling_dynamic_class_literal() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
class Foo(type("Ba<CURSOR>r", (), {})):
pass
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
/// goto-definition on a dynamic namedtuple class literal (created via `collections.namedtuple()`)
#[test]
fn goto_definition_dynamic_namedtuple_literal() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Poi<CURSOR>nt(1, 2)
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Go to definition
--> main.py:6:5
|
4 | Point = namedtuple("Point", ["x", "y"])
5 |
6 | p = Point(1, 2)
| ^^^^^ Clicking here
|
info: Found 1 definition
--> main.py:4:1
|
2 | from collections import namedtuple
3 |
4 | Point = namedtuple("Point", ["x", "y"])
| -----
5 |
6 | p = Point(1, 2)
|
"#);
}
// TODO: Should only list `a: int`
#[test]
fn redeclarations() {

View File

@@ -4166,6 +4166,34 @@ def function():
");
}
#[test]
fn hover_dunder_file() {
let test = cursor_test(
r#"
__fil<CURSOR>e__
"#,
);
// __file__ should be `str` when accessed within a module, not `str | None`
assert_snapshot!(test.hover(), @r"
str
---------------------------------------------
```python
str
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:1
|
2 | __file__
| ^^^^^-^^
| | |
| | Cursor offset
| source
|
");
}
impl CursorTest {
fn hover(&self) -> String {
use std::fmt::Write;

View File

@@ -456,6 +456,40 @@ reveal_type(int_container) # revealed: Container[int]
reveal_type(int_container.set_value(1)) # revealed: Container[int]
```
## Generic class with bounded type variable
This is a regression test for <https://github.com/astral-sh/ty/issues/2467>.
Calling a method on a generic class instance should work when the type parameter is specialized with
a type that satisfies a bound.
```py
from typing import NewType
class Base: ...
class C[T: Base]:
x: T
def g(self) -> None:
pass
# Calling a method on a specialized instance should not produce an error
C[Base]().g()
# Test with a NewType bound
K = NewType("K", int)
class D[T: K]:
x: T
def h(self) -> None:
pass
# Calling a method on a specialized instance should not produce an error
D[K]().h()
```
## Protocols
TODO: <https://typing.python.org/en/latest/spec/generics.html#use-in-protocols>

View File

@@ -1,7 +1,7 @@
# Unsupported special types
We do not understand the functional syntax for creating `NamedTuple`s, `TypedDict`s or `Enum`s yet.
But we also do not emit false positives when these are used in type expressions.
We do not understand the functional syntax for creating `TypedDict`s or `Enum`s yet. But we also do
not emit false positives when these are used in type expressions.
```py
import collections
@@ -11,8 +11,6 @@ import typing
MyEnum = enum.Enum("MyEnum", ["foo", "bar", "baz"])
MyIntEnum = enum.IntEnum("MyIntEnum", ["foo", "bar", "baz"])
MyTypedDict = typing.TypedDict("MyTypedDict", {"foo": int})
MyNamedTuple1 = typing.NamedTuple("MyNamedTuple1", [("foo", int)])
MyNamedTuple2 = collections.namedtuple("MyNamedTuple2", ["foo"])
def f(a: MyEnum, b: MyTypedDict, c: MyNamedTuple1, d: MyNamedTuple2): ...
def f(a: MyEnum, b: MyTypedDict): ...
```

View File

@@ -1208,7 +1208,7 @@ def _(flag: bool):
reveal_type(C1.y) # revealed: int | str
C1.y = 100
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8'>`"
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3:15'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8:15'>`"
C1.y = "problematic"
class C2:

View File

@@ -42,6 +42,12 @@ def f[T](x: T, cond: bool) -> T | list[T]:
return x if cond else [x]
l5: int | list[int] = f(1, True)
a: list[int] = [1, 2, *(3, 4, 5)]
reveal_type(a) # revealed: list[int]
b: list[list[int]] = [[1], [2], *([3], [4])]
reveal_type(b) # revealed: list[list[int]]
```
`typed_dict.py`:
@@ -297,6 +303,33 @@ def _(flag: bool):
reveal_type(x2) # revealed: list[int | None]
```
## Dunder Calls
The key and value parameters types are used as type context for `__setitem__` dunder calls:
```py
from typing import TypedDict
class Bar(TypedDict):
baz: float
def _(x: dict[str, Bar]):
x["foo"] = reveal_type({"baz": 2}) # revealed: Bar
class X:
def __setitem__(self, key: Bar, value: Bar): ...
def _(x: X):
# revealed: Bar
x[reveal_type({"baz": 1})] = reveal_type({"baz": 2}) # revealed: Bar
# TODO: Support type context with union subscripting.
def _(x: X | dict[Bar, Bar]):
# error: [invalid-assignment]
# error: [invalid-assignment]
x[{"baz": 1}] = {"baz": 2}
```
## Multi-inference diagnostics
```toml

View File

@@ -13,54 +13,6 @@ bool(1, 2)
bool(NotBool())
```
## Calls to `type()`
A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the
tests for the `__class__` attribute.)
```py
reveal_type(type(1)) # revealed: <class 'int'>
```
But a three-argument call to type creates a dynamic instance of the `type` class:
```py
class Base: ...
reveal_type(type("Foo", (), {})) # revealed: type
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
```
Other numbers of arguments are invalid
```py
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", ())
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", (), {}, weird_other_arg=42)
```
The following calls are also invalid, due to incorrect argument types:
```py
class Base: ...
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
type(b"Foo", (), {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `<class 'Base'>`"
type("Foo", Base, {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
type("Foo", (1, 2), {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
type("Foo", (Base,), {b"attr": 1})
```
## Calls to `str()`
### Valid calls

View File

@@ -464,10 +464,51 @@ reveal_type(C.f2(1)) # revealed: str
reveal_type(C().f2(1)) # revealed: str
```
### Classmethods with `Self` and callable-returning decorators
When a classmethod is decorated with a decorator that returns a callable type (like
`@contextmanager`), `Self` in the return type should correctly resolve to the subclass when accessed
on a derived class.
```py
from contextlib import contextmanager
from typing import Iterator
from typing_extensions import Self
class Base:
@classmethod
@contextmanager
def create(cls) -> Iterator[Self]:
yield cls()
class Child(Base): ...
reveal_type(Base.create()) # revealed: _GeneratorContextManager[Base, None, None]
with Base.create() as base:
reveal_type(base) # revealed: Base
reveal_type(Base().create()) # revealed: _GeneratorContextManager[Base, None, None]
with Base().create() as base:
reveal_type(base) # revealed: Base
reveal_type(Child.create()) # revealed: _GeneratorContextManager[Child, None, None]
with Child.create() as child:
reveal_type(child) # revealed: Child
reveal_type(Child().create()) # revealed: _GeneratorContextManager[Child, None, None]
with Child().create() as child:
reveal_type(child) # revealed: Child
```
### `__init_subclass__`
The [`__init_subclass__`] method is implicitly a classmethod:
```toml
[environment]
python-version = "3.12"
```
```py
class Base:
def __init_subclass__(cls, **kwargs):
@@ -480,6 +521,130 @@ class Derived(Base):
reveal_type(Derived.custom_attribute) # revealed: int
```
Subclasses must be constructed with arguments matching the required arguments of the base
`__init_subclass__` method.
```py
class Empty: ...
class RequiresArg:
def __init_subclass__(cls, arg: int): ...
class NoArg:
def __init_subclass__(cls): ...
# Single-base definitions
class MissingArg(RequiresArg): ... # error: [missing-argument]
class InvalidType(RequiresArg, arg="foo"): ... # error: [invalid-argument-type]
class Valid(RequiresArg, arg=1): ...
# error: [missing-argument]
# error: [unknown-argument]
class IncorrectArg(RequiresArg, not_arg="foo"): ...
```
For multiple inheritance, the first resolved `__init_subclass__` method is used.
```py
class Empty: ...
class RequiresArg:
def __init_subclass__(cls, arg: int): ...
class NoArg:
def __init_subclass__(cls): ...
class Valid(NoArg, RequiresArg): ...
class MissingArg(RequiresArg, NoArg): ... # error: [missing-argument]
class InvalidType(RequiresArg, NoArg, arg="foo"): ... # error: [invalid-argument-type]
class Valid(RequiresArg, NoArg, arg=1): ...
# Ensure base class without __init_subclass__ is ignored
class Valid(Empty, NoArg): ...
class Valid(Empty, RequiresArg, NoArg, arg=1): ...
class MissingArg(Empty, RequiresArg): ... # error: [missing-argument]
class MissingArg(Empty, RequiresArg, NoArg): ... # error: [missing-argument]
class InvalidType(Empty, RequiresArg, NoArg, arg="foo"): ... # error: [invalid-argument-type]
# Multiple inheritance with args
class Base(Empty, RequiresArg, NoArg, arg=1): ...
class Valid(Base, arg=1): ...
class MissingArg(Base): ... # error: [missing-argument]
class InvalidType(Base, arg="foo"): ... # error: [invalid-argument-type]
```
Keyword splats are allowed if their type can be determined:
```py
from typing import TypedDict
class RequiresKwarg:
def __init_subclass__(cls, arg: int): ...
class WrongArg(TypedDict):
kwarg: int
class InvalidType(TypedDict):
arg: str
wrong_arg: WrongArg = {"kwarg": 5}
# error: [missing-argument]
# error: [unknown-argument]
class MissingArg(RequiresKwarg, **wrong_arg): ...
invalid_type: InvalidType = {"arg": "foo"}
# error: [invalid-argument-type]
class InvalidType(RequiresKwarg, **invalid_type): ...
```
So are generics:
```py
from typing import Generic, TypeVar, Literal, overload
class Base[T]:
def __init_subclass__(cls, arg: T): ...
class Valid(Base[int], arg=1): ...
class InvalidType(Base[int], arg="x"): ... # error: [invalid-argument-type]
# Old generic syntax
T = TypeVar("T")
class Base(Generic[T]):
def __init_subclass__(cls, arg: T) -> None: ...
class Valid(Base[int], arg=1): ...
class InvalidType(Base[int], arg="x"): ... # error: [invalid-argument-type]
```
So are overloads:
```py
class Base:
@overload
def __init_subclass__(cls, mode: Literal["a"], arg: int) -> None: ...
@overload
def __init_subclass__(cls, mode: Literal["b"], arg: str) -> None: ...
def __init_subclass__(cls, mode: str, arg: int | str) -> None: ...
class Valid(Base, mode="a", arg=5): ...
class Valid(Base, mode="b", arg="foo"): ...
class InvalidType(Base, mode="b", arg=5): ... # error: [no-matching-overload]
```
The `metaclass` keyword is ignored, as it has special meaning and is not passed to
`__init_subclass__` at runtime.
```py
class Base:
def __init_subclass__(cls, arg: int): ...
class Valid(Base, arg=5, metaclass=object): ...
```
## `@staticmethod`
### Basic
@@ -621,17 +786,17 @@ argument:
```py
from typing_extensions import Self
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
reveal_type(object.__new__) # revealed: def __new__[Self](cls) -> Self
reveal_type(object().__new__) # revealed: def __new__[Self](cls) -> Self
# revealed: Overload[[Self](cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self, [Self](cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self]
reveal_type(int.__new__)
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
# revealed: Overload[[Self](cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self, [Self](cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self]
reveal_type((42).__new__)
class X:
def __init__(self, val: int): ...
def make_another(self) -> Self:
reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__
reveal_type(self.__new__) # revealed: def __new__[Self](cls) -> Self
return self.__new__(type(self))
```

File diff suppressed because it is too large Load Diff

View File

@@ -25,3 +25,22 @@ B = bytes
reveal_mro(C) # revealed: (<class 'C'>, <class 'int'>, <class 'G[bytes]'>, typing.Generic, <class 'object'>)
```
## Starred bases
These are currently not supported, but ideally we would support them in some limited situations.
```py
from ty_extensions import reveal_mro
class A: ...
class B: ...
class C: ...
bases = (A, B, C)
class Foo(*bases): ...
# revealed: (<class 'Foo'>, @Todo(StarredExpression), <class 'object'>)
reveal_mro(Foo)
```

View File

@@ -1032,4 +1032,125 @@ reveal_type(asdict(p)) # revealed: dict[str, Any]
reveal_type(replace(p, name="Bob")) # revealed: Person
```
## Calling decorator function directly with a class argument
When a function decorated with `@dataclass_transform()` is called directly with a class argument
(not used as a decorator), it should return the class with the dataclass transformation applied.
### Basic case
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
return cls
class A:
x: int
B = my_dataclass(A)
reveal_type(B) # revealed: <class 'A'>
B(1)
```
### Function with additional parameters
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T], *, order: bool = False) -> type[T]:
return cls
class A:
x: int
B = my_dataclass(A, order=True)
reveal_type(B) # revealed: <class 'A'>
reveal_type(B(1) < B(2)) # revealed: bool
```
### Overloaded decorator function
When the decorator function has overloads (one for direct class application, one for returning a
decorator), calling it with a class should return the class type.
```py
from typing_extensions import dataclass_transform, Callable, overload
@overload
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]: ...
@overload
def my_dataclass[T]() -> Callable[[type[T]], type[T]]: ...
def my_dataclass[T](cls: type[T] | None = None) -> type[T] | Callable[[type[T]], type[T]]:
raise NotImplementedError
class A:
x: int
B = my_dataclass(A)
reveal_type(B) # revealed: <class 'A'>
B(1)
```
### Passing a specialized generic class
When calling a `@dataclass_transform()` decorated function with a specialized generic class, the
specialization should be preserved.
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
return cls
class A[T]:
x: T
B = my_dataclass(A[int])
reveal_type(B) # revealed: <class 'A[int]'>
B(1)
```
### Decorator factory with class parameter
When a `@dataclass_transform()` decorated function takes a class as a parameter but is used as a
decorator factory (returns a decorator), the dataclass behavior should be applied to the decorated
class, not to the parameter class.
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def hydrated_dataclass[T](target: type[T], *, frozen: bool = False):
def decorator[U](cls: type[U]) -> type[U]:
return cls
return decorator
class Target:
pass
decorator = hydrated_dataclass(Target)
reveal_type(decorator) # revealed: <decorator produced by dataclass-like function>
@hydrated_dataclass(Target)
class Model:
x: int
# Model should be a dataclass-like class with x as a field
Model(x=1)
reveal_type(Model.__init__) # revealed: (self: Model, x: int) -> None
```
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform

View File

@@ -1208,7 +1208,7 @@ def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]:
# revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None
reveal_type(ParentDataclass.__init__)
# revealed: (self: ParentDataclass[T@ChildOfParentDataclass], value: T@ChildOfParentDataclass) -> None
# revealed: [T](self: ParentDataclass[T], value: T) -> None
reveal_type(ChildOfParentDataclass.__init__)
result_int = uses_dataclass(42)
@@ -1682,9 +1682,42 @@ def sequence4(cls: type) -> type:
class Foo: ...
ordered_foo = dataclass(order=True)(Foo)
reveal_type(ordered_foo) # revealed: type[Foo] & Any
# TODO: should be `Foo & Any`
reveal_type(ordered_foo()) # revealed: @Todo(Type::Intersection.call)
# TODO: should be `Any`
reveal_type(ordered_foo() < ordered_foo()) # revealed: @Todo(Type::Intersection.call)
reveal_type(ordered_foo) # revealed: <class 'Foo'>
reveal_type(ordered_foo()) # revealed: Foo
reveal_type(ordered_foo() < ordered_foo()) # revealed: bool
```
## Dynamic class literals
Dynamic classes created with `type()` can be wrapped with `dataclass()` as a function:
```py
from dataclasses import dataclass
# Basic dynamic class wrapped with dataclass
DynamicFoo = type("DynamicFoo", (), {})
DynamicFoo = dataclass(DynamicFoo)
# The class is recognized as a dataclass
reveal_type(DynamicFoo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
# Can create instances
instance = DynamicFoo()
reveal_type(instance) # revealed: DynamicFoo
```
Dynamic classes that inherit from a dataclass base also work:
```py
from dataclasses import dataclass
@dataclass
class Base:
x: int
# Dynamic class inheriting from a dataclass
DynamicChild = type("DynamicChild", (Base,), {})
DynamicChild = dataclass(DynamicChild)
reveal_type(DynamicChild.__dataclass_fields__) # revealed: dict[str, Field[Any]]
```

View File

@@ -38,6 +38,125 @@ reveal_type(s1 > s2) # revealed: bool
reveal_type(s1 >= s2) # revealed: bool
```
## Signature derived from source ordering method
When the source ordering method accepts a broader type (like `object`) for its `other` parameter,
the synthesized comparison methods should use the same signature. This allows comparisons with types
other than the class itself:
```py
from functools import total_ordering
@total_ordering
class Comparable:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
if isinstance(other, Comparable):
return self.value == other.value
if isinstance(other, int):
return self.value == other
return NotImplemented
def __lt__(self, other: object) -> bool:
if isinstance(other, Comparable):
return self.value < other.value
if isinstance(other, int):
return self.value < other
return NotImplemented
a = Comparable(10)
b = Comparable(20)
# Comparisons with the same type work.
reveal_type(a <= b) # revealed: bool
reveal_type(a >= b) # revealed: bool
# Comparisons with `int` also work because `__lt__` accepts `object`.
reveal_type(a <= 15) # revealed: bool
reveal_type(a >= 5) # revealed: bool
```
## Multiple ordering methods with different signatures
When multiple ordering methods are defined with different signatures, the decorator selects a "root"
method using the priority order: `__lt__` > `__le__` > `__gt__` > `__ge__`. Synthesized methods use
the signature from the highest-priority method. Methods that are explicitly defined are not
overridden.
```py
from functools import total_ordering
@total_ordering
class MultiSig:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
return True
# __lt__ accepts `object` (highest priority, used as root)
def __lt__(self, other: object) -> bool:
return True
# __gt__ only accepts `MultiSig` (not overridden by decorator)
def __gt__(self, other: "MultiSig") -> bool:
return True
a = MultiSig(10)
b = MultiSig(20)
# __le__ and __ge__ are synthesized with __lt__'s signature (accepts `object`)
reveal_type(a <= b) # revealed: bool
reveal_type(a <= 15) # revealed: bool
reveal_type(a >= b) # revealed: bool
reveal_type(a >= 15) # revealed: bool
# __gt__ keeps its original signature (only accepts MultiSig)
reveal_type(a > b) # revealed: bool
a > 15 # error: [unsupported-operator]
```
## Overloaded ordering method
When the source ordering method is overloaded, the synthesized comparison methods should preserve
all overloads:
```py
from functools import total_ordering
from typing import overload
@total_ordering
class Flexible:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
return True
@overload
def __lt__(self, other: "Flexible") -> bool: ...
@overload
def __lt__(self, other: int) -> bool: ...
def __lt__(self, other: "Flexible | int") -> bool:
if isinstance(other, Flexible):
return self.value < other.value
return self.value < other
a = Flexible(10)
b = Flexible(20)
# Synthesized __le__ preserves overloads from __lt__
reveal_type(a <= b) # revealed: bool
reveal_type(a <= 15) # revealed: bool
# Synthesized __ge__ also preserves overloads
reveal_type(a >= b) # revealed: bool
reveal_type(a >= 15) # revealed: bool
# But comparison with an unsupported type should still error
a <= "string" # error: [unsupported-operator]
```
## Using `__gt__` as the root comparison method
When a class defines `__eq__` and `__gt__`, the decorator synthesizes `__lt__`, `__le__`, and
@@ -127,6 +246,41 @@ reveal_type(c1 > c2) # revealed: bool
reveal_type(c1 >= c2) # revealed: bool
```
## Method precedence with inheritance
The decorator always prefers `__lt__` > `__le__` > `__gt__` > `__ge__`, regardless of whether the
method is defined locally or inherited. In this example, the inherited `__lt__` takes precedence
over the locally-defined `__gt__`:
```py
from functools import total_ordering
from typing import Literal
class Base:
def __lt__(self, other: "Base") -> Literal[True]:
return True
@total_ordering
class Child(Base):
# __gt__ is defined locally, but __lt__ (inherited) takes precedence
def __gt__(self, other: "Child") -> Literal[False]:
return False
c1 = Child()
c2 = Child()
# __lt__ is inherited from Base
reveal_type(c1 < c2) # revealed: Literal[True]
# __gt__ is defined locally on Child
reveal_type(c1 > c2) # revealed: Literal[False]
# __le__ and __ge__ are synthesized from __lt__ (the highest-priority method),
# even though __gt__ is defined locally on the class itself
reveal_type(c1 <= c2) # revealed: bool
reveal_type(c1 >= c2) # revealed: bool
```
## Explicitly-defined methods are not overridden
When a class explicitly defines multiple comparison methods, the decorator does not override them.
@@ -244,3 +398,200 @@ reveal_type(n1 > n2) # revealed: bool
n1 <= n2 # error: [unsupported-operator]
n1 >= n2 # error: [unsupported-operator]
```
## Non-bool return type
When the root ordering method returns a non-bool type (like `int`), the synthesized methods return a
union of that type and `bool`. This is because `@total_ordering` generates methods like:
```python
def __le__(self, other):
return self < other or self == other
```
If `__lt__` returns `int`, then the synthesized `__le__` could return either `int` (from
`self < other`) or `bool` (from `self == other`). Since `bool` is a subtype of `int`, the union
simplifies to `int`:
```py
from functools import total_ordering
@total_ordering
class IntReturn:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
if not isinstance(other, IntReturn):
return NotImplemented
return self.value == other.value
def __lt__(self, other: "IntReturn") -> int:
return self.value - other.value
a = IntReturn(10)
b = IntReturn(20)
# User-defined __lt__ returns int.
reveal_type(a < b) # revealed: int
# Synthesized methods return int (the union int | bool simplifies to int
# because bool is a subtype of int in Python).
reveal_type(a <= b) # revealed: int
reveal_type(a > b) # revealed: int
reveal_type(a >= b) # revealed: int
```
When the root method returns a type that is not a supertype of `bool`, the union is preserved:
```py
from functools import total_ordering
@total_ordering
class StrReturn:
def __init__(self, value: str):
self.value = value
def __eq__(self, other: object) -> bool:
if not isinstance(other, StrReturn):
return NotImplemented
return self.value == other.value
def __lt__(self, other: "StrReturn") -> str:
return self.value
a = StrReturn("a")
b = StrReturn("b")
# User-defined __lt__ returns str.
reveal_type(a < b) # revealed: str
# Synthesized methods return str | bool.
reveal_type(a <= b) # revealed: str | bool
reveal_type(a > b) # revealed: str | bool
reveal_type(a >= b) # revealed: str | bool
```
## Function call form
When `total_ordering` is called as a function (not as a decorator), the same validation is
performed:
```py
from functools import total_ordering
class NoOrderingMethod:
def __eq__(self, other: object) -> bool:
return True
# error: [invalid-total-ordering]
InvalidOrderedClass = total_ordering(NoOrderingMethod)
```
When the class does define an ordering method, no error is emitted:
```py
from functools import total_ordering
class HasOrderingMethod:
def __eq__(self, other: object) -> bool:
return True
def __lt__(self, other: "HasOrderingMethod") -> bool:
return True
# No error (class defines `__lt__`).
ValidOrderedClass = total_ordering(HasOrderingMethod)
reveal_type(ValidOrderedClass) # revealed: type[HasOrderingMethod]
```
## Function call form with `type()`
When `total_ordering` is called on a class created with `type()`, the same validation is performed:
```py
from functools import total_ordering
def lt_impl(self, other) -> bool:
return True
# No error: the functional class defines `__lt__` in its namespace
ValidFunctional = total_ordering(type("ValidFunctional", (), {"__lt__": lt_impl}))
InvalidFunctionalBase = type("InvalidFunctionalBase", (), {})
# error: [invalid-total-ordering]
InvalidFunctional = total_ordering(InvalidFunctionalBase)
```
## Inherited from functional class
When a class inherits from a functional class that defines an ordering method, `@total_ordering`
correctly detects it:
```py
from functools import total_ordering
def lt_impl(self, other) -> bool:
return True
def eq_impl(self, other) -> bool:
return True
# Functional class with __lt__ method
OrderedBase = type("OrderedBase", (), {"__lt__": lt_impl})
# A class inheriting from OrderedBase gets the ordering method
@total_ordering
class Ordered(OrderedBase):
def __eq__(self, other: object) -> bool:
return True
o1 = Ordered()
o2 = Ordered()
# Inherited __lt__ is available
reveal_type(o1 < o2) # revealed: bool
# @total_ordering synthesizes the other methods
reveal_type(o1 <= o2) # revealed: bool
reveal_type(o1 > o2) # revealed: bool
reveal_type(o1 >= o2) # revealed: bool
```
When the dynamic base class does not define any ordering method, `@total_ordering` emits an error:
```py
from functools import total_ordering
# Dynamic class without ordering methods (invalid for @total_ordering)
NoOrderBase = type("NoOrderBase", (), {})
@total_ordering # error: [invalid-total-ordering]
class NoOrder(NoOrderBase):
def __eq__(self, other: object) -> bool:
return True
```
## Dynamic namespace
When a `type()`-constructed class has a dynamic namespace, we assume it might provide an ordering
method (since we can't know what's in the namespace). No error is emitted when such a class is
passed to `@total_ordering`:
```py
from functools import total_ordering
from typing import Any
def f(ns: dict[str, Any]):
# Dynamic class with dynamic namespace - might have ordering methods
DynamicBase = type("DynamicBase", (), ns)
# No error: the dynamic namespace might contain __lt__ or another ordering method
@total_ordering
class Ordered(DynamicBase):
def __eq__(self, other: object) -> bool:
return True
# Also works when calling total_ordering as a function
OrderedDirect = total_ordering(type("OrderedDirect", (), ns))
```

View File

@@ -86,7 +86,7 @@ class MyClass: ...
def get_MyClass() -> MyClass:
from . import make_MyClass
# error: [invalid-return-type] "Return type does not match returned value: expected `package.foo.MyClass @ src/package/foo.py:1`, found `package.foo.MyClass @ src/package/foo.pyi:1`"
# error: [invalid-return-type] "Return type does not match returned value: expected `package.foo.MyClass @ src/package/foo.py:1:7`, found `package.foo.MyClass @ src/package/foo.pyi:1:7`"
return make_MyClass()
```

View File

@@ -0,0 +1,78 @@
# Unsupported base for dynamic `type()` classes
<!-- snapshot-diagnostics -->
## `@final` class
Classes decorated with `@final` cannot be subclassed:
```py
from typing import final
@final
class FinalClass:
pass
X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class]
```
## `Generic` base
Dynamic classes created via `type()` cannot inherit from `Generic`:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
X = type("X", (Generic[T],), {}) # error: [invalid-base]
```
## `Protocol` base
Dynamic classes created via `type()` cannot inherit from `Protocol`:
```py
from typing import Protocol
X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base]
```
## `TypedDict` base
Dynamic classes created via `type()` cannot inherit from `TypedDict` directly. Use
`TypedDict("Name", ...)` instead:
```py
from typing_extensions import TypedDict
X = type("X", (TypedDict,), {}) # error: [invalid-base]
```
## Enum base
Dynamic classes created via `type()` cannot inherit from Enum classes because `EnumMeta` expects
special dict attributes that `type()` doesn't provide:
```py
from enum import Enum
class MyEnum(Enum):
pass
X = type("X", (MyEnum,), {}) # error: [invalid-base]
```
## Enum with members
Enums with members are final and cannot be subclassed at all:
```py
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
X = type("X", (Color,), {}) # error: [subclass-of-final-class]
```

View File

@@ -1016,6 +1016,108 @@ class Color(Enum):
reveal_type(Color.RED != Color.RED) # revealed: bool
```
## Generic enums are invalid
Enum classes cannot be generic. Python does not support generic enums, and attempting to create one
will result in a `TypeError` at runtime.
### PEP 695 syntax
Using PEP 695 type parameters on an enum is invalid:
```toml
[environment]
python-version = "3.12"
```
```py
from enum import Enum
# error: [invalid-generic-enum] "Enum class `E` cannot be generic"
class E[T](Enum):
A = 1
B = 2
```
### Legacy `Generic` base class
Inheriting from both `Enum` and `Generic[T]` is also invalid:
```py
from enum import Enum
from typing import Generic, TypeVar
T = TypeVar("T")
# error: [invalid-generic-enum] "Enum class `F` cannot be generic"
class F(Enum, Generic[T]):
A = 1
B = 2
```
### Swapped order (`Generic` first)
The order of bases doesn't matter; it's still invalid:
```py
from enum import Enum
from typing import Generic, TypeVar
T = TypeVar("T")
# error: [invalid-generic-enum] "Enum class `G` cannot be generic"
class G(Generic[T], Enum):
A = 1
B = 2
```
### Enum subclasses
Subclasses of enum base classes also cannot be generic:
```toml
[environment]
python-version = "3.12"
```
```py
from enum import Enum, IntEnum
from typing import Generic, TypeVar
T = TypeVar("T")
# error: [invalid-generic-enum] "Enum class `MyIntEnum` cannot be generic"
class MyIntEnum[T](IntEnum):
A = 1
# error: [invalid-generic-enum] "Enum class `MyFlagEnum` cannot be generic"
class MyFlagEnum(IntEnum, Generic[T]):
A = 1
```
### Custom enum base class
Even with custom enum subclasses that don't have members, they cannot be made generic:
```toml
[environment]
python-version = "3.12"
```
```py
from enum import Enum
from typing import Generic, TypeVar
T = TypeVar("T")
class MyEnumBase(Enum):
def some_method(self) -> None: ...
# error: [invalid-generic-enum] "Enum class `MyEnum` cannot be generic"
class MyEnum[T](MyEnumBase):
A = 1
```
## References
- Typing spec: <https://typing.python.org/en/latest/spec/enums.html>

View File

@@ -43,13 +43,11 @@ reveal_type(len((1,))) # revealed: Literal[1]
reveal_type(len((1, 2))) # revealed: Literal[2]
reveal_type(len(tuple())) # revealed: Literal[0]
# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[],))) # revealed: Literal[1]
reveal_type(len((*[],))) # revealed: Literal[0]
# fmt: off
# TODO: Handle star unpacks; Should be: Literal[1]
reveal_type(len( # revealed: Literal[2]
reveal_type(len( # revealed: Literal[1]
(
*[],
1,
@@ -58,11 +56,8 @@ reveal_type(len( # revealed: Literal[2]
# fmt: on
# TODO: Handle star unpacks; Should be: Literal[2]
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[], *{}))) # revealed: Literal[2]
reveal_type(len((*[], 1, 2))) # revealed: Literal[2]
reveal_type(len((*[], *{}))) # revealed: Literal[0]
```
Tuple subclasses:

View File

@@ -817,7 +817,7 @@ class WithOverloadedMethod(Generic[T]):
def method(self, x: S | T) -> S | T:
return x
# revealed: Overload[(self, x: int) -> int, (self, x: S@method) -> S@method | int]
# revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int]
reveal_type(WithOverloadedMethod[int].method)
```

View File

@@ -171,6 +171,27 @@ static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
## Bounded typevars in contravariant positions
When a bounded typevar appears in a contravariant position, the actual type doesn't need to satisfy
the bound directly. The typevar can be solved to the intersection of the actual type and the bound
(e.g., `Never` when disjoint).
```py
from typing import Generic, TypeVar
T = TypeVar("T", contravariant=True)
T_int = TypeVar("T_int", bound=int)
class Contra(Generic[T]): ...
def f(x: Contra[T_int]) -> T_int:
raise NotImplementedError
def _(x: Contra[str]):
reveal_type(f(x)) # revealed: Never
```
## Invariance
With an invariant typevar, only equivalent specializations of the generic class are subtypes of or

View File

@@ -702,7 +702,7 @@ class WithOverloadedMethod[T]:
def method[S](self, x: S | T) -> S | T:
return x
# revealed: Overload[(self, x: int) -> int, (self, x: S@method) -> S@method | int]
# revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int]
reveal_type(WithOverloadedMethod[int].method)
```

View File

@@ -867,7 +867,7 @@ class ClassWithOverloadedInit[T]:
# overload. We would then also have to determine that R must be equal to the return type of **P's
# solution.
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
# revealed: Overload[[T](x: int) -> ClassWithOverloadedInit[int], [T](x: str) -> ClassWithOverloadedInit[str]]
reveal_type(into_callable(ClassWithOverloadedInit))
# TODO: revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str], (x: str) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]]
@@ -888,7 +888,7 @@ class GenericClass[T]:
def _(x: list[str]):
# TODO: This fails because we are not propagating GenericClass's generic context into the
# Callable that we create for it.
# revealed: (x: list[T@GenericClass], y: list[T@GenericClass]) -> GenericClass[T@GenericClass]
# revealed: [T](x: list[T], y: list[T]) -> GenericClass[T]
reveal_type(into_callable(GenericClass))
# revealed: ty_extensions.GenericContext[T@GenericClass]
reveal_type(generic_context(into_callable(GenericClass)))

View File

@@ -172,6 +172,23 @@ static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
## Bounded typevars in contravariant positions
When a bounded typevar appears in a contravariant position, the actual type doesn't need to satisfy
the bound directly. The typevar can be solved to the intersection of the actual type and the bound
(e.g., `Never` when disjoint).
```py
class Contra[T]:
def append(self, x: T): ...
def f[T: int](x: Contra[T]) -> T:
raise NotImplementedError
def _(x: Contra[str]):
reveal_type(f(x)) # revealed: Never
```
## Invariance
With an invariant typevar, only equivalent specializations of the generic class are subtypes of or
@@ -780,7 +797,7 @@ class B(A):
pass
class C[T]:
def check(x: object) -> TypeIs[T]:
def check(self, x: object) -> TypeIs[T]:
# this is a bad check, but we only care about it type-checking
return False
@@ -818,7 +835,7 @@ class B(A):
pass
class C[T]:
def check(x: object) -> TypeGuard[T]:
def check(self, x: object) -> TypeGuard[T]:
# this is a bad check, but we only care about it type-checking
return False

View File

@@ -153,7 +153,7 @@ already solved and specialized when the class was specialized:
from ty_extensions import generic_context
legacy.m("string", None) # error: [invalid-argument-type]
reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S@m) -> S@m
reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S) -> S
# revealed: ty_extensions.GenericContext[T@Legacy]
reveal_type(generic_context(Legacy))
# revealed: ty_extensions.GenericContext[Self@m, S@m]
@@ -344,4 +344,27 @@ class C[T]:
ok2: Inner[T]
```
## Mixed-scope type parameters
Methods can have type parameters that are scoped to the method itself, while also referring to type
parameters from the enclosing class.
```py
from typing import Generic, TypeVar
from ty_extensions import into_callable
T = TypeVar("T")
S = TypeVar("S")
class Foo(Generic[T]):
def bar(self, x: T, y: S) -> tuple[T, S]:
raise NotImplementedError
def f(x: type[Foo[T]]) -> T:
# revealed: [S](self, x: T@f, y: S) -> tuple[T@f, S]
reveal_type(into_callable(x.bar))
raise NotImplementedError
```
[scoping]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables

View File

@@ -870,6 +870,102 @@ static_assert(not has_member(F, "__match_args__"))
static_assert(not has_member(F(), "__weakref__"))
```
### Dynamic classes (created via `type()`)
Dynamic classes created using the three-argument form of `type()` support autocomplete for members
inherited from their base classes on the class object:
```py
from ty_extensions import has_member, static_assert
class Base:
base_attr: int = 1
def base_method(self) -> str:
return "hello"
class Mixin:
mixin_attr: str = "mixin"
# Dynamic class with a single base
DynamicSingle = type("DynamicSingle", (Base,), {})
# The class object has access to base class attributes
static_assert(has_member(DynamicSingle, "base_attr"))
static_assert(has_member(DynamicSingle, "base_method"))
# Dynamic class with multiple bases
DynamicMulti = type("DynamicMulti", (Base, Mixin), {})
static_assert(has_member(DynamicMulti, "base_attr"))
static_assert(has_member(DynamicMulti, "mixin_attr"))
```
Members from `object` and the `type` metaclass are available on the class object:
```py
from ty_extensions import has_member, static_assert
Dynamic = type("Dynamic", (), {})
# object members are available on the class
static_assert(has_member(Dynamic, "__doc__"))
static_assert(has_member(Dynamic, "__init__"))
# type metaclass members are available on the class
static_assert(has_member(Dynamic, "__name__"))
static_assert(has_member(Dynamic, "__bases__"))
static_assert(has_member(Dynamic, "__mro__"))
static_assert(has_member(Dynamic, "__subclasses__"))
```
Attributes from the namespace dict (third argument) are not tracked:
```py
from ty_extensions import has_member, static_assert
DynamicWithDict = type("DynamicWithDict", (), {"custom_attr": 42})
# TODO: these should pass -- namespace dict attributes are not yet available for autocomplete
static_assert(has_member(DynamicWithDict, "custom_attr")) # error: [static-assert-error]
static_assert(has_member(DynamicWithDict(), "custom_attr")) # error: [static-assert-error]
```
Dynamic classes inheriting from classes with custom metaclasses get metaclass members:
```py
from ty_extensions import has_member, static_assert
class MyMeta(type):
meta_attr: str = "meta"
class Base(metaclass=MyMeta):
base_attr: int = 1
Dynamic = type("Dynamic", (Base,), {})
# Metaclass attributes are available on the class
static_assert(has_member(Dynamic, "meta_attr"))
static_assert(has_member(Dynamic, "base_attr"))
```
However, instances of dynamic classes currently do not expose members for autocomplete:
```py
from ty_extensions import has_member, static_assert
class Base:
base_attr: int = 1
DynamicSingle = type("DynamicSingle", (Base,), {})
instance = DynamicSingle()
# TODO: these should pass; instance members should be available
static_assert(has_member(instance, "base_attr")) # error: [static-assert-error]
static_assert(has_member(instance, "__repr__")) # error: [static-assert-error]
static_assert(has_member(instance, "__hash__")) # error: [static-assert-error]
```
### Attributes not available at runtime
Typeshed includes some attributes in `object` that are not available for some (builtin) types. For

View File

@@ -140,7 +140,11 @@ If a child class's method definition is Liskov-compatible with the method defini
class, Liskov compatibility must also nonetheless be checked with respect to the method definition
on its grandparent class. This is because type checkers will treat the child class as a subtype of
the grandparent class just as much as they treat it as a subtype of the parent class, so
substitutability with respect to the grandparent class is just as important:
substitutability with respect to the grandparent class is just as important.
However, if the parent class itself already has an LSP violation with an ancestor, we do not report
the same violation for the child class. This is because the child class cannot fix the violation
without introducing a new, worse violation against its immediate parent's contract.
<!-- snapshot-diagnostics -->
@@ -156,13 +160,31 @@ class Parent(Grandparent):
def method(self, x: str) -> None: ... # error: [invalid-method-override]
class Child(Parent):
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
def method(self, x: str) -> None: ... # error: [invalid-method-override]
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`.
# However, since `Parent.method` already violates LSP with `Grandparent.method`,
# we don't report the same violation for `Child` -- it's inherited from `Parent`.
def method(self, x: str) -> None: ...
class OtherChild(Parent):
# compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
def method(self, x: int) -> None: ... # error: [invalid-method-override]
class ChildWithNewViolation(Parent):
# incompatible with BOTH `Parent.method` (str) and `Grandparent.method` (int).
# We report the violation against the immediate parent (`Parent`), not the grandparent.
def method(self, x: bytes) -> None: ... # error: [invalid-method-override]
class GrandparentWithReturnType:
def method(self) -> int: ...
class ParentWithReturnType(GrandparentWithReturnType):
def method(self) -> str: ... # error: [invalid-method-override]
class ChildWithReturnType(ParentWithReturnType):
# Returns `int` again -- compatible with `GrandparentWithReturnType.method`,
# but not with `ParentWithReturnType.method`. We report against the immediate parent.
def method(self) -> int: ... # error: [invalid-method-override]
class GradualParent(Grandparent):
def method(self, x: Any) -> None: ...
@@ -190,8 +212,9 @@ class C(B):
foo = get
class D(C):
# compatible with `C.get` and `B.get`, but not with `A.get`
def get(self, my_default): ... # error: [invalid-method-override]
# compatible with `C.get` and `B.get`, but not with `A.get`.
# Since `B.get` already violates LSP with `A.get`, we don't report for `D`.
def get(self, my_default): ...
```
## Non-generic methods on generic classes work as expected

View File

@@ -398,10 +398,10 @@ if returns_bool():
else:
class B(Y, X): ...
# revealed: (<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
# revealed: (<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25:11'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'mdtest_snippet.B @ src/mdtest_snippet.py:28:11'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
reveal_mro(B)
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28'>`"
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25:11'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28:11'>`"
class Z(A, B): ...
reveal_mro(Z) # revealed: (<class 'Z'>, Unknown, <class 'object'>)

View File

@@ -84,17 +84,490 @@ alice.id = 42
bob.age = None
```
Alternative functional syntax:
Alternative functional syntax with a list of tuples:
```py
Person2 = NamedTuple("Person", [("id", int), ("name", str)])
alice2 = Person2(1, "Alice")
# TODO: should be an error
# error: [missing-argument]
Person2(1)
reveal_type(alice2.id) # revealed: @Todo(functional `NamedTuple` syntax)
reveal_type(alice2.name) # revealed: @Todo(functional `NamedTuple` syntax)
reveal_type(alice2.id) # revealed: int
reveal_type(alice2.name) # revealed: str
```
Functional syntax with a tuple of tuples:
```py
Person3 = NamedTuple("Person", (("id", int), ("name", str)))
alice3 = Person3(1, "Alice")
reveal_type(alice3.id) # revealed: int
reveal_type(alice3.name) # revealed: str
```
Functional syntax with a tuple of lists:
```py
Person4 = NamedTuple("Person", (["id", int], ["name", str]))
alice4 = Person4(1, "Alice")
reveal_type(alice4.id) # revealed: int
reveal_type(alice4.name) # revealed: str
```
Functional syntax with a list of lists:
```py
Person5 = NamedTuple("Person", [["id", int], ["name", str]])
alice5 = Person5(1, "Alice")
reveal_type(alice5.id) # revealed: int
reveal_type(alice5.name) # revealed: str
```
### Functional syntax with variable name
When the typename is passed via a variable, we can extract it from the inferred literal string type:
```py
from typing import NamedTuple
name = "Person"
Person = NamedTuple(name, [("id", int), ("name", str)])
p = Person(1, "Alice")
reveal_type(p.id) # revealed: int
reveal_type(p.name) # revealed: str
```
### Functional syntax with tuple variable fields
When fields are passed via a tuple variable, we can extract the literal field names and types from
the inferred tuple type:
```py
from typing import NamedTuple
from ty_extensions import static_assert, is_subtype_of, reveal_mro
fields = (("host", str), ("port", int))
Url = NamedTuple("Url", fields)
url = Url("localhost", 8080)
reveal_type(url.host) # revealed: str
reveal_type(url.port) # revealed: int
# Generic types are also correctly converted to instance types.
generic_fields = (("items", list[int]), ("mapping", dict[str, bool]))
Container = NamedTuple("Container", generic_fields)
container = Container([1, 2, 3], {"a": True})
reveal_type(container.items) # revealed: list[int]
reveal_type(container.mapping) # revealed: dict[str, bool]
# MRO includes the properly specialized tuple type.
# revealed: (<class 'Url'>, <class 'tuple[str, int]'>, <class 'object'>)
reveal_mro(Url)
static_assert(is_subtype_of(Url, tuple[str, int]))
# Invalid type expressions in fields produce a diagnostic.
invalid_fields = (("x", 42),) # 42 is not a valid type
# error: [invalid-type-form] "Invalid type `Literal[42]` in `NamedTuple` field type"
InvalidNT = NamedTuple("InvalidNT", invalid_fields)
reveal_type(InvalidNT) # revealed: <class 'InvalidNT'>
# Unpacking works correctly with the field types.
host, port = url
reveal_type(host) # revealed: str
reveal_type(port) # revealed: int
# error: [invalid-assignment] "Too many values to unpack: Expected 1"
(only_one,) = url
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
a, b, c = url
# Indexing works correctly.
reveal_type(url[0]) # revealed: str
reveal_type(url[1]) # revealed: int
# error: [index-out-of-bounds]
url[2]
```
### Functional syntax with variadic tuple fields
When fields are passed as a variadic tuple (e.g., `tuple[..., *tuple[T, ...]]`), we cannot determine
the exact field count statically. In this case, we fall back to unknown fields:
```toml
[environment]
python-version = "3.11"
```
```py
from typing import NamedTuple
from ty_extensions import reveal_mro
# Variadic tuple - we can't determine the exact fields statically.
def get_fields() -> tuple[tuple[str, type[int]], *tuple[tuple[str, type[str]], ...]]:
return (("x", int), ("y", str))
fields = get_fields()
NT = NamedTuple("NT", fields)
# Fields are unknown, so attribute access returns Any and MRO has Unknown tuple.
reveal_type(NT) # revealed: <class 'NT'>
reveal_mro(NT) # revealed: (<class 'NT'>, <class 'tuple[Unknown, ...]'>, <class 'object'>)
reveal_type(NT(1, "a").x) # revealed: Any
```
Similarly for `collections.namedtuple`:
```py
import collections
from ty_extensions import reveal_mro
def get_field_names() -> tuple[str, *tuple[str, ...]]:
return ("x", "y")
field_names = get_field_names()
NT = collections.namedtuple("NT", field_names)
# Fields are unknown, so attribute access returns Any and MRO has Unknown tuple.
reveal_type(NT) # revealed: <class 'NT'>
reveal_mro(NT) # revealed: (<class 'NT'>, <class 'tuple[Unknown, ...]'>, <class 'object'>)
reveal_type(NT(1, 2).x) # revealed: Any
```
### Class inheriting from functional NamedTuple
Classes can inherit from functional namedtuples. The constructor parameters and field types are
properly inherited:
```py
from typing import NamedTuple
from ty_extensions import reveal_mro
class Url(NamedTuple("Url", [("host", str), ("path", str)])):
pass
reveal_type(Url) # revealed: <class 'Url'>
# revealed: (<class 'mdtest_snippet.Url @ src/mdtest_snippet.py:4:7'>, <class 'mdtest_snippet.Url @ src/mdtest_snippet.py:4:11'>, <class 'tuple[str, str]'>, <class 'object'>)
reveal_mro(Url)
reveal_type(Url.__new__) # revealed: (cls: type, host: str, path: str) -> Url
# Constructor works with the inherited fields.
url = Url("example.com", "/path")
reveal_type(url) # revealed: Url
reveal_type(url.host) # revealed: str
reveal_type(url.path) # revealed: str
# Error handling works correctly.
# error: [missing-argument]
Url("example.com")
# error: [too-many-positional-arguments]
Url("example.com", "/path", "extra")
```
Subclasses can add methods that use inherited fields:
```py
from typing import NamedTuple
from typing_extensions import Self
class Url(NamedTuple("Url", [("host", str), ("port", int)])):
def with_port(self, port: int) -> Self:
reveal_type(self.host) # revealed: str
reveal_type(self.port) # revealed: int
return self._replace(port=port)
url = Url("localhost", 8080)
reveal_type(url.with_port(9000)) # revealed: Url
```
For `class Foo(namedtuple("Foo", ...)): ...`, the inner call creates a namedtuple class, but the
outer class is just a regular class inheriting from it. This is equivalent to:
```py
class _Foo(NamedTuple): ...
class Foo(_Foo): # Regular class, not a namedtuple
...
```
Because the outer class is not itself a namedtuple, it can use `super()` and override `__new__`:
```py
from collections import namedtuple
from typing import NamedTuple
class ExtType(namedtuple("ExtType", "code data")):
"""Override __new__ to add validation."""
def __new__(cls, code, data):
if not isinstance(code, int):
raise TypeError("code must be int")
return super().__new__(cls, code, data)
class Url(NamedTuple("Url", [("host", str), ("path", str)])):
"""Override __new__ to normalize the path."""
def __new__(cls, host, path):
if path and not path.startswith("/"):
path = "/" + path
return super().__new__(cls, host, path)
# Both work correctly.
ext = ExtType(42, b"hello")
reveal_type(ext) # revealed: ExtType
url = Url("example.com", "path")
reveal_type(url) # revealed: Url
```
### Functional syntax with list variable fields
When fields are passed via a list variable (not a literal), the field names cannot be determined
statically. Attribute access returns `Any` and the constructor accepts any arguments:
```py
from typing import NamedTuple
from typing_extensions import Self
fields = [("host", str), ("port", int)]
class Url(NamedTuple("Url", fields)):
def with_port(self, port: int) -> Self:
# Fields are unknown, so attribute access returns Any.
reveal_type(self.host) # revealed: Any
reveal_type(self.port) # revealed: Any
reveal_type(self.unknown) # revealed: Any
return self._replace(port=port)
```
When constructing a namedtuple directly with dynamically-defined fields, keyword arguments are
accepted because the constructor uses a gradual signature:
```py
import collections
from ty_extensions import reveal_mro
CheckerConfig = ["duration", "video_fps", "audio_sample_rate"]
GroundTruth = collections.namedtuple("GroundTruth", " ".join(CheckerConfig))
# No error - fields are unknown, so any keyword arguments are accepted
config = GroundTruth(duration=0, video_fps=30, audio_sample_rate=44100)
reveal_type(config) # revealed: GroundTruth
reveal_type(config.duration) # revealed: Any
# Namedtuples with unknown fields inherit from tuple[Unknown, ...] to avoid false positives.
# revealed: (<class 'GroundTruth'>, <class 'tuple[Unknown, ...]'>, <class 'object'>)
reveal_mro(GroundTruth)
# No index-out-of-bounds error since the tuple length is unknown.
reveal_type(config[0]) # revealed: Unknown
reveal_type(config[100]) # revealed: Unknown
```
### Functional syntax signature validation
The `collections.namedtuple` function accepts `str | Iterable[str]` for `field_names`:
```py
import collections
from ty_extensions import reveal_mro
# String field names (space-separated)
Point1 = collections.namedtuple("Point", "x y")
reveal_type(Point1) # revealed: <class 'Point'>
reveal_mro(Point1) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
# String field names with multiple spaces
Point1a = collections.namedtuple("Point", "x y")
reveal_type(Point1a) # revealed: <class 'Point'>
reveal_mro(Point1a) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
# String field names (comma-separated also works at runtime)
Point2 = collections.namedtuple("Point", "x, y")
reveal_type(Point2) # revealed: <class 'Point'>
reveal_mro(Point2) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
# List of strings
Point3 = collections.namedtuple("Point", ["x", "y"])
reveal_type(Point3) # revealed: <class 'Point'>
reveal_mro(Point3) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
# Tuple of strings
Point4 = collections.namedtuple("Point", ("x", "y"))
reveal_type(Point4) # revealed: <class 'Point'>
reveal_mro(Point4) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
# Invalid: integer is not a valid typename
# error: [invalid-argument-type]
Invalid = collections.namedtuple(123, ["x", "y"])
reveal_type(Invalid) # revealed: <class '<unknown>'>
reveal_mro(Invalid) # revealed: (<class '<unknown>'>, <class 'tuple[Any, Any]'>, <class 'object'>)
# Invalid: too many positional arguments
# error: [too-many-positional-arguments] "Too many positional arguments to function `namedtuple`: expected 2, got 4"
TooMany = collections.namedtuple("TooMany", "x", "y", "z")
reveal_type(TooMany) # revealed: <class 'TooMany'>
```
The `typing.NamedTuple` function accepts `Iterable[tuple[str, Any]]` for `fields`:
```py
from typing import NamedTuple
# List of tuples
Person1 = NamedTuple("Person", [("name", str), ("age", int)])
reveal_type(Person1) # revealed: <class 'Person'>
# Tuple of tuples
Person2 = NamedTuple("Person", (("name", str), ("age", int)))
reveal_type(Person2) # revealed: <class 'Person'>
# Invalid: integer is not a valid typename
# error: [invalid-argument-type]
NamedTuple(123, [("name", str)])
# Invalid: too many positional arguments
# error: [too-many-positional-arguments] "Too many positional arguments to function `NamedTuple`: expected 2, got 4"
TooMany = NamedTuple("TooMany", [("x", int)], "extra", "args")
reveal_type(TooMany) # revealed: <class 'TooMany'>
```
### Keyword arguments for `collections.namedtuple`
The `collections.namedtuple` function accepts `rename`, `defaults`, and `module` keyword arguments:
```py
import collections
from ty_extensions import reveal_mro
# `rename=True` replaces invalid identifiers with positional names
Point = collections.namedtuple("Point", ["x", "class", "_y", "z", "z"], rename=True)
reveal_type(Point) # revealed: <class 'Point'>
reveal_type(Point.__new__) # revealed: (cls: type, x: Any, _1: Any, _2: Any, z: Any, _4: Any) -> Point
reveal_mro(Point) # revealed: (<class 'Point'>, <class 'tuple[Any, Any, Any, Any, Any]'>, <class 'object'>)
p = Point(1, 2, 3, 4, 5)
reveal_type(p.x) # revealed: Any
reveal_type(p._1) # revealed: Any
reveal_type(p._2) # revealed: Any
reveal_type(p.z) # revealed: Any
reveal_type(p._4) # revealed: Any
# Truthy non-bool values for `rename` are also handled, but emit a diagnostic
# error: [invalid-argument-type] "Invalid argument to parameter `rename` of `namedtuple()`"
Point2 = collections.namedtuple("Point2", ["_x", "class"], rename=1)
reveal_type(Point2) # revealed: <class 'Point2'>
reveal_type(Point2.__new__) # revealed: (cls: type, _0: Any, _1: Any) -> Point2
# `defaults` provides default values for the rightmost fields
Person = collections.namedtuple("Person", ["name", "age", "city"], defaults=["Unknown"])
reveal_type(Person) # revealed: <class 'Person'>
reveal_type(Person.__new__) # revealed: (cls: type, name: Any, age: Any, city: Any = "Unknown") -> Person
reveal_mro(Person) # revealed: (<class 'Person'>, <class 'tuple[Any, Any, Any]'>, <class 'object'>)
# Can create with all fields
person1 = Person("Alice", 30, "NYC")
# Can omit the field with default
person2 = Person("Bob", 25)
reveal_type(person1.city) # revealed: Any
reveal_type(person2.city) # revealed: Any
# `module` is valid but doesn't affect type checking
Config = collections.namedtuple("Config", ["host", "port"], module="myapp")
reveal_type(Config) # revealed: <class 'Config'>
# When more defaults are provided than fields, we treat all fields as having defaults.
# TODO: This should emit a diagnostic since it would fail at runtime.
TooManyDefaults = collections.namedtuple("TooManyDefaults", ["x", "y"], defaults=("a", "b", "c"))
reveal_type(TooManyDefaults) # revealed: <class 'TooManyDefaults'>
reveal_type(TooManyDefaults.__new__) # revealed: (cls: type, x: Any = "a", y: Any = "b") -> TooManyDefaults
# Unknown keyword arguments produce an error
# error: [unknown-argument]
Bad1 = collections.namedtuple("Bad1", ["x", "y"], foobarbaz=42)
reveal_type(Bad1) # revealed: <class 'Bad1'>
reveal_mro(Bad1) # revealed: (<class 'Bad1'>, <class 'tuple[Any, Any]'>, <class 'object'>)
# Multiple unknown keyword arguments
# error: [unknown-argument]
# error: [unknown-argument]
Bad2 = collections.namedtuple("Bad2", ["x"], invalid1=True, invalid2=False)
reveal_type(Bad2) # revealed: <class 'Bad2'>
reveal_mro(Bad2) # revealed: (<class 'Bad2'>, <class 'tuple[Any]'>, <class 'object'>)
# Invalid type for `defaults` (not Iterable[Any] | None)
# error: [invalid-argument-type] "Invalid argument to parameter `defaults` of `namedtuple()`"
Bad3 = collections.namedtuple("Bad3", ["x"], defaults=123)
reveal_type(Bad3) # revealed: <class 'Bad3'>
# Invalid type for `module` (not str | None)
# error: [invalid-argument-type] "Invalid argument to parameter `module` of `namedtuple()`"
Bad4 = collections.namedtuple("Bad4", ["x"], module=456)
reveal_type(Bad4) # revealed: <class 'Bad4'>
# Invalid type for `field_names` (not str | Iterable[str])
# error: [invalid-argument-type] "Invalid argument to parameter `field_names` of `namedtuple()`"
Bad5 = collections.namedtuple("Bad5", 12345)
reveal_type(Bad5) # revealed: <class 'Bad5'>
```
### Keyword arguments for `typing.NamedTuple`
The `typing.NamedTuple` function does not accept any keyword arguments:
```py
from typing import NamedTuple
# error: [unknown-argument]
Bad3 = NamedTuple("Bad3", [("x", int)], rename=True)
# error: [unknown-argument]
Bad4 = NamedTuple("Bad4", [("x", int)], defaults=[0])
# error: [unknown-argument]
Bad5 = NamedTuple("Bad5", [("x", int)], foobarbaz=42)
# Invalid type for `fields` (not an iterable)
# error: [invalid-argument-type] "Invalid argument to parameter `fields` of `NamedTuple()`"
Bad6 = NamedTuple("Bad6", 12345)
reveal_type(Bad6) # revealed: <class 'Bad6'>
```
### Starred and double-starred arguments
When starred (`*args`) or double-starred (`**kwargs`) arguments are used, we fall back to normal
call binding since we can't statically determine the arguments. This results in `NamedTupleFallback`
being returned:
```py
import collections
from typing import NamedTuple
args = ("Point", ["x", "y"])
kwargs = {"rename": True}
# Starred positional arguments - falls back to NamedTupleFallback
Point1 = collections.namedtuple(*args)
reveal_type(Point1) # revealed: type[NamedTupleFallback]
# Ideally we'd catch this false negative,
# but it's unclear if it's worth the added complexity
Point2 = NamedTuple(*args)
reveal_type(Point2) # revealed: type[NamedTupleFallback]
# Double-starred keyword arguments - falls back to NamedTupleFallback
Point3 = collections.namedtuple("Point", ["x", "y"], **kwargs)
reveal_type(Point3) # revealed: type[NamedTupleFallback]
Point4 = NamedTuple("Point", [("x", int), ("y", int)], **kwargs)
reveal_type(Point4) # revealed: type[NamedTupleFallback]
```
### Definition
@@ -154,6 +627,84 @@ class D(
class E(NamedTuple, Protocol): ...
```
However, as explained above, for `class Foo(namedtuple("Foo", ...)): ...` the outer class is not
itself a namedtuple—it just inherits from one. So it can use multiple inheritance freely:
```py
from abc import ABC
from collections import namedtuple
from typing import NamedTuple
class Point(namedtuple("Point", ["x", "y"]), ABC):
"""No error - functional namedtuple inheritance allows multiple inheritance."""
class Url(NamedTuple("Url", [("host", str), ("port", int)]), ABC):
"""No error - typing.NamedTuple functional syntax also allows multiple inheritance."""
p = Point(1, 2)
reveal_type(p.x) # revealed: Any
reveal_type(p.y) # revealed: Any
u = Url("localhost", 8080)
reveal_type(u.host) # revealed: str
reveal_type(u.port) # revealed: int
```
### Inherited tuple methods
Namedtuples inherit methods from their tuple base class, including `count`, `index`, and comparison
methods (`__lt__`, `__le__`, `__gt__`, `__ge__`).
```py
from collections import namedtuple
from typing import NamedTuple
# typing.NamedTuple inherits tuple methods
class Point(NamedTuple):
x: int
y: int
p = Point(1, 2)
reveal_type(p.count(1)) # revealed: int
reveal_type(p.index(2)) # revealed: int
# collections.namedtuple also inherits tuple methods
Person = namedtuple("Person", ["name", "age"])
alice = Person("Alice", 30)
reveal_type(alice.count("Alice")) # revealed: int
reveal_type(alice.index(30)) # revealed: int
```
The `@total_ordering` decorator should not emit a diagnostic, since the required `__lt__` method is
already present:
```py
from collections import namedtuple
from functools import total_ordering
from typing import NamedTuple
# No error - __lt__ is inherited from the tuple base class
@total_ordering
class Point(namedtuple("Point", "x y")): ...
p1 = Point(1, 2)
p2 = Point(3, 4)
# TODO: should be `bool`, not `Any | Literal[False]`
reveal_type(p1 < p2) # revealed: Any | Literal[False]
reveal_type(p1 <= p2) # revealed: Any | Literal[True]
# Same for typing.NamedTuple - no error
@total_ordering
class Person(NamedTuple):
name: str
age: int
alice = Person("Alice", 30)
bob = Person("Bob", 25)
reveal_type(alice < bob) # revealed: bool
reveal_type(alice >= bob) # revealed: bool
```
### Inheriting from a `NamedTuple`
Inheriting from a `NamedTuple` is supported, but new fields on the subclass will not be part of the
@@ -254,6 +805,34 @@ reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str
reveal_type(LegacyProperty("height", 3.4).value) # revealed: int | float
```
Generic namedtuples can also be defined using the functional syntax with type variables in the field
types. We don't currently support this, but mypy does:
```py
from typing import NamedTuple, TypeVar
T = TypeVar("T")
# TODO: ideally this would create a generic namedtuple class
Pair = NamedTuple("Pair", [("first", T), ("second", T)])
# For now, the TypeVar is not specialized, so the field types remain as `T@Pair` and argument type
# errors are emitted when calling the constructor.
reveal_type(Pair) # revealed: <class 'Pair'>
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(Pair(1, 2)) # revealed: Pair
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(Pair(1, 2).first) # revealed: T@Pair
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(Pair(1, 2).second) # revealed: T@Pair
```
## Attributes on `NamedTuple`
The following attributes are available on `NamedTuple` classes / instances:
@@ -267,6 +846,7 @@ class Person(NamedTuple):
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]]
reveal_type(Person.__slots__) # revealed: tuple[()]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: (self: Self, *, name: str = ..., age: int | None = ...) -> Self
@@ -309,6 +889,75 @@ Person = namedtuple("Person", ["id", "name", "age"], defaults=[None])
alice = Person(1, "Alice", 42)
bob = Person(2, "Bob")
reveal_type(Person.__slots__) # revealed: tuple[()]
```
## `collections.namedtuple` with tuple variable field names
When field names are passed via a tuple variable, we can extract the literal field names from the
inferred tuple type. The class is properly synthesized (not a fallback), but field types are `Any`
since `collections.namedtuple` doesn't include type annotations:
```py
from collections import namedtuple
field_names = ("name", "age")
Person = namedtuple("Person", field_names)
reveal_type(Person) # revealed: <class 'Person'>
alice = Person("Alice", 42)
reveal_type(alice) # revealed: Person
reveal_type(alice.name) # revealed: Any
reveal_type(alice.age) # revealed: Any
```
## `collections.namedtuple` with list variable field names
When field names are passed via a list variable (not a literal), we fall back to
`NamedTupleFallback` which allows any attribute access. This is a regression test for accessing
`Self` attributes in methods of classes that inherit from namedtuples with dynamic fields:
```py
from collections import namedtuple
from typing_extensions import Self
field_names = ["host", "port"]
class Url(namedtuple("Url", field_names)):
def with_port(self, port: int) -> Self:
# Fields are unknown, so attribute access returns `Any`.
reveal_type(self.host) # revealed: Any
reveal_type(self.port) # revealed: Any
reveal_type(self.unknown) # revealed: Any
return self._replace(port=port)
```
## `collections.namedtuple` attributes
Functional namedtuples have synthesized attributes similar to class-based namedtuples:
```py
from collections import namedtuple
Person = namedtuple("Person", ["name", "age"])
reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]]
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: (self: Self, *, name: Any = ..., age: Any = ...) -> Self
# _make creates instances from an iterable.
reveal_type(Person._make(["Alice", 30])) # revealed: Person
# _asdict converts to a dictionary.
person = Person("Alice", 30)
reveal_type(person._asdict()) # revealed: dict[str, Any]
# _replace creates a copy with replaced fields.
reveal_type(person._replace(name="Bob")) # revealed: Person
```
## The symbol `NamedTuple` itself
@@ -348,7 +997,7 @@ def expects_named_tuple(x: typing.NamedTuple):
reveal_type(x) # revealed: tuple[object, ...] & NamedTupleLike
reveal_type(x._make) # revealed: bound method type[NamedTupleLike]._make(iterable: Iterable[Any]) -> NamedTupleLike
reveal_type(x._replace) # revealed: bound method NamedTupleLike._replace(...) -> NamedTupleLike
# revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], (value: tuple[_T@__add__, ...], /) -> tuple[object, ...]]
# revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], [_T](value: tuple[_T, ...], /) -> tuple[object, ...]]
reveal_type(x.__add__)
reveal_type(x.__iter__) # revealed: bound method tuple[object, ...].__iter__() -> Iterator[object]

View File

@@ -69,6 +69,30 @@ def call_with_args(y: object, a: int, b: str) -> object:
return None
```
## Narrowing with named expressions (walrus operator)
When `callable()` is used with a named expression, the target of the named expression should be
narrowed.
```py
from typing import Any
class Foo:
func: Any | None
def f(foo: Foo):
first = getattr(foo, "func", None)
if callable(first):
reveal_type(first) # revealed: Any & Top[(...) -> object]
else:
reveal_type(first) # revealed: (Any & ~Top[(...) -> object]) | None
if callable(second := getattr(foo, "func", None)):
reveal_type(second) # revealed: Any & Top[(...) -> object]
else:
reveal_type(second) # revealed: (Any & ~Top[(...) -> object]) | None
```
## Assignability of narrowed callables
A narrowed callable `Top[Callable[..., object]]` should be assignable to `Callable[..., Any]`. This

View File

@@ -12,6 +12,30 @@ def _(flag: bool):
reveal_type(x) # revealed: None
```
## `None != x` (reversed operands)
```py
def _(flag: bool):
x = None if flag else 1
if None != x:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
```
This also works for `==` with reversed operands:
```py
def _(flag: bool):
x = None if flag else 1
if None == x:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1]
```
## `!=` for other singleton types
### Bool

View File

@@ -121,6 +121,31 @@ def test(x: Literal["a", "b", "c"] | None | int = None):
reveal_type(x) # revealed: Literal["a", "c"] | int
```
## No narrowing for the right-hand side (currently)
No narrowing is done for the right-hand side currently, even if the right-hand side is a valid
"target" (name/attribute/subscript) that could potentially be narrowed. We may change this in the
future:
```py
from typing import Literal
def f(x: Literal["abc", "def"]):
if "a" in x:
# `x` could also be validly narrowed to `Literal["abc"]` here:
reveal_type(x) # revealed: Literal["abc", "def"]
else:
# `x` could also be validly narrowed to `Literal["def"]` here:
reveal_type(x) # revealed: Literal["abc", "def"]
if "a" not in x:
# `x` could also be validly narrowed to `Literal["def"]` here:
reveal_type(x) # revealed: Literal["abc", "def"]
else:
# `x` could also be validly narrowed to `Literal["abc"]` here:
reveal_type(x) # revealed: Literal["abc", "def"]
```
## bool
```py

View File

@@ -16,6 +16,32 @@ def _(flag: bool):
reveal_type(x) # revealed: None | Literal[1]
```
## `None is not x` (reversed operands)
```py
def _(flag: bool):
x = None if flag else 1
if None is not x:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
reveal_type(x) # revealed: None | Literal[1]
```
This also works for other singleton types with reversed operands:
```py
def _(flag: bool):
x = True if flag else False
if False is not x:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
```
## `is not` for other singleton types
Boolean literals:

View File

@@ -556,3 +556,43 @@ def _(x: type[object], y: type[object], z: type[object]):
if issubclass(z, Invariant):
reveal_type(z) # revealed: type[Top[Invariant[Unknown]]]
```
## Narrowing with TypedDict unions
Narrowing unions of `int` and multiple TypedDicts using `isinstance(x, dict)` should not panic
during type ordering of normalized intersection types. Regression test for
<https://github.com/astral-sh/ty/issues/2451>.
```py
from typing import Any, TypedDict, cast
class A(TypedDict):
x: str
class B(TypedDict):
y: str
T = int | A | B
def test(a: Any, items: list[T]) -> None:
combined = a or items
v = combined[0]
if isinstance(v, dict):
cast(T, v) # no panic
```
## Narrowing with named expressions (walrus operator)
When `isinstance()` is used with a named expression, the target of the named expression should be
narrowed.
```py
def get_value() -> int | str:
return 1
def f():
if isinstance(x := get_value(), int):
reveal_type(x) # revealed: int
else:
reveal_type(x) # revealed: str
```

View File

@@ -347,3 +347,19 @@ def _(x: LiteralString):
else:
reveal_type(x) # revealed: LiteralString & ~Literal[""]
```
## Narrowing with named expressions (walrus operator)
When a truthiness check is used with a named expression, the target of the named expression should
be narrowed.
```py
def get_value() -> str | None:
return "hello"
def f():
if x := get_value():
reveal_type(x) # revealed: str & ~AlwaysFalsy
else:
reveal_type(x) # revealed: (str & ~AlwaysTruthy) | None
```

View File

@@ -70,6 +70,47 @@ def _(x: A | B, y: A | C):
reveal_type(y) # revealed: A
```
## The top materialization is used for generic classes
```py
# list is invariant
def f(x: list[int] | None):
if type(x) is list:
reveal_type(x) # revealed: list[int]
else:
reveal_type(x) # revealed: list[int] | None
if type(x) is not list:
reveal_type(x) # revealed: list[int] | None
else:
reveal_type(x) # revealed: list[int]
# frozenset is covariant
def g(x: frozenset[bytes] | None):
if type(x) is frozenset:
reveal_type(x) # revealed: frozenset[bytes]
else:
reveal_type(x) # revealed: frozenset[bytes] | None
if type(x) is not frozenset:
reveal_type(x) # revealed: frozenset[bytes] | None
else:
reveal_type(x) # revealed: frozenset[bytes]
def h(x: object):
if type(x) is list:
reveal_type(x) # revealed: Top[list[Unknown]]
elif type(x) is frozenset:
reveal_type(x) # revealed: frozenset[object]
else:
reveal_type(x) # revealed: object
if type(x) is not list and type(x) is not frozenset:
reveal_type(x) # revealed: object
else:
reveal_type(x) # revealed: Top[list[Unknown]] | frozenset[object]
```
## No narrowing for `type(x) is C[int]`
At runtime, `type(x)` will never return a generic alias object (only ever a class-literal object),
@@ -134,12 +175,17 @@ class IsEqualToEverything(type):
class A(metaclass=IsEqualToEverything): ...
class B(metaclass=IsEqualToEverything): ...
def _(x: A | B):
def _(x: A | B, y: object):
if type(x) == A:
reveal_type(x) # revealed: A | B
if type(x) != A:
reveal_type(x) # revealed: A | B
if type(y) == bool:
reveal_type(y) # revealed: object
else:
reveal_type(y) # revealed: object
```
## No narrowing for custom `type` callable
@@ -160,15 +206,19 @@ def _(x: A | B):
## No narrowing for multiple arguments
No narrowing should occur if `type` is used to dynamically create a class:
Narrowing does not occur in the same way if `type` is used to dynamically create a class:
```py
def _(x: str | int):
# The following diagnostic is valid, since the three-argument form of `type`
# can only be called with `str` as the first argument.
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
#
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `str | int`"
if type(x, (), {}) is str:
reveal_type(x) # revealed: str | int
# But we synthesize a new class object as the result of a three-argument call to `type`,
# and we know that this synthesized class object is not the same object as the `str` class object,
# so here the type is narrowed to `Never`!
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: str | int
```
@@ -228,8 +278,7 @@ An early version of <https://github.com/astral-sh/ruff/pull/19920> caused us to
```py
def _(val):
if type(val) is tuple:
# TODO: better would be `Unknown & tuple[object, ...]`
reveal_type(val) # revealed: Unknown & tuple[Unknown, ...]
reveal_type(val) # revealed: Unknown & tuple[object, ...]
```
## Limitations

View File

@@ -14,8 +14,8 @@ def _(
b: TypeIs[str | int],
c: TypeGuard[bool],
d: TypeIs[tuple[TypeOf[bytes]]],
e: TypeGuard, # error: [invalid-type-form]
f: TypeIs, # error: [invalid-type-form]
e: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression"
f: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
):
reveal_type(a) # revealed: TypeGuard[str]
reveal_type(b) # revealed: TypeIs[str | int]
@@ -46,12 +46,23 @@ A user-defined type guard must accept at least one positional argument (in addit
for non-static methods).
```pyi
from typing import Any, TypeVar
from typing_extensions import TypeGuard, TypeIs
# TODO: error: [invalid-type-guard-definition]
T = TypeVar("T")
# Multiple parameters are allowed
def is_str_list(val: list[object], allow_empty: bool) -> TypeGuard[list[str]]: ...
def is_set_of(val: set[Any], type: type[T]) -> TypeGuard[set[T]]: ...
def is_two_element_tuple(val: tuple[object, ...], a: str, b: str) -> TypeIs[tuple[str, str]]: ...
# error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
def _() -> TypeGuard[str]: ...
# TODO: error: [invalid-type-guard-definition]
# error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
def _(*args) -> TypeGuard[str]: ...
# error: [invalid-type-guard-definition] "`TypeIs` function must have a parameter to narrow"
def _(**kwargs) -> TypeIs[str]: ...
class _:
@@ -63,14 +74,14 @@ class _:
def _(a) -> TypeIs[str]: ...
# errors
def _(self) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
def _(self, /, *, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
def _(self) -> TypeGuard[str]: ... # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
def _(self, /, *, a) -> TypeGuard[str]: ... # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
@classmethod
def _(cls) -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
def _(cls) -> TypeIs[str]: ... # error: [invalid-type-guard-definition] "`TypeIs` function must have a parameter to narrow"
@classmethod
def _() -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
def _() -> TypeIs[str]: ... # error: [invalid-type-guard-definition] "`TypeIs` function must have a parameter to narrow"
@staticmethod
def _(*, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
def _(*, a) -> TypeGuard[str]: ... # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
```
For `TypeIs` functions, the narrowed type must be assignable to the declared type of that parameter,
@@ -86,10 +97,10 @@ def _(a: tuple[object]) -> TypeIs[tuple[str]]: ...
def _(a: str | Any) -> TypeIs[str]: ...
def _(a) -> TypeIs[str]: ...
# TODO: error: [invalid-type-guard-definition]
# error: [invalid-type-guard-definition] "Narrowed type `str` is not assignable to the declared parameter type `int`"
def _(a: int) -> TypeIs[str]: ...
# TODO: error: [invalid-type-guard-definition]
# error: [invalid-type-guard-definition] "Narrowed type `int` is not assignable to the declared parameter type `bool | str`"
def _(a: bool | str) -> TypeIs[int]: ...
```
@@ -107,12 +118,14 @@ class C:
@classmethod
def g(cls, x: object) -> TypeGuard[int]:
return True
# TODO: this could error at definition time
def h(self) -> TypeGuard[str]:
def h(
self,
) -> TypeGuard[str]: # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
return True
# TODO: this could error at definition time
@classmethod
def j(cls) -> TypeGuard[int]:
def j(cls) -> TypeGuard[int]: # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
return True
def _(x: object):
@@ -221,7 +234,7 @@ def g(a: object) -> TypeIs[int]:
return True
def _(d: Any):
if f(): # error: [missing-argument]
if f(): # error: [missing-argument] "No argument provided for required parameter `a` of function `f`"
...
if g(*d):
@@ -230,7 +243,7 @@ def _(d: Any):
if f("foo"): # TODO: error: [invalid-type-guard-call]
...
if g(a=d): # error: [invalid-type-guard-call]
if g(a=d): # error: [invalid-type-guard-call] "Type guard call does not have a target"
...
```
@@ -499,3 +512,32 @@ def _(x: object):
if f(x) and (g(x) or h(x)):
reveal_type(x) # revealed: B | (A & C)
```
## Narrowing with named expressions (walrus operator)
When a type guard is used with a named expression, the target of the named expression should be
narrowed.
```py
from typing_extensions import TypeGuard, TypeIs
def is_str(x: object) -> TypeIs[str]:
return isinstance(x, str)
def guard_str(x: object) -> TypeGuard[str]:
return isinstance(x, str)
def get_value() -> int | str:
return 1
def f():
if is_str(x := get_value()):
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: int
if guard_str(y := get_value()):
reveal_type(y) # revealed: str
else:
reveal_type(y) # revealed: int | str
```

View File

@@ -311,7 +311,7 @@ def func[T](x: T) -> T: ...
def func[T](x: T | None = None) -> T | None:
return x
reveal_type(func) # revealed: Overload[() -> None, (x: T@func) -> T@func]
reveal_type(func) # revealed: Overload[() -> None, [T](x: T) -> T]
reveal_type(func()) # revealed: None
reveal_type(func(1)) # revealed: Literal[1]
reveal_type(func("")) # revealed: Literal[""]

View File

@@ -453,3 +453,17 @@ def _(y: Y):
if isinstance(y, dict):
reveal_type(y) # revealed: dict[str, X] | dict[str, Y]
```
### Recursive alias with tuple - stack overflow test (issue 2470)
This test case used to cause a stack overflow. The returned type `list[int]` is not assignable to
`RecursiveT = int | tuple[RecursiveT, ...]`, so we get an error.
```py
type RecursiveT = int | tuple[RecursiveT, ...]
def foo(a: int, b: int) -> RecursiveT:
some_intermediate_var = (a, b)
# error: [invalid-return-type] "Return type does not match returned value: expected `RecursiveT`, found `list[int]`"
return list(some_intermediate_var)
```

View File

@@ -339,7 +339,7 @@ class A: ...
def f(x: A):
# TODO: no error
# error: [invalid-assignment] "Object of type `mdtest_snippet.A @ src/mdtest_snippet.py:12 | mdtest_snippet.A @ src/mdtest_snippet.py:13` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13`"
# error: [invalid-assignment] "Object of type `mdtest_snippet.A @ src/mdtest_snippet.py:12:7 | mdtest_snippet.A @ src/mdtest_snippet.py:13:7` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13:7`"
x = A()
```

View File

@@ -11,7 +11,7 @@ def _(flag: bool) -> None:
abs = 1
chr: int = 1
reveal_type(abs) # revealed: Literal[1] | (def abs[_T](x: SupportsAbs[_T@abs], /) -> _T@abs)
reveal_type(abs) # revealed: Literal[1] | (def abs[_T](x: SupportsAbs[_T], /) -> _T)
reveal_type(chr) # revealed: Literal[1] | (def chr(i: SupportsIndex, /) -> str)
```

View File

@@ -38,7 +38,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
# Diagnostics
```
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7'>`
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3:15'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7:15'>`
--> src/mdtest_snippet.py:11:5
|
10 | # TODO: The error message here could be improved to explain why the assignment fails.

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