Skip to content

Merge upstream - #48

Merged
KotlinIsland merged 223 commits into
mainfrom
merge-upstream
Jul 23, 2026
Merged

Merge upstream#48
KotlinIsland merged 223 commits into
mainfrom
merge-upstream

Conversation

@KotlinIsland

Copy link
Copy Markdown
Owner

No description provided.

ntBre and others added 30 commits July 9, 2026 15:42
These tests are currently split across `assignment/annotations.md` and
`bidirectional.md` somewhat arbitrarily. I merged the two and
reorganized some of the larger snippets which had devolved into
catch-all sections.
…TZ002`) (#26658)

## Summary

This updates the DTZ002 documentation to clarify the `datetime.today()`
naming confusion raised in #23392.

- Notes that `datetime.datetime.today()` is named like a date helper,
but it returns a local `datetime`.
- Keeps the existing guidance to use timezone-aware `datetime.now(...)`
when you need an aware timestamp.

## Why this changed

The code behavior is already correct; this is a docs-only improvement to
make the rule intent easier to understand.

## Test Plan

- `cargo dev generate-docs`
- `cargo fmt --check --all`
- `PATH="$PWD/target/debug:$PATH" uv run --only-group dev --locked
python scripts/check_docs_formatted.py`

---------

Co-authored-by: Mahadev Annabhimoju <219508079+Joosboy@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
…105`) (#26423)

Summary
--

This is the first in a series of migration rules to help users move from
`noqa` comments with rule
codes to `ruff:ignore` comments with names. This rule simply replaces
`noqa: codes` with
`ruff:ignore[codes]` (and the file-level variants), deferring the
transformation of codes to names
to a follow-up rule. This rule may be useful on its own without the
codes -> names rule, assuming we
stabilize `ruff:ignore` in the next minor release without stabilizing
human-readable names.

The rule skips any comment consisting entirely of known `external`
selectors, while still emitting diagnostics for totally unknown codes.
We also skip file-level `flake8: noqa` directives assuming they are
intended to be shared with flake8.

I initially wrote a small, standalone implementation in the rule file
itself, but it had to be
combined with the `RUF100` implementation to leave unused `noqa` codes
out of `RUF105` to be cleaned
up by `RUF100` instead, as I note in the mdtest for this case.

Test Plan
--

New mdtests

I also expect a huge number of ecosystem results on this PR, possibly so
many that the check times out.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
…2901`) (#25733)

## Summary

Addresses #24986

The rule has been updated to ignore cases where a mutable object is
updated in-place. This makes the behavior consistent with how
method‑based updates are already ignored.

## Test Plan

Added new test cases
…29`) (#26641)

## Summary

This updates the RUF029 documentation to mention that an async function
can sometimes be intentional even when it does not directly await
anything.

One example is an async test or callback that needs to run inside an
async execution context, such as code that uses a ContextVar.

The docs now point users toward either adding a real await when
appropriate or disabling the rule for that specific function when the
async boundary is intentional.

Refs #23196.

## Test Plan

- `cargo dev generate-docs`
- `cargo fmt --check --all`
- `PATH="$PWD/target/debug:$PATH" uv run --only-group dev --locked
python scripts/check_docs_formatted.py`
- `PATH="$PWD/target/debug:$PATH" uv run --only-group dev --locked prek
run --files crates/ruff_linter/src/rules/ruff/rules/unused_async.rs`
- `cargo test -p ruff_linter
rules::ruff::tests::rules::rule_unusedasync_path_new_ruf029_py_expects`

Co-authored-by: Mahadev Annabhimoju <219508079+Joosboy@users.noreply.github.com>
## Summary

Prior to this change, we treated a protocol member produced by a
descriptor decorator as the descriptor object itself. This caused a
class using `cached_property` to fail an otherwise matching protocol
because we compared the instance read type, `str`, with
`cached_property[str]`:

```python
from functools import cached_property
from typing import Protocol

class HasName(Protocol):
    @cached_property
    def name(self) -> str: ...

class WithName:
    @cached_property
    def name(self) -> str:
        return "example"

has_name: HasName = WithName()
```

We now resolve descriptor-decorated protocol methods through their
instance interface. The result of `__get__` determines the readable
member type, and the value parameter of a single ordinary `__set__`
signature determines the writable member type. This lets
`cached_property` implementations satisfy the protocol and preserves the
expected type when reading from or assigning through a protocol
instance.

This PR is focused on the simple case and leaves others as TODOs (with
our existing, conservative behavior).

Closes astral-sh/ty#3953.
## Summary

Prior to this change, we rejected an unhashable class even though its
`__hash__` matched the protocol:

```python
from typing import ClassVar, Protocol

class NotHashableProto(Protocol):
    __hash__: ClassVar[None]

class NotHashable:
    __hash__: ClassVar[None] = None

def accepts(value: NotHashableProto) -> None: ...

# Before: rejected
# After: accepted
accepts(NotHashable())
```

The problem is that we were checking the inherited `type.__hash__`
instead of the class's own `__hash__ = None`.

For this case, class-object reads already used the correct precedence:
metaclass data descriptor, then class attribute, then metaclass non-data
descriptor. For writes, though, we treated _any_ metaclass member as the
primary and only consulted the class attribute as a fallback. We now let
an always-defined class member take precedence over an ordinary
metaclass member that is known not to be a data descriptor, while
preserving uncertainty for gradual metaclass members.

Closes astral-sh/ty#3951.
<!--
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?
- Does this PR follow our AI policy
(https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
This improves our tests by making significant whitespace visible in
snapshot output. Two trailing spaces at the end of a Markdown paragraph
preserve a hard break (rendered as `<HB>` in snapshots) at the end of
that line.

## Test Plan

See included tests.
<!-- How was it tested? -->
…e in structural rendering. (#26691)

<!--
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?
- Does this PR follow our AI policy
(https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
This just picks a new name for a type that makes more sense with
[upcoming markdown rendering for Google-style
docstrings](astral-sh/ruff#26599).

## Test Plan

Relies on existing test coverage.
<!-- How was it tested? -->
## Summary

Our data-descriptor predicates recursively inspect outer unions,
intersections, and type aliases. An unguarded recursive alias can
therefore revisit the same classification:

```python
class Descriptor:
    def __set__(self, instance: object, value: object) -> None: ...

type NonData = int | NonDataTail
type NonDataTail = NonData

type Data = Descriptor | DataTail
type DataTail = Data
```

We now evaluate both descriptor classifiers as Salsa queries and route
alias expansion back through those queries. Definite classifications use
`true`, the identity for their all-of union folds, as cycle recovery;
possible-data classification uses `false`, the identity for its any-of
fold.

The regression test exercises `is_data_descriptor`,
`may_be_data_descriptor`, and `is_definitely_non_data_descriptor` for
recursive aliases with and without a reachable data descriptor.
## Summary

Add support for `Annotated[T, Field(...)]`, `Strict(..)` and
(transitively) also support for aliases like `StrictInt`.

One challenge here is that aliases like `StrictInt` are defined as
`StrictInt = Annotated[int, Strict()]`. When we see `StrictInt` in a
field annotation, we have already lost the `Annotated` metadata. For
now, we simply resolve this by following the alias back to it's original
definition, since it seems wrong to store `Annotated` metadata (or a
link back to its definition) inside the type for this specific use-case
only.

closes astral-sh/ty#2130
closes astral-sh/ty#3948
towards astral-sh/ty#2403

## Test Plan

New and updated Markdown tests.
## Summary

`Name` represents a Python identifier, but ty's IDE paths also used it
for arbitrary text such as dotted module and qualified names,
synthesized completion insertions, and dotted base-class references.

Use `CompactString` for completion matching, display, insertion, and
fully qualified symbol text, and construct `ModuleName` directly for
module references used in `__all__` resolution. This keeps dotted and
generated text out of the AST identifier type, avoids intermediate
`String` allocations when formatting completion text and module paths,
and preserves the existing completion and symbol behavior.
## Summary

We allocated a new `Box<dyn Write>` for every `Write` call (unless the
compiler managed to remove it). This is rather expensive.

This PR replaces the `handle() -> Box<dyn Write>` function with
`with_handle` that takes a callback instead. This avoids the allocation
entirely.

> On 50k diagnostics, concise improved from 497.1ms to 475.2ms—about
13.5% faster for rendering-only time.

Related to astral-sh/ty#3958.

## Test Plan

Testing: Passed the ty test suite and repository hooks.
## Summary

A Pydantic model with a base class with a custom `__init__` method
previously prevented us from adding `extra: **Unknown` to the models
constructor.

## Test Plan

Added Markdown tests
<!--
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?
- Does this PR follow our AI policy
(https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)?
-->

## Summary

This corrects how we expand tabs as part of PEP-257 normalization of
docstrings.

Previously, we incorrectly replaced each tab in the docstring with 8
spaces. However, as per [the reference
implementation](https://peps.python.org/pep-0257/#handling-docstring-indentation)
(and the [`str.expandtabs`
docs](https://docs.python.org/3/library/stdtypes.html#str.expandtabs)),
we should instead treat a tab as a directive to advance to the next tab
stop (the next column at a multiple of 8).

This matters for Google-style parameter extraction when a tab follows
spaces. For example, consider an `Args:` section whose first item is
indented with two spaces followed by a tab, while the second uses eight
spaces:

```python
def example(first: str, second: str):
    """Summary.

    Args:
      \tfirst: First parameter.
        second: Second parameter.
    """
```

The previous normalization produced this docstring. Because the
parameter items have different indentation, `second` is treated as
continuation text for `first` rather than as a separate parameter:

```text
Summary.

Args:
          first: First parameter.
    second: Second parameter.
```

Whereas the correct interpretation according to PEP 257 aligns the
parameter items, allowing documentation to be extracted for both `first`
and `second`:

```text
Summary.

Args:
    first: First parameter.
    second: Second parameter.
```

## Test Plan

See included tests.
<!-- How was it tested? -->
## Summary

This PR allows `str`, `int`, and `bytes` types to narrow to literal
values when an equality check succeeds by default.

The behavior is unsound because a subclass could override `__eq__`, but
it is convenient for users and matches other type checkers. Users who
prefer conservative behavior can enable `strict-literal-narrowing`; the
option defaults to `false`.

```toml
[tool.ty.analysis]
strict-literal-narrowing = true
```

For example, the default behavior allows:

```python
from typing import Literal

type Choice = Literal["a", "b"]

def parse(value: str, choices: list[Choice]) -> Choice | None:
    if value in choices:
        reveal_type(value)  # Literal["a", "b"]
        return value
    return None
```

Closes astral-sh/ty#1566.
Closes astral-sh/ty#1659.
Closes astral-sh/ty#2178.
Closes astral-sh/ty#2211.
Closes astral-sh/ty#3233.
Closes astral-sh/ty#3852.
…(#26696)

## Summary

We generally treat `Callable`-typed dunder class members as
function-like descriptors. This is deliberately a name-based heuristic:
a `Callable` annotation can describe either a function descriptor or an
arbitrary callable object, so the type alone does not tell us whether
instance access should bind the first parameter. The heuristic
nevertheless supports common patterns where special methods are
installed dynamically and explicitly include the instance in their
signature:

```python
def pow_impl(tensor: Tensor, exponent: int) -> Tensor: ...

class Tensor:
    __pow__: Callable[[Tensor, int], Tensor] = pow_impl

Tensor() ** 2
```

There are two narrow cases where we should not apply this heuristic. A
callable with no parameters cannot accept a receiver, and a callable
parameterized directly by a `ParamSpec` does not identify a receiver
parameter:

```python
class Task[**P](Protocol):
    __call__: Callable[P, int]

def check(task: Task[[str]]) -> int:
    return task("value")
```

Once `P` is specialized to `(str,)`, converting the member to a
function-like callable would bind away the `str` parameter and expose
`() -> int` instead. We now record that a dunder callable was declared
with a bare `ParamSpec` before generic specialization and preserve it as
a regular callable during instance and protocol binding. Parameterless
callable dunders likewise remain regular.

All other concrete and gradual `Callable` dunders retain the existing
descriptor heuristic. In particular, we deliberately do not inspect
whether the first parameter annotation can accept the owning instance.

Closes astral-sh/ty#3957.
## Summary

This follows up on #26414 by documenting that the standard library's
`StrEnum` and `IntEnum` types are subclasses of `str` and `int`,
respectively, and are therefore affected by unsafe builtin literal
narrowing.

The new `StrEnum` example shows how a call can be statically typed as
returning `Literal["a"] | None` while returning a `Choice` enum member
at runtime. The generated configuration documentation and schema now
include the clarification.
## Summary

In #26696, we stopped treating `Callable[P, R]`-typed dunder members as
function-like descriptors because binding the first parameter after
specializing `P` would incorrectly remove a real argument. However,
implicit dunder lookup uses a no-instance-fallback path that returned
before the normal eager `Self` binding step. As a result, a ParamSpec
dunder that returned `Self` preserved the declaring class's `Self` type
instead of resolving it to the concrete receiver:

```python
class Base[**P]:
    __getitem__: Callable[P, Self]

class Child(Base[[int]]): ...

def check(value: Child) -> None:
    reveal_type(value[0])  # Self@Base, expected Child
```

We now apply the existing receiver-aware `Self` mapping to the result of
no-instance-fallback descriptor lookup. Function-like callables continue
to defer `Self` binding to their signature-binding path, while regular
ParamSpec callables resolve `Self` without dropping any parameters.

The regression coverage verifies that explicit `value.__getitem__(0)`
and implicit `value[0]` access both return `Child`.
…ts (`RUF106`) (#26682)

Summary
--

This is the second of our migration rules for moving from `noqa`
comments to `ruff:ignore` comments
with human-readable names. `RUF106` performs the second part of this
transformation, converting
`ruff:ignore` comments that use rule codes to `ruff:ignore` comments
with human-readable names.

The rule skips unknown codes, external and otherwise. Unlike `RUF105` I
don't think there's anything
sensible we could do with those here.

Test Plan
--

New mdtests and a CLI test for the combination of `RUF105` and `RUF106`.

I did a local grep of our ecosystem projects and didn't find any
`ruff:ignore`-style suppressions, so I don't expect any ecosystem hits
for this.
… observations from popular projects (#26673)

<!--
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?
- Does this PR follow our AI policy
(https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)?
-->

## Summary

This changes how we resolve an ambiguity in extracting parameter
documentation from Google-style docstrings that being with a
reStructuredText literal block. Here is an [example of such a docstring
that occurs in the
wild](https://github.com/pytorch/pytorch/blob/e3f5bf0b18585511e6cd7d7a574ebf82f465e5ae/torch/_native/instrumentation.py#L360-L384)
(paraphrased below):

```py
def instrument_triton_kernel(op: str):
    """Instrument a Triton kernel, with the instrumentation decorator above the jit::

        @instrument_triton_kernel("aten::bmm")
        @triton.jit
        def kernel(...): ...

    The kernel compiles lazily and caches variants on the kernel object.

    Args:
        op: Operator symbol being compiled.
    """
```

Previously, the parser used the raw indentation of the docstring and
incorrectly treated the entire remainder of the docstring as part of the
literal block. As a result, we did not recognize any parameter
documentation from the above block. After the fix, we instead parse the
PEP-257-normalized docstring, and thus correct recognize the
documentation for `op`.

The tradeoff is that if the entire remainder of a docstring beginning
with a literal block were intended as literal content, and that content
looked like a Google-style param section, we would instead interpret it
as real parameter documentation:

```py
def example():
    """Example::

        Args:
            value: Literal text, not parameter documentation.
    """
```

Whereas the former example was actually observed in the wild in quick
survey of popular Python repos, the latter was not.

As an added benefit, this simplifies our Google-style docstring parsing
and aligns it with our reStructuredText parsing, thereby streamlining
follow-up [document-model](astral-sh/ruff#26670)
and [Markdown-rendering](astral-sh/ruff#26599)
changes.

## Test Plan

Please see included tests.
<!-- How was it tested? -->
…(#26714)

Use the `remove_inferable_typevar_artifacts_from_solution` helper from
astral-sh/ruff#26099, instead of the current
naive union filtering during collection inference, to eliminate
unnecessary inferable type variables that prevent the constraint solver
from determining bidirectional inference type mappings. I also
reorganized the relevant tests slightly.

Resolves
astral-sh/ty#3956 (comment).
Note that the diagnostics there were a little misleading because the
callable types and concrete function types are rendered identically if
they have the same signature, despite being distinct types.
## Summary

Check only the current function definition when validating `TypeIs` and
`TypeGuard` definitions. `check_type_guard_definition` runs for every
`FunctionDef`, which includes each individual overload, so it shouldn't
iterate previous overloads each time also.

Fixes astral-sh/ty#3968

## Validation

Added mdtests.
## Summary

Prior to this change, we could incorrectly reject a call to a stored
bound method obtained from a class-based protocol. Member lookup has
already specialized the method for its receiver, but calling through a
callable union reconstructed the original signature and prepended the
receiver as a synthetic argument. Generic call inference then
structurally checked the inferred `self` annotation again:

```python
from collections.abc import Iterator
from typing import Any

class PeekIterator(Iterator[Any]):
    def __init__(self, iterator: Iterator[Any]) -> None:
        self._next = iterator.__next__

    def __next__(self) -> Any:
        # Before: Expected `Self@__next__`, found `Iterator[Any]`
        return self._next()

    def use_fallback(self) -> None:
        self._next = lambda: None
```

The receiver is already bound, so `Iterator.__next__` genuinely takes no
call-site arguments and should not be checked against `Self` again. We
now consume an implicit positional receiver before call inference, apply
the `typing.Self` substitution to the remaining signature, and refresh
its return type. This removes the false positive and avoids re-entering
recursive protocol interfaces during binding, which could otherwise
cause overloaded recursive members to grow the deferred constraint graph
combinatorially.

This also corrects constructor diagnostics for class-based protocols.
Previously we counted the implicit receiver in both the expected and
supplied arity:

```python
from collections.abc import Hashable

def construct(value: Hashable) -> None:
    # Before: expected 1, got 4
    # After:  expected 0, got 3
    type(value)(1970, 1, 1)
```

Because binding removes the leading parameter, we preserve its
source-parameter offset so invalid-argument and missing-argument
diagnostics still point at the correct declaration. Explicit, variadic,
keyword-only, and other nonstandard receivers retain the existing
call-inference path; the protocol relation itself is unchanged.

The independent recursive traversal in `redundant-cast` checks is
handled separately in #26708.

Related: <astral-sh/ty#3954>
## Summary

Prior to this change, the common-protocol-constraint optimization for
unions of `TypedDict`s did not apply after an `isinstance(value, dict)`
check. Narrowing represents each surviving arm as an intersection with
`Top[dict[Unknown, Unknown]]`, so a call like `dict(value)` fell back to
combining equivalent generic protocol constraints independently across
every union member:

```python
from typing import Literal, TypedDict

class A(TypedDict):
    tag: Literal["a"]

class B(TypedDict):
    tag: Literal["b"]

def copy(value: A | B | str) -> None:
    if isinstance(value, dict):
        reveal_type(dict(value))  # dict[str, object]
```

With a sufficiently wide union, that fallback exhibited pathological
runtime. On the issue's 21-arm reproduction, the pre-fix build did not
complete within ten seconds; the optimized path completes in under 0.2
seconds.

We now recognize intersections containing a positive `TypedDict` as
`TypedDict` alternatives, while retaining the complete intersection when
comparing its protocol constraints with `Mapping[str, object]`.

Closes astral-sh/ty#3974.

Co-authored-by: Nate Bracy <nate@bracy.dev>
renovate Bot and others added 26 commits July 22, 2026 08:03
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Micha Reiser <micha@reiser.io>
## Summary

Pull in
[astral-sh/ecosystem-analyzer#130](astral-sh/ecosystem-analyzer#130),
which updates its mypy-primer pin to include
[hauntsaninja/mypy_primer#256](hauntsaninja/mypy_primer#256).

`svcs` moved its typing tests from `tests/typing` to `typing_tests` in
[hynek/svcs#177](hynek/svcs#177). The previous
analyzer pin still used a mypy-primer entry with `paths=["src",
"tests/typing"]`, so ty reported the missing path as an I/O error and
ecosystem-analyzer recorded a persistent exit code 2 on both sides of PR
comparisons.
## Summary

Prior to this change, every system-file lookup normalized and allocated
an absolute path before checking the file cache, even when the supplied
path was already absolute and cached. Module resolution repeatedly takes
this path while discovering packages and directory listings.

We now check the cache with the supplied absolute path first in both
`Files::system` and `Files::try_system`. Camino's path equality and
hashing already normalize repeated separators and `.` components; paths
containing `..` or relative paths safely miss and continue through the
existing normalization path.

---------

Co-authored-by: Charlie Marsh <charliemarsh@openai.com>
## Summary

Prior to this change, equality and membership narrowing preserved
otherwise-incompatible union members because a subclass could override
`__eq__`. By default, we now assume that subclasses of a non-final class
do not override the inherited comparison method:

```py
class Foo: ...

def f(value: Foo | None, other: Foo) -> None:
    reveal_type(None == other)  # Literal[False]

    if value == other:
        reveal_type(value)  # Foo

    if value in [other]:
        reveal_type(value)  # Foo
```

The behavior is intentionally unsound. Users who want conservative
equality behavior can enable the single `strict-equality-narrowing`
setting, which also preserves broad builtin types instead of narrowing
them to literals:

```toml
[tool.ty.analysis]
strict-equality-narrowing = true
```

For compatibility, `strict-literal-narrowing` remains supported as an
alias for `strict-equality-narrowing`.

Closes astral-sh/ty#3419.
As a prereq for implementing [scoped
quantifiers](https://gist.github.com/dcreager/679132607be4c7cfb08ddc8ad1076982),
this PR updates all of our BDD path walking code to use a new visitor
trait. Instead of each higher level method implementing its own copy of
the path walking code, there is now a `PathAssignments::visit` method
that handles the path walking. It takes in an impl of the new
`PathVisitor` trait, which lets each higher level method define what
overall result is being produced, and to provide opportunites to abort
the walk once we've found an answer.

As an interesting wrinkle, the `is_always_satisfied` method needs its
own variant, `visit_negated`, which visits the negation of a BDD.
Importantly, it constructs that negation lazily, so that we can still
return early. (An early draft of this PR just called `visit` on
`node.negated`, which quickly exposed this perf regression.)
…#27100)

## Summary

Prior to this change, narrowing an `Unknown | None` match subject across
a sequence of value patterns accumulated constraints of the form
`(Unknown & ~Literal[EventType.A00]) | None`. Intersecting all of those
constraints before normalization distributed the shared `None` arm
across every combination, yielding exponential time and memory.

We now normalize each intersection as it is accumulated, allowing
equivalent union arms to collapse before the next constraint is applied.
A 24-arm `StrEnum` match that previously exhausted memory now finishes
in about 40 ms using 38 MiB, and an mdtest covers the original nullable
dynamic-subject shape.

Closes astral-sh/ty#4068.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
## Summary

Fix disjointness checking between `type[T]` and `TypeForm[S]` by
comparing the represented instance types. PEP 747 defines `type[T]` as a
subtype of `TypeForm[T]`, but the generic-subclass fallback could
incorrectly report the pair as disjoint; intersection reduction then
became order-dependent and generic `TypeIs[type[T]]` narrowing produced
`Never`.

## Test plan

Added mdtest coverage for subtype and disjointness invariants, both
intersection orders, overlapping and genuinely disjoint represented
types, and generic `TypeIs` narrowing from `TypeForm[T]` to `type[T]`.
## Summary

Fixes some issues with generic stringified PEP 613 type aliases:

```py
from typing import TypeAlias, TypeVar

T = TypeVar("T")

ListAlias: TypeAlias = "list[T]"

def takes_list(value: ListAlias) -> None:
    reveal_type(value)  # `list[Unknown]`, previously `list[T]`

takes_list([1])  # accepted, previously a cryptic error
```

closes astral-sh/ty#4064

## Test Plan

Added regression test

## Ecosystem

Codex's summary: "favorable and fully explained by the intended fix"
Move Ruff's macOS test and binary-build jobs to the
`namespace-profile-macos-15` runner already used by uv. Keep the
existing Rust-cache behavior and GitHub-hosted fallbacks for forks, and
leave the infrequent typeshed-sync job on GitHub-hosted macOS since it
requires `contents: write`.

Three sequential Namespace samples were compared with the three most
recent `main` test runs on `depot-macos-15` and the three most recent
production releases on `macos-15`:

| Job / metric | Baseline mean, n=3 | Namespace mean, n=3 | Improvement
|
| --- | ---: | ---: | ---: |
| macOS tests, whole job | 5:26 | 2:31 | 54% faster |
| macOS tests, test step | 4:34 | 2:13 | 51% faster |
| x86_64 release, whole job | 9:54 | 3:05 | 69% faster |
| x86_64 release, build step | 9:13 | 2:44 | 70% faster |
| aarch64 release, whole job | 9:13 | 3:14 | 65% faster |
| aarch64 release, build step | 8:52 | 2:54 | 67% faster |

All nine Namespace jobs were admitted on distinct runners within 19-29
seconds, with all three macOS jobs running concurrently in each sample:

| Namespace job | Queue median (range), n=3 |
| --- | ---: |
| macOS tests | 0:21 (0:20-0:29) |
| x86_64 release | 0:22 (0:22-0:22) |
| aarch64 release | 0:21 (0:19-0:22) |

The Namespace test runs were cold Rust-cache misses: the image's
preinstalled stable toolchain differs from Depot's and produces a
different `Swatinem/rust-cache` key. `main` will populate the new key
after the transition. This is an effective configured-runner comparison,
not an apples-to-apples provider comparison: the Namespace profile runs
on M4 Pro with 12 vCPU and 28 GB RAM, `depot-macos-15` runs on M2 with 8
CPU and 24 GB RAM, and `macos-15` uses a smaller GitHub-hosted runner.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
…` models (#27098)

## Summary

Copy the underscore-prefixed parameters like `_secrets_dir` from
`BaseSettings.__init__` when synthesizing constructors Pydantic models
derived from for `BaseSettings`. This allows us to type check valid code
like this without any errors:

```py
class Settings(BaseSettings):
    host: str
    port: int

Settings(_secrets_dir="./secrets")
```

closes astral-sh/ty#4067

## Test plan

New Markdown test
Addresses #15584 for D400. Adds fix safety documentation to the `D400`
rule.

Co-authored-by: Mahadev Annabhimoju <219508079+Joosboy@users.noreply.github.com>
…27091)

## Summary

For lax-mode models, we try to model Pydantic's coercion behavior by
promoting some types like `int` to `LaxInt = int | str | ...`. When a
field referred to a non-builtin type, we previously widened the
corresponding constructor parameter type to `Any`, since we didn't know
what that field would accept. However, there is one case where we can be
more strict: if the field refers to another Pydantic model, we can widen
`Child` to `Child | Mapping[str, Any]`, since that represents all
possible values that Pydantic can accept for a field like this:

```py
class Child(BaseModel):
    value: int

class Stranger(BaseModel): ...

class Parent(BaseModel):
    child: Child

Parent(child=Child(value=1))

Parent(child=Stranger())  # previously accepted, now an error
```

Adding support for this also required proper modeling of a few other
behaviors, or otherwise this would have caused false positives:
- Detecting the [`from_attributes`
setting](https://pydantic.dev/docs/validation/dev/api/pydantic/config/#pydantic.config.ConfigDict.from_attributes).
Models that use this setting still use `Any` as the input type.
- Detecting ["before"
validators](https://pydantic.dev/docs/validation/latest/concepts/validators/#field-before-validator)
which can change the input type. For now, affected fields also still use
`Any` as the input type.
- Handling generic models. `Box[int]` can accept a `Box[str]`, so we
currently use the `Unknown` specialization `Box[Unknown]` for the input
type.

closes astral-sh/ty#3947
closes astral-sh/ty#3510

## Test Plan

- New Markdown tests
- validated all new test assertions against Pydantic runtime behavior

## Ecosystem

The new hits on Prefect are all expected.
## Summary

A small refactor for `invalid_index_type` rule code, no functional
changes introduced.

The code has an `is_literal()` check that isn't actually needed - even
when we matched literal, we still then match `Int | Bool` or `Int | Bool
| None` explicitly, so we might as well drop `is_literal` completely.
Other literals besides `Int | Bool | None` don't carry any meaning here,
they're just other unexpected types from `CheckableExprType`, just as
`List`, `Set`, etc.


https://github.com/astral-sh/ruff/blob/4fc9653b505dd7784f56afa574ccb11b602d675e/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs#L239-L242


https://github.com/astral-sh/ruff/blob/4fc9653b505dd7784f56afa574ccb11b602d675e/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs#L91-L104

Just a small leftover from the refactor in
https://github.com/astral-sh/ruff/pull/8064/changes#diff-0a227bd56e5ecade9d52a2a29e98b681d03951897372742e42a55800481cf97f,
when `is_literal` was introduced to replace the old `Expr::Constant`.

## Test Plan

No new tests needed, existing tests pass.
## Summary

Use ty's normal intersection-member lookup when inferring class-pattern
captures instead of widening attributes from overlapping nominal classes
into a union. This lets a class pattern over a non-final union correctly
narrow a captured attribute.

## Test plan

- Covers a non-final `A | B` class-pattern capture where both classes
define `params`, verifying that the `A` arm narrows both the subject and
captured value and permits access to the `A`-specific attribute.
- Covers incompatible overlapping attributes, compatible overlapping
attributes with a reachable nested `str()` pattern, and generic
overlapping attributes that retain `Unknown`.
- Ecosystem changes are all desirable (removal of false positives from
overly-broad types).
We currently infer interpolated string literals as `LiteralString`, but
do not allow them to be later promoted to `str` if used in invariant
position. Interpolated string literals should be promotable like any
other string literal form:

```py
from typing import Literal

def _(source: Literal["foo", "bar"]):
    x = f"hello:{source}"
    reveal_type(x)  # revealed: LiteralString
    reveal_type([x])  # revealed: list[str]
```
…`) (#26972)

Closes #14353. Clarifies that `os.environ` converts environment
variables to uppercase on Windows, which can lead to cross-platform bugs
if code assumes variables preserve their original case.

---------

Co-authored-by: Mahadev Annabhimoju <219508079+Joosboy@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
merges `upstream/main` at 47d7184 into the fork. 45 conflicts resolved;
typeshed regenerated from scratch; full suite green (10355 tests), ci insta
gate clean, clippy and prek clean.

notable ripples from upstream refactors:

- salsa `Update` derive renamed to `SalsaValue`; tracked functions and
  interned fields now return references unless marked `returns(copy)`
- `Type::to_instance` returns an `InstanceProjection`; fork call sites that
  only need the type use `to_instance_approximation`
- the parser collects into `expr_scratch`/`stmt_scratch` buffers, so the
  fork's subscript and simple-statement hooks push there instead. the
  `context`-parameter validation had to move after the buffers are drained,
  or it inspected empty parameter lists and never fired
- upstream's two-tier expression cache replaces the fork's
  `(expression, tcx) -> Type` map; the fluid fields are threaded through
  `FullExpressionCacheEntry`
- `check_file` became a salsa query; `apply_overrides` became
  `apply_override_options`
- `ProtocolMemberWrite` wraps the write capability, so the reified-member
  lookup goes through `domain()`
- pydantic field metadata moved behind `pydantic::field_metadata`, which
  gains the fork's `frozen` flag

fluid specializations needed a real fix: upstream now specializes a generic
call's parameter type from the arguments themselves, so every generic call
recorded a self-derived "adoption" and locked the specialization. argument
contexts built that way are marked `inferred_from_argument` and no longer
count as external observers.

the typeshed patch pipeline is reproducible from scratch again:
`container_overlapping` moved to the post-conversion pass (it matches the
converted `Element`/`Key` names and collided with `mapping` in pass 1), and
the sync script now runs the patches to a fixed point, since
`private type _X` only becomes visible after `TypeAliasStatements` writes
the `type _X = …` statement.

fork divergences newly exposed by upstream tests are annotated in place:
`isinstance(x, (List,))` through the recursive pep 695 `_ClassInfo` alias,
covariant `Mapping` keys not widening to the declared key type, `in` on a
provably-disjoint container erroring under `Overlapping`, and a `reveal_type`
in the arguments of a fluid-receiver call reporting an intermediate pass.
@KotlinIsland
KotlinIsland merged commit 5fb58fc into main Jul 23, 2026
85 checks passed
@KotlinIsland
KotlinIsland temporarily deployed to release-playground July 23, 2026 12:30 — with GitHub Actions Inactive
@KotlinIsland
KotlinIsland deleted the merge-upstream branch July 23, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.