Compare commits

..

5 Commits

Author SHA1 Message Date
Charlie Marsh
1099c76fde Recommend running prek against upstream 2026-01-11 13:42:46 -05:00
Charlie Marsh
28cde5d7a6 Set priority for prek to enable concurrent execution 2026-01-11 13:33:20 -05:00
Charlie Marsh
2487233716 Use prek in docuemntation 2026-01-11 13:30:28 -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
15 changed files with 419 additions and 292 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

@@ -21,15 +21,62 @@ 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.40.0
hooks:
- id: typos
priority: 0
- 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
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 +91,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 +114,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,48 +130,7 @@ 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`.
@@ -131,8 +152,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 --from-ref @{upstream}` 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

@@ -57,8 +57,8 @@ You can optionally install pre-commit hooks to automatically run the validation
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

View File

@@ -38,8 +38,8 @@ You can optionally install pre-commit hooks to automatically run the validation
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

View File

@@ -406,6 +406,9 @@ reveal_type(Child.create()) # revealed: Child
## Attributes
TODO: The use of `Self` to annotate the `next_node` attribute should be
[modeled as a property][self attribute], using `Self` in its parameter and return type.
```py
from typing import Self
@@ -415,36 +418,13 @@ class LinkedList:
def next(self: Self) -> Self:
reveal_type(self.value) # revealed: int
# TODO: no error
# error: [invalid-return-type]
return self.next_node
reveal_type(LinkedList().next()) # revealed: LinkedList
```
Dataclass fields can also use `Self` in their annotations:
```py
from dataclasses import dataclass
from typing import Optional, Self
@dataclass
class Node:
parent: Optional[Self] = None
Node(Node())
```
Attributes annotated with `Self` can be assigned on instances:
```py
from typing import Optional, Self
class MyClass:
field: Optional[Self] = None
def _(c: MyClass):
c.field = c
```
Attributes can also refer to a generic parameter:
```py
@@ -695,73 +675,4 @@ def _(c: CallableTypeOf[C().method]):
reveal_type(c) # revealed: (...) -> None
```
## Bound methods from internal data structures stored as instance attributes
This tests the pattern where a class stores bound methods from internal data structures (like
`deque` or `dict`) as instance attributes for performance. When these bound methods are later
accessed and called through `self`, the `Self` type binding should not interfere with their
signatures.
This is a regression test for false positives found in ecosystem projects like jinja's `LRUCache`
and beartype's `CacheUnboundedStrong`.
```py
from collections import deque
from typing import Any
class LRUCache:
"""A simple LRU cache that stores bound methods from an internal deque."""
def __init__(self, capacity: int) -> None:
self.capacity = capacity
self._mapping: dict[Any, Any] = {}
self._queue: deque[Any] = deque()
self._postinit()
def _postinit(self) -> None:
# Store bound methods from the internal deque for faster attribute lookup
self._popleft = self._queue.popleft
self._pop = self._queue.pop
self._remove = self._queue.remove
self._append = self._queue.append
def __getitem__(self, key: Any) -> Any:
# These should not produce errors - the bound methods have signatures
# from deque, not involving Self
self._remove(key)
self._append(key)
return self._mapping[key]
def __setitem__(self, key: Any, value: Any) -> None:
self._remove(key)
if len(self._queue) >= self.capacity:
self._popleft()
self._append(key)
self._mapping[key] = value
def __delitem__(self, key: Any) -> None:
self._remove(key)
del self._mapping[key]
```
Similarly for dict-based patterns:
```py
from typing import Hashable
class CacheMap:
"""A cache that stores bound methods from an internal dict."""
def __init__(self) -> None:
self._key_to_value: dict[Hashable, object] = {}
self._key_to_value_get = self._key_to_value.get
self._key_to_value_set = self._key_to_value.__setitem__
def cache_or_get_cached_value(self, key: Hashable, value: object) -> object:
# This should not produce errors - we're using dict's get/setitem methods
cached_value = self._key_to_value_get(key)
if cached_value is not None:
return cached_value
self._key_to_value_set(key, value)
return value
```
[self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations

View File

@@ -1814,27 +1814,6 @@ def _(ns: argparse.Namespace):
reveal_type(ns.whatever) # revealed: Any
```
### `__getattr__` with `Self` type
`__getattr__` should also work when the receiver is typed as `Self`:
```toml
[environment]
python-version = "3.11"
```
```py
from typing import Self
class CustomGetAttr:
def __getattr__(self, name: str) -> int:
return 1
def method(self) -> Self:
reveal_type(self.whatever) # revealed: int
return self
```
## Classes with custom `__getattribute__` methods
If a type provides a custom `__getattribute__`, we use its return type as the type for unknown

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.
@@ -245,6 +399,79 @@ 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

View File

@@ -21,5 +21,6 @@ X.aaaaooooooo # error: [unresolved-attribute]
Foo.X.startswith # error: [unresolved-attribute]
Foo.Bar().y.startswith # error: [unresolved-attribute]
Foo().b.a
# TODO: false positive (just testing the diagnostic in the meantime)
Foo().b.a # error: [unresolved-attribute]
```

View File

@@ -31,7 +31,8 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/special_form
16 | Foo.X.startswith # error: [unresolved-attribute]
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
18 |
19 | Foo().b.a
19 | # TODO: false positive (just testing the diagnostic in the meantime)
20 | Foo().b.a # error: [unresolved-attribute]
```
# Diagnostics
@@ -94,9 +95,21 @@ error[unresolved-attribute]: Special form `typing.LiteralString` has no attribut
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^^^^^^^^
18 |
19 | Foo().b.a
19 | # TODO: false positive (just testing the diagnostic in the meantime)
|
help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString`
info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Special form `typing.Self` has no attribute `a`
--> src/mdtest_snippet.py:20:1
|
19 | # TODO: false positive (just testing the diagnostic in the meantime)
20 | Foo().b.a # error: [unresolved-attribute]
| ^^^^^^^^^
|
info: rule `unresolved-attribute` is enabled by default
```

View File

@@ -2530,34 +2530,15 @@ impl<'db> Type<'db> {
}
Type::TypeVar(bound_typevar) => {
let member = match bound_typevar.typevar(db).bound_or_constraints(db) {
match bound_typevar.typevar(db).bound_or_constraints(db) {
None => Type::object().instance_member(db, name),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
if bound_typevar.typevar(db).is_self(db) {
if let Type::NominalInstance(instance) = bound {
instance.class(db).instance_member(db, name)
} else {
bound.instance_member(db, name)
}
} else {
bound.instance_member(db, name)
}
bound.instance_member(db, name)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.map_with_boundness_and_qualifiers(db, |constraint| {
constraint.instance_member(db, name)
}),
};
if bound_typevar.typevar(db).is_self(db) {
let self_mapping = TypeMapping::BindSelf {
self_type: Type::TypeVar(*bound_typevar),
self_typevar_identity: Some(bound_typevar.typevar(db).identity(db)),
};
member.map_type(|ty| {
ty.apply_type_mapping(db, &self_mapping, TypeContext::default())
})
} else {
member
}
}
@@ -3296,20 +3277,11 @@ impl<'db> Type<'db> {
| Type::TypedDict(_) => {
let fallback = self.instance_member(db, name_str);
// `Self` type variables use `InstanceFallbackShadowsNonDataDescriptor::Yes`
// because instance attributes should shadow non-data descriptors on the class.
let instance_fallback_shadows = if matches!(self, Type::TypeVar(tv) if tv.typevar(db).is_self(db))
{
InstanceFallbackShadowsNonDataDescriptor::Yes
} else {
InstanceFallbackShadowsNonDataDescriptor::No
};
let result = self.invoke_descriptor_protocol(
db,
name_str,
fallback,
instance_fallback_shadows,
InstanceFallbackShadowsNonDataDescriptor::No,
policy,
);
@@ -6895,8 +6867,7 @@ pub enum TypeMapping<'a, 'db> {
/// Binds any `typing.Self` typevar with a particular `self` class.
BindSelf {
self_type: Type<'db>,
/// If `Some`, only bind `Self` typevars that have this identity (i.e., from the same class).
self_typevar_identity: Option<TypeVarIdentity<'db>>,
binding_context: Option<BindingContext<'db>>,
},
/// Replaces occurrences of `typing.Self` with a new `Self` type variable with the given upper bound.
ReplaceSelf { new_upper_bound: Type<'db> },
@@ -6926,9 +6897,8 @@ impl<'db> TypeMapping<'_, 'db> {
| TypeMapping::ReplaceParameterDefaults
| TypeMapping::EagerExpansion => context,
TypeMapping::BindSelf {
self_typevar_identity,
..
} => context.remove_self(db, *self_typevar_identity),
binding_context, ..
} => context.remove_self(db, *binding_context),
TypeMapping::ReplaceSelf { new_upper_bound } => GenericContext::from_typevar_instances(
db,
context.variables(db).map(|typevar| {
@@ -8563,11 +8533,10 @@ impl<'db> BoundTypeVarInstance<'db> {
}
TypeMapping::BindSelf {
self_type,
self_typevar_identity,
binding_context,
} => {
if self.typevar(db).is_self(db)
&& self_typevar_identity
.is_none_or(|identity| self.typevar(db).identity(db) == identity)
&& binding_context.is_none_or(|context| self.binding_context(db) == context)
{
*self_type
} else {

View File

@@ -1586,17 +1586,44 @@ impl<'db> ClassLiteral<'db> {
}
/// Returns `true` if any class in this class's MRO (excluding `object`) defines an ordering
/// method (`__lt__`, `__le__`, `__gt__`, `__ge__`). Used by `@total_ordering` validation and
/// for synthesizing comparison methods.
/// method (`__lt__`, `__le__`, `__gt__`, `__ge__`). Used by `@total_ordering` validation.
pub(super) fn has_ordering_method_in_mro(
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
) -> bool {
self.iter_mro(db, specialization)
.filter_map(ClassBase::into_class)
.filter(|class| !class.class_literal(db).0.is_known(db, KnownClass::Object))
.any(|class| class.class_literal(db).0.has_own_ordering_method(db))
self.total_ordering_root_method(db, specialization)
.is_some()
}
/// Returns the type of the ordering method used by `@total_ordering`, if any.
///
/// Following `functools.total_ordering` precedence, we prefer `__lt__` > `__le__` > `__gt__` >
/// `__ge__`, regardless of whether the method is defined locally or inherited.
pub(super) fn total_ordering_root_method(
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
) -> Option<Type<'db>> {
const ORDERING_METHODS: [&str; 4] = ["__lt__", "__le__", "__gt__", "__ge__"];
for name in ORDERING_METHODS {
for base in self.iter_mro(db, specialization) {
let Some(base_class) = base.into_class() else {
continue;
};
let (base_literal, base_specialization) = base_class.class_literal(db);
if base_literal.is_known(db, KnownClass::Object) {
continue;
}
let member = class_member(db, base_literal.body_scope(db), name);
if let Some(ty) = member.ignore_possibly_undefined() {
return Some(ty.apply_optional_specialization(db, base_specialization));
}
}
}
None
}
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
@@ -2448,26 +2475,44 @@ impl<'db> ClassLiteral<'db> {
// ordering method. The decorator requires at least one of __lt__,
// __le__, __gt__, or __ge__ to be defined (either in this class or
// inherited from a superclass, excluding `object`).
if self.total_ordering(db) && matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") {
if self.has_ordering_method_in_mro(db, specialization) {
let instance_ty =
Type::instance(db, self.apply_optional_specialization(db, specialization));
let signature = Signature::new(
Parameters::new(
db,
[
Parameter::positional_or_keyword(Name::new_static("self"))
.with_annotated_type(instance_ty),
Parameter::positional_or_keyword(Name::new_static("other"))
.with_annotated_type(instance_ty),
],
),
KnownClass::Bool.to_instance(db),
//
// Only synthesize methods that are not already defined in the MRO.
if self.total_ordering(db)
&& matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__")
&& !self
.iter_mro(db, specialization)
.filter_map(ClassBase::into_class)
.filter(|class| !class.class_literal(db).0.is_known(db, KnownClass::Object))
.any(|class| {
class_member(db, class.class_literal(db).0.body_scope(db), name)
.ignore_possibly_undefined()
.is_some()
})
&& self.has_ordering_method_in_mro(db, specialization)
&& let Some(root_method_ty) = self.total_ordering_root_method(db, specialization)
&& let Some(callables) = root_method_ty.try_upcast_to_callable(db)
{
let bool_ty = KnownClass::Bool.to_instance(db);
let synthesized_callables = callables.map(|callable| {
let signatures = CallableSignature::from_overloads(
callable.signatures(db).iter().map(|signature| {
// The generated methods return a union of the root method's return type
// and `bool`. This is because `@total_ordering` synthesizes methods like:
// def __gt__(self, other): return not (self == other or self < other)
// If `__lt__` returns `int`, then `__gt__` could return `int | bool`.
let return_ty =
UnionType::from_elements(db, [signature.return_ty, bool_ty]);
Signature::new_generic(
signature.generic_context,
signature.parameters().clone(),
return_ty,
)
}),
);
CallableType::new(db, signatures, CallableTypeKind::FunctionLike)
});
return Some(Type::function_like_callable(db, signature));
}
return Some(synthesized_callables.into_type(db));
}
let field_policy = CodeGeneratorKind::from_class(db, self, specialization)?;

View File

@@ -23,8 +23,8 @@ use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::variance::VarianceInferable;
use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard};
use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, ClassLiteral,
FindLegacyTypeVarsVisitor, IntersectionType, KnownClass, KnownInstanceType,
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance,
ClassLiteral, FindLegacyTypeVarsVisitor, IntersectionType, KnownClass, KnownInstanceType,
MaterializationKind, NormalizedVisitor, Type, TypeContext, TypeMapping,
TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
UnionType, declaration_type, walk_type_var_bounds,
@@ -66,26 +66,12 @@ pub(crate) fn bind_typevar<'db>(
) -> Option<BoundTypeVarInstance<'db>> {
// typing.Self is treated like a legacy typevar, but doesn't follow the same scoping rules. It is always bound to the outermost method in the containing class.
if matches!(typevar.kind(db), TypeVarKind::TypingSelf) {
let binding_function =
typevar_binding_context.filter(|definition| definition.kind(db).is_function_def());
let mut function_in_class = None;
for (_, scope) in index.ancestor_scopes(containing_scope) {
match scope.node() {
NodeWithScopeKind::Function(function) => {
function_in_class = Some(function);
}
NodeWithScopeKind::Class(class) => {
if let Some(function) = function_in_class {
let definition = index.expect_single_definition(function);
return Some(typevar.with_binding_context(db, definition));
}
if let Some(binding_context) = binding_function {
return Some(typevar.with_binding_context(db, binding_context));
}
let definition = index.expect_single_definition(class);
for ((_, inner), (_, outer)) in index.ancestor_scopes(containing_scope).tuple_windows() {
if outer.kind().is_class() {
if let NodeWithScopeKind::Function(function) = inner.node() {
let definition = index.expect_single_definition(function);
return Some(typevar.with_binding_context(db, definition));
}
_ => {}
}
}
}
@@ -295,14 +281,15 @@ impl<'db> GenericContext<'db> {
pub(crate) fn remove_self(
self,
db: &'db dyn Db,
self_typevar_identity: Option<TypeVarIdentity<'db>>,
binding_context: Option<BindingContext<'db>>,
) -> Self {
Self::from_typevar_instances(
db,
self.variables(db).filter(|bound_typevar| {
!(bound_typevar.typevar(db).is_self(db)
&& self_typevar_identity
.is_none_or(|identity| bound_typevar.typevar(db).identity(db) == identity))
&& binding_context.is_none_or(|binding_context| {
bound_typevar.binding_context(db) == binding_context
}))
}),
)
}

View File

@@ -112,10 +112,10 @@ use crate::types::{
LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType,
SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers,
TypeContext, TypeMapping, TypeQualifiers, TypeVarBoundOrConstraints,
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
UnionTypeInstance, binding_type, infer_scope_types, todo_type,
TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types,
todo_type,
};
use crate::types::{CallableTypes, overrides};
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
@@ -4530,12 +4530,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let db = self.db();
let mut first_tcx = None;
let self_mapping = TypeMapping::BindSelf {
self_type: object_ty,
self_typevar_identity: None,
};
let bind_self =
|ty: Type<'db>| ty.apply_type_mapping(db, &self_mapping, TypeContext::default());
// A wrapper over `infer_value_ty` that allows inferring the value type multiple times
// during attribute resolution.
@@ -4883,7 +4877,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}),
qualifiers,
} => {
let meta_attr_ty = bind_self(meta_attr_ty);
if invalid_assignment_to_final(self, qualifiers) {
return false;
}
@@ -4935,7 +4928,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
} =
object_ty.instance_member(db, attribute)
{
let instance_attr_ty = bind_self(instance_attr_ty);
let value_ty =
infer_value_ty(self, TypeContext::new(Some(instance_attr_ty)));
if invalid_assignment_to_final(self, qualifiers) {
@@ -4981,7 +4973,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
qualifiers,
} = object_ty.instance_member(db, attribute)
{
let instance_attr_ty = bind_self(instance_attr_ty);
let value_ty =
infer_value_ty(self, TypeContext::new(Some(instance_attr_ty)));
if invalid_assignment_to_final(self, qualifiers) {

View File

@@ -25,7 +25,7 @@ use crate::types::relation::{
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation,
};
use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, CallableTypeKind,
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, CallableTypeKind,
FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, NormalizedVisitor,
ParamSpecAttrKind, TypeContext, TypeMapping, VarianceInferable, todo_type,
};
@@ -893,21 +893,13 @@ impl<'db> Signature<'db> {
parameters.next();
}
// Find the Self typevar from this signature's generic context, if any.
// We only want to bind Self typevars that belong to this signature, not
// Self typevars from other classes that might appear in type parameters.
let self_typevar_identity = self.generic_context.and_then(|ctx| {
ctx.variables(db)
.find(|tv| tv.typevar(db).is_self(db))
.map(|tv| tv.typevar(db).identity(db))
});
let mut parameters = Parameters::new(db, parameters);
let mut return_ty = self.return_ty;
let binding_context = self.definition.map(BindingContext::Definition);
if let Some(self_type) = self_type {
let self_mapping = TypeMapping::BindSelf {
self_type,
self_typevar_identity,
binding_context,
};
parameters = parameters.apply_type_mapping_impl(
db,
@@ -920,7 +912,7 @@ impl<'db> Signature<'db> {
Self {
generic_context: self
.generic_context
.map(|generic_context| generic_context.remove_self(db, self_typevar_identity)),
.map(|generic_context| generic_context.remove_self(db, binding_context)),
definition: self.definition,
parameters,
return_ty,
@@ -928,15 +920,9 @@ impl<'db> Signature<'db> {
}
pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self {
// Find the Self typevar from this signature's generic context, if any.
let self_typevar_identity = self.generic_context.and_then(|ctx| {
ctx.variables(db)
.find(|tv| tv.typevar(db).is_self(db))
.map(|tv| tv.typevar(db).identity(db))
});
let self_mapping = TypeMapping::BindSelf {
self_type,
self_typevar_identity,
binding_context: self.definition.map(BindingContext::Definition),
};
let parameters = self.parameters.apply_type_mapping_impl(
db,