## Summary
A full ty check of isort went from 204 to 172 ms in the ecosystem run.
Across all 163 projects, the average timing factor was 1.00×. Local
checks of two isort revisions on macOS took 16–23% less time. [Ecosystem
timings](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29706/ty-ecosystem/40342132072/1/timing.html)
In `if bool(stop()):`, the call to `bool` has type `bool`, but neither
branch can run if `stop()` returns `Never`. Ty uses the operand types to
work this out. In a loop, those types can depend on an assignment
guarded by the same condition. Computing types and truthiness in
separate queries can make ty revisit that loop repeatedly.
We now record each predicate’s evaluation context when we index it.
Expression inference computes the predicate’s truthiness alongside its
operand types and recomputes it if cycle recovery widens them. Ordinary
conditions and match guards both read that result.
The only warning that changed consistently across ecosystem runs was in
psycopg. It now lists the `int` class separately even though `type[str |
Any | int]` already includes it. [Ecosystem
diff](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29706/ty-ecosystem/40342132072/1/diff.html)
In stub files, ty required `@override` on the first overload even when
an implementation was present, while overload validation required the
decorator on the implementation. Honor `@override` on the implementation
whenever one exists, matching Pyright and Pyrefly. Continue using the
first overload when there is no implementation.
The implementation also becomes the location for
`invalid-explicit-override` diagnostics in these stubs.
Fixes https://github.com/astral-sh/ty/issues/4742.
## Test plan
- Checked the reported `.pyi` example, an ellipsis-only minimized
example, and a stub without an implementation using Python 3.13 with all
rules enabled; all passed without diagnostics.
- Compared decorator placement with mypy 2.4.0, Pyright 1.1.414,
basedpyright 1.40.2, and Pyrefly 1.4.0. Pyright, basedpyright, and
Pyrefly honor the implementation's decorator in stubs; mypy rejects stub
overload implementations. All four accept the decorator on the first
overload when no implementation exists.
## Ecosystem report
For head `5690fe75333fb6755662fda0cec8d6a192905d2b`, the ecosystem run
removes 27 false `missing-override-decorator` errors from pyochain stubs
whose overload implementations already have `@override`. All changes
were reproduced and classified as expected; CI passed and no ecosystem
concern blocks review. [Full ecosystem
report](https://github.com/astral-sh/ruff/pull/29470#issuecomment-6260122936).
## Summary
Reachability and capture inference both walk alternatives such as `case
A(value=v) | B(value=v)` from left to right. They can compute the same
type remaining after an alternative fails. For a wide union, each
subtraction distributes an exclusion over the union members and
normalizes the resulting union of intersections.
Cache `subject & ~definitely_matched` by its ordered operands and
program. The query applies the exclusion directly to the subject so the
intersection builder can discard impossible branches as it distributes.
Refs astral-sh/ty#4815. The benchmark is in #29726.
## Test plan
Compared optimized dev builds on Apple Silicon, reversing the order of
the two builds in alternating blocks. Criterion results are medians of
six process estimates with ten samples each. CLI results are medians of
five runs per build after warmup.
| Input | Parent | This change | Time change |
| --- | ---: | ---: | ---: |
| Criterion, 45 classes | 76.8 ms | 57.5 ms | −25% |
| Criterion, 90 classes | 235.4 ms | 146.4 ms | −38% |
| CLI, 180 classes, four class alternatives followed by a single-class
case | 330 ms | 252 ms | −24% |
| CLI, 180 classes, twelve groups of three alternatives | 3.82 s | 2.55
s | −33% |
The 90-class CLI input retired 25% fewer instructions, with peak memory
footprint within 1% of the parent. On the eight-case tuple inputs from
astral-sh/ty#4676, retired instructions changed by less than 1% for both
generic and non-generic classes.
Also applied the benchmark and this change to the tuple-pattern stack at
`a08de583` (#29203). Across four paired Criterion runs on that stack,
the 45-class case fell from 63.9 to 53.4 ms and the 90-class case fell
from 195.9 to 131.8 ms.
When ty checks a value assignment such as `Alias = A[int] | B[int] |
C[int]`, it currently normalizes `A[int] | B[int]` and then compares
those same members again while building the final union. Longer chains
repeat that work for every prefix, even when nothing ever uses the value
as a type.
Instead, this PR just stores the runtime operands of `|` in the
runtime-union object, and only builds and normalizes the actual type
union lazily, if or when the runtime union value needs to be understood
as a type. For example, checking a stored union against `TypeForm[Base]`
walks the operands once and caches the result. Checks such as
`isinstance` can still inspect the original operands.
This is an alternative to #29304, which keeps the type of every
intermediate union available. For `(A | B) | C`, it normalizes `A | B`,
then seeds a fresh `UnionBuilder` with those members. If the
construction policy and inference context match, the builder only needs
to compare `C` with the existing members. This PR stores the `A | B`
object as the left operand of the outer union. If only the final type is
requested, one union builder visits `A`, `B`, and `C`. A second request
for that same union uses the cached type. The two approaches take about
the same time when a 256-class union is consumed once.
- For a chain of assignments like `U2 = U1 | Literal[2]`, each
assignment provides a new type-variable binding context. In #29304 that
context is part of the cache key, so these repeated assignments defeat
the caching, and extending the union can revisit its prefixes even when
nothing ever asks for their types. This accounts for the slowness of
#29304 on the "Extend a literal union across 1024 assignments" case
below.
- If `Prefix` is a long union and `(Prefix | X, Prefix | Y, ...)` is
checked against `tuple[TypeForm[Base], ...]`, every extension needs its
type. #29304 can reuse the normalized `Prefix` across those extensions.
This PR walks all its members for each distinct result. With 32
extensions, #29304 is nearly twice as fast. If this case turns out to
occur commonly in practice, we could add intermediate prefix caching as
well.
This PR is larger than #29304, which leaves the runtime union
representation largely intact and adds a path in `UnionBuilder` for
reconstructing an already normalized prefix. This PR needs extra
complexity in `UnionTypeInstance`, where specialization, cycle recovery,
and runtime checks need to distinguish the original operands from the
denoted type. But I prefer keeping the complexity of these
runtime-unions in the runtime-union implementation, rather than allowing
it to leak into the core `UnionBuilder`, as it does in #29304.
This also fixes a bug (as seen in xarray in the ecosystem report) with a
runtime check list `isinstance(x, float | int)`. Previously we would
simplify this union, eliminating `int`, but `int` and `float` are
separate classes at runtime, so we would then miss that the `isinstance`
check catches integers as well. Now the `isinstance` narrowing can look
at the original runtime operands instead.
## Test plan
I timed optimized builds on Apple silicon using the same main revision
(`157caa55a5`) for all three. The #29304 build applies its reviewed
patch (`a0f998e2b4`) to that base. Each entry is the median elapsed time
in milliseconds for six fresh, single-worker CLI checks with warm
filesystem caches.
| Workload | Main | #29304 | This change |
| --- | ---: | ---: | ---: |
| Construct an unused union of 256 specialized classes | 326 | 56 | 34 |
| Check the stored union against `TypeForm[CommonBase]` | 328 | 57 | 60
|
| Extend a literal union across 1,024 assignments | 46 | 3,789 | 29 |
| Extend a union across 128 assignments and consume each prefix | 77 |
95 | 79 |
| Check 32 extensions of the same 128-class prefix as type forms | 112 |
57 | 105 |
## Ecosystem report
At `dcc4ce9890`, xarray loses two false positives because `isinstance`
now recognizes integer chunk sizes. Three existing xarray diagnostics
include the types that actually reach those branches. Two truthiness
warnings in aioredis and dd-trace-py display the same union members in a
different order. All seven changes are expected. [Full ecosystem
report](https://github.com/astral-sh/ruff/pull/29727#issuecomment-6422364992)
## Summary
A full isort check went from 218 ms to 184 ms in the ecosystem run, a
16% reduction. Across all 163 projects the average timing factor was
1.00x, and no diagnostics changed.
In this example, `ready` can be true for either type of `value`, so the
path that skips `isinstance` already accounts for both possibilities.
```python
from typing import reveal_type
def f(value: str | None, ready: bool) -> None:
if ready or isinstance(value, str):
reveal_type(value) # str | None
```
| isort | Before | After | Wall time saved | CPU time saved | Runs per
build |
| --- | ---: | ---: | ---: | ---: | ---: |
| This patch alone | 336.0 ms | 296.9 ms | 11.6% | 6.8% | 20 |
| With #29701 and #29706 | 227.1 ms | 215.6 ms | 5.1% | 2.9% | 60 |
## Summary
This PR adds a `shared-clock-mock` rule in preview. Patching a clock or
sleep function on a shared module can affect unrelated threads or tasks,
including when the target is qualified with the consuming module:
```python
from unittest.mock import Mock, patch
from example import worker
sleep = Mock()
with patch.object(worker.time, "sleep", sleep):
worker.retry()
```
Instead, you should replace the consuming module's binding to keep the
fake local:
```python
from types import SimpleNamespace
from unittest.mock import Mock, patch
from example import worker
sleep = Mock()
with patch.object(worker, "time", SimpleNamespace(sleep=sleep)):
worker.retry()
```
We also flag patches of wall-clock, monotonic, and performance-counter
readings, plus `asyncio.sleep`. The rule recognizes `patch`,
`patch.object`, pytest-mock fixtures, and `monkeypatch.setattr` /
`delattr`. (Nested `.time` and `.asyncio` targets use a naming
heuristic, so a fixture that already installed a private clock can
produce a false positive.)
## Case analysis
The ecosystem run adds 601 diagnostics across eight projects.
### Counting sleep calls in LangChain
This reduced version of LangChain’s device-login test records the
polling interval before and after the server asks the client to slow
down.
```python
sleeps = []
monkeypatch.setattr(oauth_module.time, "sleep", sleeps.append)
login_chatgpt_device(store_path=tmp_path / "x.json", poll_interval=2.0)
assert sleeps[:2] == [2.0, 7.0]
```
The OAuth module does `import time`, so `oauth_module.time` and `time`
are the same object. If another thread calls `time.sleep(1)` between the
two polls, the recorder sees `[2.0, 1, 7.0]`. Installing
`SimpleNamespace(sleep=sleeps.append)` on `oauth_module.time` keeps that
call out of the list.
### Running out of clock readings in Superset
Superset tests how long a screenshot request waits for a lock. It
patches `superset.dashboards.api.time.monotonic` with
`side_effect=[10.0, 10.5, 11.0]` and patches `time.sleep` on the same
module. An unrelated `time.monotonic()` can consume the next reading.
The request can then see the deadline early or exhaust the iterator and
raise `StopIteration`. A private clock for `superset.dashboards.api`
needs both `monotonic` and `sleep`.
### Retrying an async request in Prefect
Prefect simulates three failed SQS requests and expects one two-second
backoff. The test uses
`@patch("prefect_aws.observers.ecs.asyncio.sleep")`, but the observer
does `import asyncio`. A second task awaiting `asyncio.sleep(0)` would
also call the mock and break `assert_called_once_with(2)`.
### A fixture that already installed a private clock
Ruff can see the same patch in two tests even though only one reaches
the shared module. For example, an autouse fixture can replace
`worker.time` before the test starts.
```python
@pytest.fixture(autouse=True)
def private_clock(monkeypatch):
monkeypatch.setattr(worker, "time", SimpleNamespace(sleep=lambda _: None))
def test_retry(monkeypatch):
sleep = Mock()
monkeypatch.setattr(worker.time, "sleep", sleep) # shared-clock-mock
worker.retry()
```
The warning in `test_retry` is a false positive. At runtime the receiver
is the fixture’s `SimpleNamespace`. If the fixture returns that
namespace, the test can use `monkeypatch.setattr(private_clock, "sleep",
sleep)` and Ruff accepts it.
### A package with its own `time` module
Gevent ships a `gevent.time` module with its own `sleep` binding. The
dotted string is enough for Ruff to flag this reduced example.
```python
import time
from unittest.mock import patch
original_sleep = time.sleep
with patch("gevent.time.sleep"):
assert time.sleep is original_sleep
```
The assertion passes. Callers that look up `gevent.time.sleep` see the
patch. A caller of standard-library `time.sleep` still gets the original
function. Ruff infers the owner from the `.time.sleep` suffix in a
string target, so it reports the wrong module here.
Hi :)
## Summary
The S113 docs say it checks `requests` or `httpx` calls that omit
`timeout`, but that hasn't been true for httpx sincee #12213
httpx applies a default timeout, so leaving the argument out is fine
there -- only an explicit `timeout=None` gets reported
In the code the implicit case is only reported when the module is
`requests`
So this rewrites "What it does" & "Why is this bad?" to match what the
rule actually does
Doc comment only, no behaviour change -- closes#27868
## Test Plan
Ran ruff 0.16.3 over three files:
- `httpx.get(url)` -> nothing reported
- `httpx.get(url, timeout=None)` -> S113, "timeout set to `None`"
- `requests.get(url)` -> S113, "without timeout"
`docs/rules` is generated rather than committed, so there's nothing to
regenerate here
Drafted with AI assistance (Claude Fable 5)
---------
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
## Summary
Add two benchmarks for the cost of narrowing a `match` subject whose
type is a large union. Both check the same three OR patterns against a
union of 45 or 90 classes. Each of those arms returns `v` from a pattern
such as `C0(value=v) | C1(value=v) | C2(value=v)`. Using
`typing.Union[...]` for the subject keeps the separate cost of inferring
a long chain of `|` expressions out of the measurement.
Refs astral-sh/ty#4815
## Test plan
Timed both inputs with ty targeting Python 3.12, then changed only the
match subject to `cast(object, subject)` and timed them again. All four
inputs checked without diagnostics. The table shows median CLI wall time
over five runs.
| Classes in the union | `match subject` | `match cast(object, subject)`
|
| --- | ---: | ---: |
| 45 | 76.7 ms | 32.9 ms |
| 90 | 180.5 ms | 36.7 ms |
## Summary
Checking 4,800 calls to a generic function with four parameters took 108
→ 100 ms of CPU time. Each parameter uses the same set of type
variables, but ty rebuilt that set for every argument. Ty now computes
it once per generic context.
Calls with plain `int` parameters were essentially unchanged. Checking
all of
[PyTorch](https://github.com/pytorch/pytorch/tree/ab92945cea42e47ae398aead69af0872d340ea0f)
showed no consistent speedup.
## Summary
Checking
[`discord/state.py`](https://github.com/Rapptz/discord.py/blob/65232c38702be5844cf2ce865a4777eb1928b5d0/discord/state.py)
opened 100 Python files instead of 112. To decide whether `Interaction`
was abstract, we used to follow the return annotation on
[`Interaction.command`](https://github.com/Rapptz/discord.py/blob/65232c38702be5844cf2ce865a4777eb1928b5d0/discord/interactions.py#L355)
into `discord.ext.commands`.
```python
def cached_slot_property[Owner, Value](
name: str,
) -> Callable[[Callable[[Owner], Value]], CachedSlotProperty[Owner, Value]]: ...
class Interaction:
@cached_slot_property("_cs_command")
def command(self) -> Command[Any, ..., Any] | ContextMenu | None: ...
```
The decorator always returns a `CachedSlotProperty`. The return
annotation on `command` determines the type of the cached value, but it
cannot make the descriptor abstract. We now skip the method’s signature
when its sole decorator rules out an abstract method or property.
Checking all of discord.py showed no measurable speedup.
## Summary
Checking Home Assistant’s [HomeKit light
module](https://github.com/home-assistant/core/blob/a0162d2ff0ed061501405a617961be149b15bd1f/homeassistant/components/homekit_controller/light.py#L53)
opened 38 Python files instead of 45 and used 10% fewer instructions.
When a class declares `value: int = 0`, ty may still need to know
whether a method writes `self.value`. It used to infer `make()` in
`self.value = make()` just to answer that question. Ty now stops once it
finds a reachable assignment. The annotation supplies the type; checking
the method still validates the assigned value.
## Summary
With two files open in Home Assistant, skipping script discovery reduced
the language server’s mean time to first diagnostics from 465 ms to 354
ms on Linux (24%). Previously, ty indexed 10,768 files looking for PEP
723 scripts, even though uv integration was off by default and ty had no
script environments to prepare.
We now scan for scripts to prepare uv environments at startup and on
reload only when uv script environments are enabled (`TY_UV=scripts` or
`TY_UV=1`). With uv off, ty still reads an open script’s inline
settings.
The benchmark client doesn’t register file watchers. With dynamic file
watching enabled, ty still scans for script-specific import paths; five
paired runs showed no startup gain in that configuration.
## Summary
When a class inherits from multiple bases, ty resolves every inherited
attribute on the subclass before it knows whether another base defines
the same name. Binding a descriptor can specialize a method signature or
infer a property's read and write types. None of that is needed if
there’s nothing to compare.
This defers binding until we find a second attribute contract. It also
deduplicates bases that appear both in the MRO and through the explicit
bases. Distinct generic specializations are still checked in order.
## Summary
Home Assistant’s `Entity` class reads or writes 106 different `self`
attributes across 70 methods, but none of those methods annotate one.
When checking overrides, ty could search all 70 methods for each name
just to determine that there was no declaration.
Collect receiver annotations once per class and reuse that list when
checking overrides. When inferring an implicit attribute, skip names
that the class only reads: a read of `self.x` does not define `x`.
On a class with 256 methods that each read eight different attributes,
median CPU time dropped from 896 ms to 126 ms over six runs. ty still
reported the one incompatible annotation. Two full Home Assistant checks
on Linux went from 47.0 to 45.4 s and from 49.4 to 49.2 s, with about 21
MiB less peak memory and the same 13,419 diagnostics.
## Summary
In a reduced example from
[PyTorch](https://github.com/pytorch/pytorch/blob/ab92945cea42e47ae398aead69af0872d340ea0f/torch/_dynamo/variables/misc.py#L2755),
ty now opens 8 Python files instead of 68 to resolve
`tx.output.side_effects`. Across four profiling runs with a single
worker, median CPU time fell from 1.20 s to 0.25 s for that example and
from 1.99 s to 1.54 s for the full `misc.py` file. Diagnostics were
unchanged.
When looking up an instance attribute, ty inferred assignments in method
bodies before checking whether an annotation already determined the
type. Those assignments could pull in modules that the lookup never
needed. We now return the declared type first. If a subclass default
shadows an inherited declaration, augmented assignments such as `self.x
+= 1` in a base class can still affect the attribute’s type, so we
continue to collect them.
Co-authored-by: Charlie Marsh <charliemarsh@openai.com>
Passing overloaded generic functions to `Callable[P, object]` can cause
the constraint solver to enumerate combinations of signatures
specialized to their declared type-variable bounds. The captured
signatures already retain those bounds, so this adds substantial work
without new call-site evidence.
Track the replacement constraint's provenance and skip this
specialization for locally bound variables in captured ParamSpec
signatures when the replacement comes solely from a validity constraint.
Evidence and mixed constraints still specialize local variables, and
free variables retain normal substitution, including expansion of free
ParamSpecs into overloads.
Preserving the generic parameters also lets a callback constrained to
`(str, bytes)` receive either type when forwarded through a `ParamSpec`,
instead of being restricted to its first constraint.
Fixes https://github.com/astral-sh/ty/issues/4796.
## Test plan
Compared against `90bcf19c58b4` using the same Linux/aarch64 environment
and optimized dev profile (Rust 1.99.0, opt-level 1, LTO off):
- The issue's NumPy reproducer, using NumPy 2.5.4 and Python 3.12
semantics, improved from an 11.03 s mean across three runs to 0.101 s
across ten runs (about 110× faster). Every run completed without
diagnostics.
- The six-overload benchmark improved from 225 ms to 22.5 ms across ten
Criterion samples (about 10× faster), with no diagnostics before or
after.
## Ecosystem report
Analyzed head `5c07a3b83f78d45ccf9994f0bfa681ee25840b9a`: one expected
stable `invalid-argument-type` diagnostic rewrite in torchvision, now
describing both constraints of the generic callback. The invalid
argument remains rejected, and no regression was found in the reproduced
case. [Full ecosystem
report](https://github.com/astral-sh/ruff/pull/29649#issuecomment-6351757125).
## Summary
With 18 assignments of the form `b = (a, a)`, `ty check --output-format
concise` used to print 3 MiB for a single `call-non-callable` error. It
now prints 1.7 KiB.
Each assignment reuses the previous tuple type twice, so spelling out
the type in the error message doubles the number of `int`s at each step:
```python
def f(x: int):
a = (x, x)
b = (a, a)
b() # Object of type `tuple[tuple[int, int], tuple[int, int]]` is not callable
```
We now share a budget of 256 elements across a tuple and its nested
tuples. If a tuple's elements exceed the remaining budget, we display it
as `tuple[...]`. Deep tuples still display in full when they fit. Inlay
hints omit the text edit when the type contains an abbreviation, so
editors won't insert `tuple[...]` as an annotation.
Builds on #29200.
When an invalid implicit alias is used as a type, the error now explains
why its original right-hand side is invalid and points to that
expression. For example:
```py
Called = type(1)
# snapshot: invalid-type-form
def inspect(called: Called):
reveal_type(called) # revealed: Unknown
```
```text
error[invalid-type-form]: Variable is not allowed in parameter annotations
--> src/mdtest_snippet.py:14:21
|
14 | def inspect(called: Called):
| ^^^^^^
info: Function calls are not allowed in type alias values
--> src/mdtest_snippet.py:11:10
|
11 | Called = type(1)
| ^^^^^^^
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
```
No other type checker provides this kind of detailed origin explanation
when a value is not usable as a type alias.
Follow imports and forwarding assignments to the original definition.
Collect these explanations independently of diagnostics in the defining
file, so unchecked dependencies and local suppressions do not hide them.
Ordinary assignments remain valid at runtime; the error is reported only
where the value is used as a type.
https://github.com/astral-sh/ruff/pull/28425 added a new way to infer
implicit and PEP 613 type aliases, which I'd previously described in
https://github.com/astral-sh/ty/issues/1711. Rather than using the value
type of the alias and converting it back to its type-annotation meaning,
this system goes back to the original definition and infers it directly
as a type expression. This gives us more robust validation that implicit
type aliases are valid type expressions, and allows us to emit accurate
diagnostics both for invalid-as-type-expression and for
invalid-at-runtime.
https://github.com/astral-sh/ruff/pull/28425 used this new approach only
for recursive type aliases; this PR expands it to all implicit and PEP
613 type aliases, so that we consistently get its benefits. This also
significantly improves our conformance results for implicit type
aliases; we now pass two more conformance files.
This is an example of the sort of thing that we previously accepted as a
type alias, and now reject (as the conformance suite says we should):
```py
BadAlias = (lambda: int)()
def f(value: BadAlias): # Main: int, Now: invalid-type-form
...
```
This also makes us more consistent (with ourselves, other type checkers
,and the conformance suite) when a generic type is used "bare" in an
alias. The core rule we use is that the RHS of an implicit or PEP 613
type alias is a type expression, therefore a use of a bare generic alias
in that type expression should be implicitly default-specialized.
```py
from typing import TypeVar
T = TypeVar("T", default=str)
GenAlias = list[T]
AnotherAlias = GenAlias | int
# this is now an error: AnotherAlias is not generic
def _(x: AnotherAlias[bytes]): ...
# The bare reference to GenAlias inside AnotherAlias is default-specialized:
def _(x: AnotherAlias):
reveal_type(x) # list[str] | int
```
We do make one carve-out from this rule: we consider a plain assignment
like `A = B` or `A = module.B` to be a direct "rename" or "forward" that
doesn't really treat the RHS as a type expression, and thus doesn't
default-specialize anything; instead `A` just becomes a new name for
`B`. This feels more intuitive, and matches pyrefly and pyright. This
carve-out does not apply to PEP 613 type aliases: `A: TypeAlias = B` is
explicitly a type alias, not a rename, so it does default-specialize
`B`.
Reject computed values used as types, including values that evaluate to
classes or `Any`, and preserve that validation when `invalid-type-form`
is disabled. Definition lookup stays behind a tracked query so unrelated
edits in an imported module do not invalidate consumers.
Fixes https://github.com/astral-sh/ty/issues/1711.
Fixes https://github.com/astral-sh/ty/issues/1846.
Prepares https://github.com/astral-sh/ty/issues/4683. Runtime `|`
behavior is not changed here.
Source-location diagnostic explanations for invalid aliases are added in
the follow-up #29654.
## Ecosystem
[Full ecosystem
analysis](https://github.com/astral-sh/ruff/pull/29200#issuecomment-6353595871)
for `b175c73` covers all 150 diagnostic changes across 21 projects: 61
additions, 58 removals, and 31 message changes. New diagnostics
primarily validate computed values used as types and default-specialized
explicit aliases. Improved alias inference removes cascading errors and
preserves forward references and generic parameters.
The nine new discord.py false positives match the known
[ty#4016](https://github.com/astral-sh/ty/issues/4016) callback-context
limitation and are accepted for this PR. No other new false positives
were identified.
Conformance gains six expected diagnostics (five in
`aliases_implicit.py`, one in `aliases_explicit.py`) and two fully
passing files, with no new conformance false positives.
Summary
--
As pointed out in [#29635], we still had this `Emitter` trait, despite
almost all output formats
moving to `ruff_db` and its `Renderer` trait. This PR cleans up the last
use of `Emitter` in Ruff's
`Grouped` output format, as well as some custom settings handling in the
`GroupedEmitter` that is
now addressed by the shared `DisplayDiagnosticConfig`.
If we want to move this output format to `ruff_db` or share it with ty
in the future, this should
also help with that.
Test Plan
--
Existing tests
[#29635]:
https://github.com/astral-sh/ruff/pull/29635#discussion_r4362259048
## Summary
Fixes part of #15874: UP032 emitted broken fixes for str.format calls
with no f-string equivalent. Non-identifier attributes, quote-clashing
or backslash keys, unknown conversions, and mixed numbering are now
skipped. Signed indices like {+0} were a parser bug (leading +
accepted), which also affected F523/F524
## Test Plan
New mdtest at crates/ruff_linter/resources/mdtest/pyupgrade/f-string.md
plus parser unit tests
## Summary
Changing the lexer's keyword set now takes one edit to the Rust list.
The compiler finds a collision-free multiplier and builds the 64-slot
table, so we can remove the Python generator and its lockfile.
A two-byte keyword like `if` packs to `0x6669_0000_0000_0000`. Only the
low two bytes of the multiplier affect its slot. We place keywords from
shortest to longest, choosing each new byte of the multiplier without
disturbing the shorter keywords. The lexer still selects a slot with one
multiply and shift, then compares the full word.
This patch has us compare members during protocol-to-protocol
comparisons using the same "structural priority" that we use during
nominal-to-protocol comparisons. This ensures that we check simple
non-recursive members first, often allowing us to determine a (failed)
comparison result before having to perform any expensive recursive
member checks.
Summary
--
This PR fixesastral-sh/ty#2484 by moving our SARIF rendering from
`ruff_linter` into `ruff_db` and
wiring that up as an output format for ty.
The first commit introduces the metadata and makes some other
preparatory changes to the SARIF
emitter in Ruff. The second commit then moves the emitter to `ruff_db`
and removes the remaining
Ruff-specific calls. The third commit then wires that up to ty's CLI.
Test Plan
--
Existing tests, adapted to `ruff_db`, as well as a new ty CLI test
## Summary
We give ASCII identifiers a dedicated path, avoiding Unicode bookkeeping
and checking string prefixes only when a one- or two-letter name reaches
a quote. Both paths share the existing batched ASCII scanner, preserving
fast scanning for Unicode names with long ASCII suffixes.
Stacked on #29463 to measure the incremental improvement. [CodSpeed
reports](https://github.com/astral-sh/ruff/pull/29503/checks?check_run_id=118300735426)
6.6–9.8% greater efficiency across all five lexer benchmarks, with no
reported regressions. We preserve the existing scanner and add no
inlining attributes or unsafe code.
## Summary
We scan ASCII identifier continuations in sixteen-byte batches using a
byte lookup table, avoiding per-character decoding. Keyword lookup
filters by length and initial byte, then uses a compact hash table with
a full-word comparison to reject collisions. Non-ASCII suffixes retain
the existing Unicode path.
Handle `bot:ai-policy-comment` and `bot:ai-policy-close` on issues and
pull requests with the shared `ai-policy` action. The automation
dispatcher runs `ai-policy.yml` on `main`, where an STS-issued
`astral-automations-bot` token posts the policy reminder and optionally
closes the target.
Draft and fork PRs are supported. The workflow checks the current label
and state without checking out PR code.
Co-authored-by: zaniebot <242828183+zaniebot@users.noreply.github.com>
The bottom signature is trivially assignable _to_, and the top signature
is trivially assignable _from_, all other signatures. We can add fast
path checks for those before performing more complex signature
comparisons.
## Summary
This PR adds support for the `<field>_id` attributes that Django
generates for foreign keys. Their types follow the related model's
primary key, or the field specified by `to_field`.
Example:
```py
from django.db import models
class Author(models.Model):
pass
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE)
def _(book: Book):
reveal_type(book.author) # Author
reveal_type(book.author_id) # int
```
Towards https://github.com/astral-sh/ty/issues/1018.
## Test Plan
New Markdown tests. I checked against Django runtime behavior.
## Summary
This PR adds support for the `get_next_by_<field>()` and
`get_previous_by_<field>()` methods that Django generates for
(non-`null=True`) `DateField` and `DateTimeField` fields.
Example:
```py
from django.db import models
class Article(models.Model):
published = models.DateField()
def _(article: Article):
reveal_type(article.get_next_by_published()) # Article
reveal_type(article.get_previous_by_published()) # Article
```
Towards https://github.com/astral-sh/ty/issues/1018.
## Test Plan
New Markdown tests.
Summary
--
This addresses the suggestion from [#29635]. `DisplayDiagnosticConfig`
previously determined the
`DiagnosticFormat` for diagnostics coming from both Ruff and ty, which
meant that it had to have a
shared enum between the two, despite each program supporting a different
subset of the available
formats. This PR eliminates the shared `DiagnosticFormat` and dispatch
through `DisplayDiagnostics`,
leaving Ruff and ty to construct `Renderer`s directly and do their own
dispatch on the output
formats they actually support. This will be especially helpful in
#29635, where we want to provide
slightly different arguments to each new `SarifRenderer`.
The `Renderer` trait itself isn't strictly necessary, but it made the
calls on the Ruff side a lot
neater since the trait provides a default `write` method based on the
required `render` method.
Codex also threw in a default `display` method that helps in a couple of
places too.
Test Plan
--
Existing tests
[#29635]:
https://github.com/astral-sh/ruff/pull/29635#discussion_r4352505588
## Summary
This PR adds support for the `get_<field>_display()` methods that Django
generates for model fields with `choices`.
Example:
```py
from django.db import models
class Article(models.Model):
status = models.CharField(
max_length=1,
choices=[("D", "Draft"), ("P", "Published")],
)
article = Article(status="P")
reveal_type(article.status) # str
reveal_type(article.get_status_display()) # str ("Published" at runtime)
```
Towards https://github.com/astral-sh/ty/issues/1018.
## Test Plan
New Markdown test.
Report comments include a redundant ZIP download link after the hosted
HTML links. Show the ZIP link only when the HTML report is unavailable,
including when its upload fails.
Co-authored-by: zaniebot <242828183+zaniebot@users.noreply.github.com>
ty ecosystem comments currently require downloading a ZIP to view the
full reports. Add an optional `upload-html` mode to the shared report
publisher and enable it for ty, linking directly to the diagnostic diff
and timing results while retaining the downloadable artifact.
The publisher runs from `main`, uses STS for GitHub credentials, and
obtains temporary AWS upload credentials through GitHub OIDC when
hosting is enabled. Each caller supplies filenames and content types,
with optional link labels. The publisher reads only those files from a
bounded archive without extracting or executing them during publication,
and stores reports under separate paths for each PR, report, run, and
attempt. If an upload fails, the publisher still attempts to post the
summary and, when available, the artifact link.
The `automations` environment requires `REPORTS_BUCKET`, `REPORTS_URL`,
`REPORTS_ROLE_ARN`, and `REPORTS_REGION` from the report hosting
infrastructure.
---------
Co-authored-by: Zanie Blue <2586601+zanieb@users.noreply.github.com>
Co-authored-by: zaniebot <242828183+zaniebot@users.noreply.github.com>
## Summary
This PR adds basic tests for Django support in ty, and adds a first
customization point where we now synthesize `.id` and `.pk` attributes
for primary keys on Django models. Django support currently relies on
`django-types`, a fork of `django-stubs` which requires less
special-casing in type checkers.
Example
```py
from django.db import models
class Book(models.Model):
isbn = models.CharField(max_length=13, primary_key=True)
title = models.CharField()
pages = models.IntegerField(null=True)
def _(book: Book):
reveal_type(book.isbn) # str
reveal_type(book.title) # str
reveal_type(book.pages) # int | None (because null=True)
reveal_type(book.pk) # str (because `isbn` is the primary key)
book.id # error: no automatic `id` with a custom primary key
```
## Test Plan
I wrote new Markdown test and also verified all of our assertions
against Django's runtime behavior.
Linux CI installs `mold` by replacing the system linker, but the current
Rust toolchain and MSRV already select bundled `lld` for
`x86_64-unknown-linux-gnu`. Remove the installation steps to avoid
downloading `mold` and changing the system linker for these jobs.
[Eight pairs of `ruff` debug
builds](https://github.com/astral-sh/ruff/actions/runs/39841713358) on
16-vCPU CI runners averaged 28.6 s with `mold` installation and 27.5 s
without. Each pair built both variants on the same runner from empty
target directories, with build order alternated. Times include
installation when applicable and exclude dependency downloads. The mean
change was −1.1 s (two-sided 90% paired t confidence interval: −8.8 to
+6.7 s); the interval includes zero. The second build was faster in
every pair, indicating a substantial order effect.
Co-authored-by: zaniebot <242828183+zaniebot@users.noreply.github.com>
## Summary
We make redundancy comparisons converge when they participate in a Salsa
inference cycle. Comparing inferred callable signatures can depend on
the union being simplified: removing an alternative changes the inferred
signatures, which can make the next comparison return a different
answer.
The query still starts with `true`, but combines successive results with
`previous && current`. Once a comparison returns `false`, it remains
false for that cycle. Retaining the union alternative prevents the
comparison from oscillating; the tradeoff is that a redundant
alternative may remain in the union.
This is a prerequisite for #29553, whose stable lambda identities expose
this cycle. The regression examples belong to that PR because they
already terminate on `main`.