Compare commits

...

8 Commits

Author SHA1 Message Date
Charlie Marsh
9cd4499fe5 [ty] Add support for functional namedtuple 2026-01-12 08:14:29 -05:00
Charlie Marsh
169208a55e Support own instance members for type(...) classes 2026-01-11 23:02:10 -05:00
Charlie Marsh
dd8e67d666 Review feedback 2026-01-11 23:02:02 -05:00
Alex Waygood
4021c050d1 Simplify and correct various details in infer/builder.rs 2026-01-11 23:02:02 -05:00
Charlie Marsh
d126911948 Add support for dynamic type() classes 2026-01-11 23:02:02 -05:00
Charlie Marsh
09ff3e7056 Set priority for prek to enable concurrent execution (#22506)
## Summary

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

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

Closes https://github.com/astral-sh/ty/issues/2435.
2026-01-10 14:35:58 -05:00
44 changed files with 5212 additions and 997 deletions

View File

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

View File

@@ -1,4 +1,4 @@
# Configuration for the actionlint tool, which we run via pre-commit
# Configuration for the actionlint tool, which we run via prek
# to verify the correctness of the syntax in our GitHub Actions workflows.
self-hosted-runner:

View File

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

View File

@@ -769,8 +769,8 @@ jobs:
- name: "Remove wheels from cache"
run: rm -rf target/wheels
pre-commit:
name: "pre-commit"
prek:
name: "prek"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
timeout-minutes: 10
steps:
@@ -784,17 +784,17 @@ jobs:
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 24
- name: "Cache pre-commit"
- name: "Cache prek"
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: "Run pre-commit"
path: ~/.cache/prek
key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
- name: "Run prek"
run: |
echo '```console' > "$GITHUB_STEP_SUMMARY"
# Enable color output for pre-commit and remove it for the summary
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
SKIP=cargo-fmt uvx --python="${PYTHON_VERSION}" pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
# Enable color output for prek and remove it for the summary
# Use --hook-stage=manual to enable slower hooks that are skipped by default
SKIP=cargo-fmt uvx prek run --all-files --show-diff-on-failure --color always --hook-stage manual | \
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
exit_code="${PIPESTATUS[0]}"
echo '```' >> "$GITHUB_STEP_SUMMARY"

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`.
@@ -119,8 +140,8 @@ repos:
- id: actionlint
stages:
# This hook is disabled by default, since it's quite slow.
# To run all hooks *including* this hook, use `uvx pre-commit run -a --hook-stage=manual`.
# To run *just* this hook, use `uvx pre-commit run -a actionlint --hook-stage=manual`.
# To run all hooks *including* this hook, use `uvx prek run -a --hook-stage=manual`.
# To run *just* this hook, use `uvx prek run -a actionlint --hook-stage=manual`.
- manual
args:
- "-ignore=SC2129" # ignorable stylistic lint from shellcheck
@@ -131,8 +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,7 +65,7 @@ When working on ty, PR titles should start with `[ty]` and be tagged with the `t
- All changes must be tested. If you're not testing your changes, you're not done.
- Get your tests to pass. If you didn't run the tests, your code does not work.
- Follow existing code style. Check neighboring files for patterns.
- Always run `uvx pre-commit run -a` at the end of a task.
- Always run `uvx prek run -a` at the end of a task.
- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first.
- Avoid falling back to patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system.
- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability.

View File

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

View File

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

View File

@@ -4025,7 +4025,7 @@ quux.<CURSOR>
__module__ :: str
__mul__ :: bound method Quux.__mul__(value: SupportsIndex, /) -> tuple[int | str, ...]
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: (x: int, y: str) -> None
__new__ :: (x: int, y: str) -> Quux
__orig_bases__ :: tuple[Any, ...]
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]

View File

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

View File

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

View File

@@ -0,0 +1,939 @@
# Calls to `type()`
## Single-argument form
A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the
tests for the `__class__` attribute.)
```py
reveal_type(type(1)) # revealed: <class 'int'>
```
## Three-argument form (dynamic class creation)
A three-argument call to `type()` creates a new class. We synthesize a class type using the name
from the first argument:
```py
class Base: ...
class Mixin: ...
# We synthesize a class type using the name argument
reveal_type(type("Foo", (), {})) # revealed: <class 'Foo'>
# With a single base class
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: <class 'Foo'>
# With multiple base classes
reveal_type(type("Foo", (Base, Mixin), {})) # revealed: <class 'Foo'>
# The inferred type is assignable to type[Base] since Foo inherits from Base
tests: list[type[Base]] = []
testCaseClass = type("Foo", (Base,), {})
tests.append(testCaseClass) # No error - type[Foo] is assignable to type[Base]
```
The name can also be provided indirectly via a variable with a string literal type:
```py
name = "IndirectClass"
IndirectClass = type(name, (), {})
reveal_type(IndirectClass) # revealed: <class 'IndirectClass'>
# Works with base classes too
class Base: ...
base_name = "DerivedClass"
DerivedClass = type(base_name, (Base,), {})
reveal_type(DerivedClass) # revealed: <class 'DerivedClass'>
```
## Distinct class types
Each `type()` call produces a distinct class type, even if they have the same name and bases:
```py
from ty_extensions import static_assert, is_equivalent_to
class Base: ...
Foo1 = type("Foo", (Base,), {})
Foo2 = type("Foo", (Base,), {})
# Even though they have the same name and bases, they are distinct types
static_assert(not is_equivalent_to(Foo1, Foo2))
# Each instance is typed with its respective class
foo1 = Foo1()
foo2 = Foo2()
def takes_foo1(x: Foo1) -> None: ...
def takes_foo2(x: Foo2) -> None: ...
takes_foo1(foo1) # OK
takes_foo2(foo2) # OK
# error: [invalid-argument-type] "Argument to function `takes_foo1` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`"
takes_foo1(foo2)
# error: [invalid-argument-type] "Argument to function `takes_foo2` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`"
takes_foo2(foo1)
```
## Instances and attribute access
Instances of dynamic classes are typed with the synthesized class name. Attributes from all base
classes are accessible:
```py
class Base:
base_attr: int = 1
def base_method(self) -> str:
return "hello"
class Mixin:
mixin_attr: str = "mixin"
Foo = type("Foo", (Base,), {})
foo = Foo()
# Instance is typed with the synthesized class name
reveal_type(foo) # revealed: Foo
# Inherited attributes are accessible
reveal_type(foo.base_attr) # revealed: int
reveal_type(foo.base_method()) # revealed: str
# Multiple inheritance: attributes from all bases are accessible
Bar = type("Bar", (Base, Mixin), {})
bar = Bar()
reveal_type(bar.base_attr) # revealed: int
reveal_type(bar.mixin_attr) # revealed: str
```
Attributes from the namespace dict (third argument) are tracked:
```py
class Base: ...
Foo = type("Foo", (Base,), {"custom_attr": 42})
# Class attribute access
reveal_type(Foo.custom_attr) # revealed: Literal[42]
# Instance attribute access
foo = Foo()
reveal_type(foo.custom_attr) # revealed: Literal[42]
```
When the namespace dict is not a literal (e.g., passed as a parameter), attribute access returns
`Unknown` since we can't know what attributes might be defined:
```py
from typing import Any
class DynamicBase: ...
def f(attributes: dict[str, Any]):
X = type("X", (DynamicBase,), attributes)
reveal_type(X) # revealed: <class 'X'>
# Attribute access returns Unknown since the namespace is dynamic
reveal_type(X.foo) # revealed: Unknown
x = X()
reveal_type(x.bar) # revealed: Unknown
```
When a `TypedDict` is passed as the namespace argument, we synthesize a class type with the known
keys from the `TypedDict` as attributes. Since `TypedDict` instances are "open" (they can have
arbitrary additional string keys), unknown attributes return `Unknown`:
```py
from typing import TypedDict
class Namespace(TypedDict):
z: int
def g(attributes: Namespace):
Y = type("Y", (), attributes)
reveal_type(Y) # revealed: <class 'Y'>
# Known keys from the TypedDict are tracked as attributes
reveal_type(Y.z) # revealed: int
y = Y()
reveal_type(y.z) # revealed: int
# Unknown attributes return Unknown since TypedDicts are open
reveal_type(Y.unknown) # revealed: Unknown
reveal_type(y.unknown) # revealed: Unknown
```
## Closed TypedDicts (PEP-728)
TODO: We don't support the PEP-728 `closed=True` keyword argument to `TypedDict` yet. When we do, a
closed TypedDict namespace should NOT be marked as dynamic, and accessing unknown attributes should
emit an error instead of returning `Unknown`.
```py
from typing import TypedDict
class ClosedNamespace(TypedDict, closed=True):
x: int
y: str
def h(ns: ClosedNamespace):
X = type("X", (), ns)
reveal_type(X) # revealed: <class 'X'>
# Known keys from the TypedDict are tracked as attributes
reveal_type(X.x) # revealed: int
reveal_type(X.y) # revealed: str
x = X()
reveal_type(x.x) # revealed: int
reveal_type(x.y) # revealed: str
# TODO: Once we support `closed=True`, these should emit errors instead of returning Unknown
reveal_type(X.unknown) # revealed: Unknown
reveal_type(x.unknown) # revealed: Unknown
```
## Inheritance from dynamic classes
Regular classes can inherit from dynamic classes:
```py
class Base:
base_attr: int = 1
DynamicClass = type("DynamicClass", (Base,), {})
class Child(DynamicClass):
child_attr: str = "child"
child = Child()
# Attributes from the dynamic class's base are accessible
reveal_type(child.base_attr) # revealed: int
# The child class's own attributes are accessible
reveal_type(child.child_attr) # revealed: str
# Child instances are subtypes of DynamicClass instances
def takes_dynamic(x: DynamicClass) -> None: ...
takes_dynamic(child) # No error - Child is a subtype of DynamicClass
# isinstance narrows to the dynamic class instance type
def check_isinstance(x: object) -> None:
if isinstance(x, DynamicClass):
reveal_type(x) # revealed: DynamicClass
# Dynamic class inheriting from int narrows correctly with isinstance
IntSubclass = type("IntSubclass", (int,), {})
def check_int_subclass(x: IntSubclass | str) -> None:
if isinstance(x, int):
# IntSubclass inherits from int, so it's included in the narrowed type
reveal_type(x) # revealed: IntSubclass
else:
reveal_type(x) # revealed: str
```
## Disjointness
Dynamic classes are not considered disjoint from unrelated types (since a subclass could inherit
from both):
```py
class Base: ...
Foo = type("Foo", (Base,), {})
def check_disjointness(x: Foo | int) -> None:
if isinstance(x, int):
reveal_type(x) # revealed: int
else:
# Foo and int are not considered disjoint because `class C(Foo, int)` could exist.
reveal_type(x) # revealed: Foo & ~int
```
Disjointness also works for `type[]` of dynamic classes:
```py
from ty_extensions import is_disjoint_from, static_assert
# Dynamic classes with disjoint bases have disjoint type[] types.
IntClass = type("IntClass", (int,), {})
StrClass = type("StrClass", (str,), {})
static_assert(is_disjoint_from(type[IntClass], type[StrClass]))
static_assert(is_disjoint_from(type[StrClass], type[IntClass]))
# Dynamic classes that share a common base are not disjoint.
class Base: ...
Foo = type("Foo", (Base,), {})
Bar = type("Bar", (Base,), {})
static_assert(not is_disjoint_from(type[Foo], type[Bar]))
```
## Using dynamic classes with `super()`
Dynamic classes can be used as the pivot class in `super()` calls:
```py
class Base:
def method(self) -> int:
return 42
DynamicChild = type("DynamicChild", (Base,), {})
# Using dynamic class as pivot with dynamic class instance owner
fc = DynamicChild()
reveal_type(super(DynamicChild, fc)) # revealed: <super: <class 'DynamicChild'>, DynamicChild>
reveal_type(super(DynamicChild, fc).method()) # revealed: int
# Regular class inheriting from dynamic class
class RegularChild(DynamicChild):
pass
rc = RegularChild()
reveal_type(super(RegularChild, rc)) # revealed: <super: <class 'RegularChild'>, RegularChild>
reveal_type(super(RegularChild, rc).method()) # revealed: int
# Using dynamic class as pivot with regular class instance owner
reveal_type(super(DynamicChild, rc)) # revealed: <super: <class 'DynamicChild'>, RegularChild>
reveal_type(super(DynamicChild, rc).method()) # revealed: int
```
## Dynamic class inheritance chains
Dynamic classes can inherit from other dynamic classes:
```py
class Base:
base_attr: int = 1
# Create a dynamic class that inherits from a regular class.
Parent = type("Parent", (Base,), {})
reveal_type(Parent) # revealed: <class 'Parent'>
# Create a dynamic class that inherits from another dynamic class.
ChildCls = type("ChildCls", (Parent,), {})
reveal_type(ChildCls) # revealed: <class 'ChildCls'>
# Child instances have access to attributes from the entire inheritance chain.
child = ChildCls()
reveal_type(child) # revealed: ChildCls
reveal_type(child.base_attr) # revealed: int
# Child instances are subtypes of `Parent` instances.
def takes_parent(x: Parent) -> None: ...
takes_parent(child) # No error - `ChildCls` is a subtype of `Parent`
```
## Dataclass transform inheritance
Dynamic classes that inherit from a `@dataclass_transform()` decorated base class are recognized as
dataclass-like and have the synthesized `__dataclass_fields__` attribute:
```py
from dataclasses import Field
from typing_extensions import dataclass_transform
@dataclass_transform()
class DataclassBase:
"""Base class decorated with @dataclass_transform()."""
pass
# A dynamic class inheriting from a dataclass_transform base
DynamicModel = type("DynamicModel", (DataclassBase,), {})
# The dynamic class has __dataclass_fields__ synthesized
reveal_type(DynamicModel.__dataclass_fields__) # revealed: dict[str, Field[Any]]
```
## Applying `@dataclass` decorator directly
Applying the `@dataclass` decorator directly to a dynamic class is supported:
```py
from dataclasses import dataclass
Foo = type("Foo", (), {})
Foo = dataclass(Foo)
reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
```
## Generic base classes
Dynamic classes with generic base classes:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Container(Generic[T]):
value: T
# Dynamic class inheriting from a generic class specialization
IntContainer = type("IntContainer", (Container[int],), {})
reveal_type(IntContainer) # revealed: <class 'IntContainer'>
container = IntContainer()
reveal_type(container) # revealed: IntContainer
reveal_type(container.value) # revealed: int
```
## `type()` and `__class__` on dynamic instances
`type(instance)` returns the class of the dynamic instance:
```py
class Base: ...
Foo = type("Foo", (Base,), {})
foo = Foo()
# type() on an instance returns the class
reveal_type(type(foo)) # revealed: type[Foo]
```
`__class__` attribute access on dynamic instances:
```py
class Base: ...
Foo = type("Foo", (Base,), {})
foo = Foo()
# __class__ returns the class type
reveal_type(foo.__class__) # revealed: type[Foo]
```
`__class__` on the dynamic class itself returns the metaclass (consistent with static classes):
```py
class StaticClass: ...
DynamicClass = type("DynamicClass", (), {})
# Both static and dynamic classes have `type` as their metaclass
reveal_type(StaticClass.__class__) # revealed: <class 'type'>
reveal_type(DynamicClass.__class__) # revealed: <class 'type'>
```
## Subtype relationships
Dynamic instances are subtypes of `object`:
```py
class Base: ...
Foo = type("Foo", (Base,), {})
foo = Foo()
# All dynamic instances are subtypes of object
def takes_object(x: object) -> None: ...
takes_object(foo) # No error - Foo is a subtype of object
# Even dynamic classes with no explicit bases are subtypes of object
EmptyBases = type("EmptyBases", (), {})
empty = EmptyBases()
takes_object(empty) # No error
```
## Attributes from `builtins.type`
Attributes defined on `builtins.type` are accessible on dynamic classes:
```py
T = type("T", (), {})
# Inherited from `builtins.type`:
reveal_type(T.__dictoffset__) # revealed: int
reveal_type(T.__name__) # revealed: str
reveal_type(T.__bases__) # revealed: tuple[type, ...]
reveal_type(T.__mro__) # revealed: tuple[type, ...]
```
## Invalid calls
Other numbers of arguments are invalid:
```py
# error: [no-matching-overload] "No overload of class `type` matches arguments"
reveal_type(type("Foo", ())) # revealed: Unknown
# TODO: the keyword arguments for `Foo`/`Bar`/`Baz` here are invalid
# (you cannot pass `metaclass=` to `type()`, and none of them have
# base classes with `__init_subclass__` methods),
# but `type[Unknown]` would be better than `Unknown` here
#
# error: [no-matching-overload] "No overload of class `type` matches arguments"
reveal_type(type("Foo", (), {}, weird_other_arg=42)) # revealed: Unknown
# error: [no-matching-overload] "No overload of class `type` matches arguments"
reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown
# error: [no-matching-overload] "No overload of class `type` matches arguments"
reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown
```
The following calls are also invalid, due to incorrect argument types:
```py
class Base: ...
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `Literal[b"Foo"]`"
type(b"Foo", (), {})
# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found `<class 'Base'>`"
type("Foo", Base, {})
# error: 14 [invalid-base] "Invalid class base with type `Literal[1]`"
# error: 17 [invalid-base] "Invalid class base with type `Literal[2]`"
type("Foo", (1, 2), {})
# error: [invalid-argument-type] "Invalid argument to parameter 3 (`namespace`) of `type()`: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`"
type("Foo", (Base,), {b"attr": 1})
```
## `type[...]` as base class
`type[...]` (SubclassOf) types cannot be used as base classes. When a `type[...]` is used in the
bases tuple, we emit a diagnostic and insert `Unknown` into the MRO. This gives exactly one
diagnostic about the unsupported base, rather than cascading errors:
```py
from ty_extensions import reveal_mro
class Base:
base_attr: int = 1
def f(x: type[Base]):
# error: [unsupported-base] "Unsupported class base"
Child = type("Child", (x,), {})
# The class is still created with `Unknown` in MRO, allowing attribute access
reveal_type(Child) # revealed: <class 'Child'>
reveal_mro(Child) # revealed: (<class 'Child'>, Unknown, <class 'object'>)
child = Child()
reveal_type(child) # revealed: Child
# Attributes from `Unknown` are accessible without further errors
reveal_type(child.base_attr) # revealed: Unknown
```
## MRO errors
MRO errors are detected and reported:
```py
class A: ...
# Duplicate bases are detected
# error: [duplicate-base] "Duplicate base class <class 'A'> in class `Dup`"
Dup = type("Dup", (A, A), {})
```
Unknown bases (from unresolved imports) don't trigger duplicate-base diagnostics, since we can't
know if they represent the same type:
```py
from unresolved_module import Bar, Baz # error: [unresolved-import]
# No duplicate-base error here - Bar and Baz are Unknown, and we can't
# know if they're the same type.
X = type("X", (Bar, Baz), {})
```
```py
class A: ...
class B(A): ...
class C(A): ...
# This creates an inconsistent MRO because D would need B before C (from first base)
# but also C before B (from second base inheritance through A)
class X(B, C): ...
class Y(C, B): ...
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Conflict` with bases `[<class 'X'>, <class 'Y'>]`"
Conflict = type("Conflict", (X, Y), {})
```
## Metaclass conflicts
Metaclass conflicts are detected and reported:
```py
class Meta1(type): ...
class Meta2(type): ...
class A(metaclass=Meta1): ...
class B(metaclass=Meta2): ...
# error: [conflicting-metaclass] "The metaclass of a derived class (`Bad`) must be a subclass of the metaclasses of all its bases, but `Meta1` (metaclass of base class `<class 'A'>`) and `Meta2` (metaclass of base class `<class 'B'>`) have no subclass relationship"
Bad = type("Bad", (A, B), {})
```
## `__slots__` in namespace dictionary
Functional classes can define `__slots__` in the namespace dictionary. Non-empty `__slots__` makes
the class a "disjoint base", which prevents it from being used alongside other disjoint bases in a
class hierarchy:
```py
# Functional class with non-empty __slots__
Slotted = type("Slotted", (), {"__slots__": ("x", "y")})
slotted = Slotted()
reveal_type(slotted) # revealed: Slotted
# Classes with empty __slots__ are not disjoint bases
EmptySlots = type("EmptySlots", (), {"__slots__": ()})
# Classes with no __slots__ are not disjoint bases
NoSlots = type("NoSlots", (), {})
# String __slots__ are treated as a single slot (non-empty)
StringSlots = type("StringSlots", (), {"__slots__": "x"})
```
Functional classes with non-empty `__slots__` cannot coexist with other disjoint bases:
```py
class RegularSlotted:
__slots__ = ("a",)
# error: [instance-layout-conflict]
class Conflict(
RegularSlotted,
type("FuncSlotted", (), {"__slots__": ("b",)}),
): ...
```
Two functional classes with non-empty `__slots__` also conflict:
```py
A = type("A", (), {"__slots__": ("x",)})
B = type("B", (), {"__slots__": ("y",)})
# error: [instance-layout-conflict]
class Conflict(
A,
B,
): ...
```
When the namespace dictionary is dynamic (not a literal), we can't determine if `__slots__` is
defined, so no diagnostic is emitted:
```py
from typing import Any
class SlottedBase:
__slots__ = ("a",)
def f(ns: dict[str, Any]):
# The namespace might or might not contain __slots__, so no error is emitted
Dynamic = type("Dynamic", (), ns)
# No error: we can't prove there's a conflict since ns might not have __slots__
class MaybeConflict(SlottedBase, Dynamic): ...
```
## Cyclic functional class definitions
Self-referential class definitions using `type()` are detected. The name being defined is referenced
in the bases tuple before it's available:
```pyi
# error: [unresolved-reference] "Name `X` used when not defined"
X = type("X", (X,), {})
```
## Dynamic class names (non-literal strings)
When the class name is not a string literal, we still create a class literal type but with a
placeholder name `<unknown>`:
```py
def make_class(name: str):
# When the name is a dynamic string, we use a placeholder name
cls = type(name, (), {})
reveal_type(cls) # revealed: <class '<unknown>'>
return cls
def make_classes(name1: str, name2: str):
cls1 = type(name1, (), {})
cls2 = type(name2, (), {})
def inner(x: cls1): ...
# error: [invalid-argument-type] "Argument to function `inner` is incorrect: Expected `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:8`, found `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:9`"
inner(cls2())
```
When the name comes from a union of string literals, we also use a placeholder name:
```py
import random
name = "Foo" if random.random() > 0.5 else "Bar"
reveal_type(name) # revealed: Literal["Foo", "Bar"]
# We cannot determine which name will be used at runtime
cls = type(name, (), {})
reveal_type(cls) # revealed: <class '<unknown>'>
```
## Dynamic bases (variable tuple)
When the bases tuple is a function parameter with a non-literal tuple type, we still create a class
literal type but with `Unknown` in the MRO. This means instances are treated highly dynamically -
any attribute access returns `Unknown`:
```py
from ty_extensions import reveal_mro
class Base1: ...
class Base2: ...
def make_class(bases: tuple[type, ...]):
# Class literal is created with Unknown base in MRO
cls = type("Cls", bases, {})
reveal_type(cls) # revealed: <class 'Cls'>
reveal_mro(cls) # revealed: (<class 'Cls'>, Unknown, <class 'object'>)
# Instances have dynamic attribute access due to Unknown base
instance = cls()
reveal_type(instance) # revealed: Cls
reveal_type(instance.any_attr) # revealed: Unknown
reveal_type(instance.any_method()) # revealed: Unknown
return cls
```
When `bases` is a module-level variable holding a tuple of class literals, we can extract the base
classes:
```py
class Base:
attr: int = 1
bases = (Base,)
Cls = type("Cls", bases, {})
reveal_type(Cls) # revealed: <class 'Cls'>
instance = Cls()
reveal_type(instance.attr) # revealed: int
```
## Variadic arguments
Unpacking arguments with `*args` or `**kwargs`:
```py
class Base: ...
# Unpacking a tuple for bases
bases_tuple = (Base,)
Cls1 = type("Cls1", (*bases_tuple,), {})
reveal_type(Cls1) # revealed: <class 'Cls1'>
# Unpacking a dict for the namespace - the dict contents are not tracked anyway
namespace = {"attr": 1}
Cls2 = type("Cls2", (Base,), {**namespace})
reveal_type(Cls2) # revealed: <class 'Cls2'>
```
When `*args` or `**kwargs` fill an unknown number of parameters, we cannot determine which overload
of `type()` is being called:
```py
def f(*args, **kwargs):
# Completely dynamic: could be 1-arg or 3-arg form
A = type(*args, **kwargs)
reveal_type(A) # revealed: type[Unknown]
# Has a string first arg, but unknown additional args from *args
B = type("B", *args, **kwargs)
# TODO: `type[Unknown]` would cause fewer false positives
reveal_type(B) # revealed: <class 'str'>
# Has string and tuple, but unknown additional args
C = type("C", (), *args, **kwargs)
# TODO: `type[Unknown]` would cause fewer false positives
reveal_type(C) # revealed: type
# All three positional args provided, only **kwargs unknown
D = type("D", (), {}, **kwargs)
# TODO: `type[Unknown]` would cause fewer false positives
reveal_type(D) # revealed: type
```
## Explicit type annotations
When an explicit type annotation is provided, the inferred type is checked against it:
```py
# The annotation `type` is compatible with the inferred class literal type
T: type = type("T", (), {})
reveal_type(T) # revealed: <class 'T'>
# The annotation `type[Base]` is compatible with the inferred type
class Base: ...
Derived: type[Base] = type("Derived", (Base,), {})
reveal_type(Derived) # revealed: <class 'Derived'>
# Incompatible annotation produces an error
class Unrelated: ...
# error: [invalid-assignment]
Bad: type[Unrelated] = type("Bad", (Base,), {})
```
## Special base classes
Some special base classes work with dynamic class creation, but special semantics may not be fully
synthesized:
### Protocol bases
```py
# Protocol bases work - the class is created as a subclass of the protocol
from typing import Protocol
class MyProtocol(Protocol):
def method(self) -> int: ...
ProtoImpl = type("ProtoImpl", (MyProtocol,), {})
reveal_type(ProtoImpl) # revealed: <class 'ProtoImpl'>
instance = ProtoImpl()
reveal_type(instance) # revealed: ProtoImpl
```
### TypedDict bases
```py
# TypedDict bases work but TypedDict semantics aren't applied to dynamic subclasses
from typing_extensions import TypedDict
class MyDict(TypedDict):
name: str
age: int
DictSubclass = type("DictSubclass", (MyDict,), {})
reveal_type(DictSubclass) # revealed: <class 'DictSubclass'>
```
### NamedTuple bases
```py
# NamedTuple bases work but the dynamic subclass isn't recognized as a NamedTuple
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
Point3D = type("Point3D", (Point,), {})
reveal_type(Point3D) # revealed: <class 'Point3D'>
```
### Enum bases
```py
# Enum subclassing via type() is not supported - EnumMeta requires special dict handling
# that type() cannot provide. This applies even to empty enums.
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
class EmptyEnum(Enum):
pass
# TODO: We should emit a diagnostic here - type() cannot create Enum subclasses
ExtendedColor = type("ExtendedColor", (Color,), {})
reveal_type(ExtendedColor) # revealed: <class 'ExtendedColor'>
# Even empty enums fail - EnumMeta requires special dict handling
# TODO: We should emit a diagnostic here too
ValidExtension = type("ValidExtension", (EmptyEnum,), {})
reveal_type(ValidExtension) # revealed: <class 'ValidExtension'>
```
## `__init_subclass__` keyword arguments
When a base class defines `__init_subclass__` with required arguments, those should be passed to
`type()`. This is not yet supported:
```py
class Base:
def __init_subclass__(cls, required_arg: str, **kwargs):
super().__init_subclass__(**kwargs)
cls.config = required_arg
# Regular class definition - this works and passes the argument
class Child(Base, required_arg="value"):
pass
# The dynamically assigned attribute has Unknown in its type
reveal_type(Child.config) # revealed: Unknown | str
# Dynamic class creation - keyword arguments are not yet supported
# TODO: This should work: type("DynamicChild", (Base,), {}, required_arg="value")
# error: [no-matching-overload]
DynamicChild = type("DynamicChild", (Base,), {}, required_arg="value")
```
## Empty bases tuple
When the bases tuple is empty, the class implicitly inherits from `object`:
```py
from ty_extensions import reveal_mro
EmptyBases = type("EmptyBases", (), {})
reveal_type(EmptyBases) # revealed: <class 'EmptyBases'>
reveal_mro(EmptyBases) # revealed: (<class 'EmptyBases'>, <class 'object'>)
instance = EmptyBases()
reveal_type(instance) # revealed: EmptyBases
# object methods are available
reveal_type(instance.__hash__()) # revealed: int
reveal_type(instance.__str__()) # revealed: str
```
## Custom metaclass via bases
When a base class has a custom metaclass, the dynamic class inherits that metaclass:
```py
class MyMeta(type):
custom_attr: str = "meta"
class Base(metaclass=MyMeta): ...
# Dynamic class inherits the metaclass from Base
Dynamic = type("Dynamic", (Base,), {})
reveal_type(Dynamic) # revealed: <class 'Dynamic'>
# Metaclass attributes are accessible on the class
reveal_type(Dynamic.custom_attr) # revealed: str
```

View File

@@ -1686,3 +1686,38 @@ reveal_type(ordered_foo) # revealed: <class 'Foo'>
reveal_type(ordered_foo()) # revealed: Foo
reveal_type(ordered_foo() < ordered_foo()) # revealed: bool
```
## Dynamic class literals
Dynamic classes created with `type()` can be wrapped with `dataclass()` as a function:
```py
from dataclasses import dataclass
# Basic dynamic class wrapped with dataclass
DynamicFoo = type("DynamicFoo", (), {})
DynamicFoo = dataclass(DynamicFoo)
# The class is recognized as a dataclass
reveal_type(DynamicFoo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
# Can create instances
instance = DynamicFoo()
reveal_type(instance) # revealed: DynamicFoo
```
Dynamic classes that inherit from a dataclass base also work:
```py
from dataclasses import dataclass
@dataclass
class Base:
x: int
# Dynamic class inheriting from a dataclass
DynamicChild = type("DynamicChild", (Base,), {})
DynamicChild = dataclass(DynamicChild)
reveal_type(DynamicChild.__dataclass_fields__) # revealed: dict[str, Field[Any]]
```

View File

@@ -38,6 +38,125 @@ reveal_type(s1 > s2) # revealed: bool
reveal_type(s1 >= s2) # revealed: bool
```
## Signature derived from source ordering method
When the source ordering method accepts a broader type (like `object`) for its `other` parameter,
the synthesized comparison methods should use the same signature. This allows comparisons with types
other than the class itself:
```py
from functools import total_ordering
@total_ordering
class Comparable:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
if isinstance(other, Comparable):
return self.value == other.value
if isinstance(other, int):
return self.value == other
return NotImplemented
def __lt__(self, other: object) -> bool:
if isinstance(other, Comparable):
return self.value < other.value
if isinstance(other, int):
return self.value < other
return NotImplemented
a = Comparable(10)
b = Comparable(20)
# Comparisons with the same type work.
reveal_type(a <= b) # revealed: bool
reveal_type(a >= b) # revealed: bool
# Comparisons with `int` also work because `__lt__` accepts `object`.
reveal_type(a <= 15) # revealed: bool
reveal_type(a >= 5) # revealed: bool
```
## Multiple ordering methods with different signatures
When multiple ordering methods are defined with different signatures, the decorator selects a "root"
method using the priority order: `__lt__` > `__le__` > `__gt__` > `__ge__`. Synthesized methods use
the signature from the highest-priority method. Methods that are explicitly defined are not
overridden.
```py
from functools import total_ordering
@total_ordering
class MultiSig:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
return True
# __lt__ accepts `object` (highest priority, used as root)
def __lt__(self, other: object) -> bool:
return True
# __gt__ only accepts `MultiSig` (not overridden by decorator)
def __gt__(self, other: "MultiSig") -> bool:
return True
a = MultiSig(10)
b = MultiSig(20)
# __le__ and __ge__ are synthesized with __lt__'s signature (accepts `object`)
reveal_type(a <= b) # revealed: bool
reveal_type(a <= 15) # revealed: bool
reveal_type(a >= b) # revealed: bool
reveal_type(a >= 15) # revealed: bool
# __gt__ keeps its original signature (only accepts MultiSig)
reveal_type(a > b) # revealed: bool
a > 15 # error: [unsupported-operator]
```
## Overloaded ordering method
When the source ordering method is overloaded, the synthesized comparison methods should preserve
all overloads:
```py
from functools import total_ordering
from typing import overload
@total_ordering
class Flexible:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
return True
@overload
def __lt__(self, other: "Flexible") -> bool: ...
@overload
def __lt__(self, other: int) -> bool: ...
def __lt__(self, other: "Flexible | int") -> bool:
if isinstance(other, Flexible):
return self.value < other.value
return self.value < other
a = Flexible(10)
b = Flexible(20)
# Synthesized __le__ preserves overloads from __lt__
reveal_type(a <= b) # revealed: bool
reveal_type(a <= 15) # revealed: bool
# Synthesized __ge__ also preserves overloads
reveal_type(a >= b) # revealed: bool
reveal_type(a >= 15) # revealed: bool
# But comparison with an unsupported type should still error
a <= "string" # error: [unsupported-operator]
```
## Using `__gt__` as the root comparison method
When a class defines `__eq__` and `__gt__`, the decorator synthesizes `__lt__`, `__le__`, and
@@ -127,6 +246,41 @@ reveal_type(c1 > c2) # revealed: bool
reveal_type(c1 >= c2) # revealed: bool
```
## Method precedence with inheritance
The decorator always prefers `__lt__` > `__le__` > `__gt__` > `__ge__`, regardless of whether the
method is defined locally or inherited. In this example, the inherited `__lt__` takes precedence
over the locally-defined `__gt__`:
```py
from functools import total_ordering
from typing import Literal
class Base:
def __lt__(self, other: "Base") -> Literal[True]:
return True
@total_ordering
class Child(Base):
# __gt__ is defined locally, but __lt__ (inherited) takes precedence
def __gt__(self, other: "Child") -> Literal[False]:
return False
c1 = Child()
c2 = Child()
# __lt__ is inherited from Base
reveal_type(c1 < c2) # revealed: Literal[True]
# __gt__ is defined locally on Child
reveal_type(c1 > c2) # revealed: Literal[False]
# __le__ and __ge__ are synthesized from __lt__ (the highest-priority method),
# even though __gt__ is defined locally on the class itself
reveal_type(c1 <= c2) # revealed: bool
reveal_type(c1 >= c2) # revealed: bool
```
## Explicitly-defined methods are not overridden
When a class explicitly defines multiple comparison methods, the decorator does not override them.
@@ -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
@@ -277,3 +504,70 @@ class HasOrderingMethod:
ValidOrderedClass = total_ordering(HasOrderingMethod)
reveal_type(ValidOrderedClass) # revealed: type[HasOrderingMethod]
```
## Function call form with `type()`
When `total_ordering` is called on a class created with `type()`, the same validation is performed:
```py
from functools import total_ordering
def lt_impl(self, other) -> bool:
return True
# No error: the functional class defines `__lt__` in its namespace
ValidFunctional = total_ordering(type("ValidFunctional", (), {"__lt__": lt_impl}))
# error: [invalid-total-ordering]
InvalidFunctional = total_ordering(type("InvalidFunctional", (), {}))
```
## Inherited from functional class
When a class inherits from a functional class that defines an ordering method, `@total_ordering`
correctly detects it:
```py
from functools import total_ordering
def lt_impl(self, other) -> bool:
return True
def eq_impl(self, other) -> bool:
return True
# Functional class with __lt__ method
OrderedBase = type("OrderedBase", (), {"__lt__": lt_impl})
# A class inheriting from OrderedBase gets the ordering method
@total_ordering
class Ordered(OrderedBase):
def __eq__(self, other: object) -> bool:
return True
o1 = Ordered()
o2 = Ordered()
# Inherited __lt__ is available
reveal_type(o1 < o2) # revealed: bool
# @total_ordering synthesizes the other methods
reveal_type(o1 <= o2) # revealed: bool
reveal_type(o1 > o2) # revealed: bool
reveal_type(o1 >= o2) # revealed: bool
```
When the functional base class does not define any ordering method, `@total_ordering` emits an
error:
```py
from functools import total_ordering
# Functional class without ordering methods (invalid for @total_ordering)
NoOrderBase = type("NoOrderBase", (), {})
@total_ordering # error: [invalid-total-ordering]
class NoOrder(NoOrderBase):
def __eq__(self, other: object) -> bool:
return True
```

View File

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

View File

@@ -84,17 +84,154 @@ alice.id = 42
bob.age = None
```
Alternative functional syntax:
Alternative functional syntax with a list literal:
```py
Person2 = NamedTuple("Person", [("id", int), ("name", str)])
alice2 = Person2(1, "Alice")
# TODO: should be an error
# error: [missing-argument]
Person2(1)
reveal_type(alice2.id) # revealed: @Todo(functional `NamedTuple` syntax)
reveal_type(alice2.name) # revealed: @Todo(functional `NamedTuple` syntax)
reveal_type(alice2.id) # revealed: int
reveal_type(alice2.name) # revealed: str
```
Functional syntax with a tuple literal:
```py
Person3 = NamedTuple("Person", (("id", int), ("name", str)))
alice3 = Person3(1, "Alice")
reveal_type(alice3.id) # revealed: int
reveal_type(alice3.name) # revealed: str
```
### Functional syntax with variable name
When the typename is passed via a variable, we can extract it from the inferred literal string type:
```py
from typing import NamedTuple
name = "Person"
Person = NamedTuple(name, [("id", int), ("name", str)])
p = Person(1, "Alice")
reveal_type(p.id) # revealed: int
reveal_type(p.name) # revealed: str
```
### Functional syntax with tuple variable fields
When fields are passed via a tuple variable, we can extract the literal field names and types from
the inferred tuple type:
```py
from typing import NamedTuple
fields = (("host", str), ("port", int))
Url = NamedTuple("Url", fields)
url = Url("localhost", 8080)
reveal_type(url.host) # revealed: str
reveal_type(url.port) # revealed: int
```
### Class inheriting from functional NamedTuple
Classes can inherit from functional namedtuples. The constructor parameters and field types are
properly inherited:
```py
from typing import NamedTuple
class Url(NamedTuple("Url", [("host", str), ("path", str)])):
pass
reveal_type(Url) # revealed: <class 'Url'>
reveal_type(Url.__new__) # revealed: (cls: type, host: str, path: str) -> Url
# Constructor works with the inherited fields.
url = Url("example.com", "/path")
reveal_type(url) # revealed: Url
reveal_type(url.host) # revealed: str
reveal_type(url.path) # revealed: str
# Error handling works correctly.
# error: [missing-argument]
Url("example.com")
# error: [too-many-positional-arguments]
Url("example.com", "/path", "extra")
```
Subclasses can add methods that use inherited fields:
```py
from typing import NamedTuple
from typing_extensions import Self
class Url(NamedTuple("Url", [("host", str), ("port", int)])):
def with_port(self, port: int) -> Self:
reveal_type(self.host) # revealed: str
reveal_type(self.port) # revealed: int
return self._replace(port=port)
url = Url("localhost", 8080)
reveal_type(url.with_port(9000)) # revealed: Url
```
Unlike classes that directly use `class Foo(NamedTuple):` syntax, classes inheriting from functional
namedtuples can use `super()` and override `__new__`:
```py
from collections import namedtuple
from typing import NamedTuple
class ExtType(namedtuple("ExtType", "code data")):
"""Override __new__ to add validation."""
def __new__(cls, code, data):
if not isinstance(code, int):
raise TypeError("code must be int")
return super().__new__(cls, code, data)
class Url(NamedTuple("Url", [("host", str), ("path", str)])):
"""Override __new__ to normalize the path."""
def __new__(cls, host, path):
if path and not path.startswith("/"):
path = "/" + path
return super().__new__(cls, host, path)
# Both work correctly.
ext = ExtType(42, b"hello")
reveal_type(ext) # revealed: ExtType
url = Url("example.com", "path")
reveal_type(url) # revealed: Url
```
### Functional syntax with list variable fields
When fields are passed via a list variable (not a literal), we fall back to `NamedTupleFallback`
which allows any attribute access. This is a regression test for accessing `Self` attributes in
methods of classes that inherit from namedtuples with dynamic fields:
```py
from typing import NamedTuple
from typing_extensions import Self
fields = [("host", str), ("port", int)]
class Url(NamedTuple("Url", fields)):
def with_port(self, port: int) -> Self:
# Attribute access on Self works via NamedTupleFallback.__getattr__.
reveal_type(self.host) # revealed: Any
reveal_type(self.port) # revealed: Any
reveal_type(self.unknown) # revealed: Any
return self._replace(port=port)
```
### Definition
@@ -311,6 +448,73 @@ alice = Person(1, "Alice", 42)
bob = Person(2, "Bob")
```
## `collections.namedtuple` with tuple variable field names
When field names are passed via a tuple variable, we can extract the literal field names from the
inferred tuple type. The class is properly synthesized (not a fallback), but field types are `Any`
since `collections.namedtuple` doesn't include type annotations:
```py
from collections import namedtuple
field_names = ("name", "age")
Person = namedtuple("Person", field_names)
reveal_type(Person) # revealed: <class 'Person'>
alice = Person("Alice", 42)
reveal_type(alice) # revealed: Person
reveal_type(alice.name) # revealed: Any
reveal_type(alice.age) # revealed: Any
```
## `collections.namedtuple` with list variable field names
When field names are passed via a list variable (not a literal), we fall back to
`NamedTupleFallback` which allows any attribute access. This is a regression test for accessing
`Self` attributes in methods of classes that inherit from namedtuples with dynamic fields:
```py
from collections import namedtuple
from typing_extensions import Self
field_names = ["host", "port"]
class Url(namedtuple("Url", field_names)):
def with_port(self, port: int) -> Self:
# Attribute access on Self works via NamedTupleFallback.__getattr__.
reveal_type(self.host) # revealed: Any
reveal_type(self.port) # revealed: Any
reveal_type(self.unknown) # revealed: Any
return self._replace(port=port)
```
## `collections.namedtuple` attributes
Functional namedtuples have synthesized attributes similar to class-based namedtuples:
```py
from collections import namedtuple
Person = namedtuple("Person", ["name", "age"])
reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]]
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: (self: Self, *, name: Any = ..., age: Any = ...) -> Self
# _make creates instances from an iterable.
reveal_type(Person._make(["Alice", 30])) # revealed: Person
# _asdict converts to a dictionary.
person = Person("Alice", 30)
reveal_type(person._asdict()) # revealed: dict[str, Any]
# _replace creates a copy with replaced fields.
reveal_type(person._replace(name="Bob")) # revealed: Person
```
## The symbol `NamedTuple` itself
At runtime, `NamedTuple` is a function, and we understand this:

View File

@@ -160,15 +160,19 @@ def _(x: A | B):
## No narrowing for multiple arguments
No narrowing should occur if `type` is used to dynamically create a class:
Narrowing does not occur in the same way if `type` is used to dynamically create a class:
```py
def _(x: str | int):
# The following diagnostic is valid, since the three-argument form of `type`
# can only be called with `str` as the first argument.
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
#
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `str | int`"
if type(x, (), {}) is str:
reveal_type(x) # revealed: str | int
# But we synthesize a new class object as the result of a three-argument call to `type`,
# and we know that this synthesized class object is not the same object as the `str` class object,
# so here the type is narrowed to `Never`!
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: str | int
```

View File

@@ -1592,7 +1592,9 @@ mod implicit_globals {
use crate::place::{Definedness, PlaceAndQualifiers};
use crate::semantic_index::symbol::Symbol;
use crate::semantic_index::{place_table, use_def_map};
use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type};
use crate::types::{
ClassLiteral, KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type,
};
use ruff_python_ast::PythonVersion;
use super::{DefinedPlace, Place, place_from_declarations};
@@ -1611,7 +1613,10 @@ mod implicit_globals {
else {
return Place::Undefined.into();
};
let module_type_scope = module_type_class.body_scope(db);
let Some(class) = module_type_class.as_static() else {
return Place::Undefined.into();
};
let module_type_scope = class.body_scope(db);
let place_table = place_table(db, module_type_scope);
let Some(symbol_id) = place_table.symbol_id(name) else {
return Place::Undefined.into();
@@ -1739,8 +1744,10 @@ mod implicit_globals {
return smallvec::SmallVec::default();
};
let module_type_scope = module_type.body_scope(db);
let module_type_symbol_table = place_table(db, module_type_scope);
let ClassLiteral::Static(module_type) = module_type else {
return smallvec::SmallVec::default();
};
let module_type_symbol_table = place_table(db, module_type.body_scope(db));
module_type_symbol_table
.symbols()

View File

@@ -24,6 +24,7 @@ use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module};
use type_ordering::union_or_intersection_elements_ordering;
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub(crate) use self::class::DynamicClassLiteral;
pub use self::cyclic::CycleDetector;
pub(crate) use self::cyclic::{PairVisitor, TypeTransformer};
pub(crate) use self::diagnostic::register_lints;
@@ -76,7 +77,7 @@ use crate::types::visitor::any_over_type;
use crate::unpack::EvaluationMode;
use crate::{Db, FxOrderSet, Program};
pub use class::KnownClass;
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, StaticClassLiteral};
use instance::Protocol;
pub use instance::{NominalInstanceType, ProtocolInstanceType};
pub use special_form::SpecialFormType;
@@ -782,7 +783,7 @@ pub enum Type<'db> {
Callable(CallableType<'db>),
/// A specific module object
ModuleLiteral(ModuleLiteralType<'db>),
/// A specific class object
/// A specific class object (either from a `class` statement or `type()` call)
ClassLiteral(ClassLiteral<'db>),
/// A specialization of a generic class
GenericAlias(GenericAlias<'db>),
@@ -976,9 +977,9 @@ impl<'db> Type<'db> {
}
fn is_enum(&self, db: &'db dyn Db) -> bool {
self.as_nominal_instance()
.and_then(|instance| crate::types::enums::enum_metadata(db, instance.class_literal(db)))
.is_some()
self.as_nominal_instance().is_some_and(|instance| {
crate::types::enums::enum_metadata(db, instance.class_literal(db)).is_some()
})
}
fn is_typealias_special_form(&self) -> bool {
@@ -1097,25 +1098,21 @@ impl<'db> Type<'db> {
pub(crate) fn specialization_of(
self,
db: &'db dyn Db,
expected_class: ClassLiteral<'_>,
expected_class: StaticClassLiteral<'_>,
) -> Option<Specialization<'db>> {
self.class_and_specialization_of_optional(db, Some(expected_class))
.map(|(_, specialization)| specialization)
self.specialization_of_optional(db, Some(expected_class))
}
/// If this type is a class instance, returns its class literal and specialization.
pub(crate) fn class_specialization(
self,
db: &'db dyn Db,
) -> Option<(ClassLiteral<'db>, Specialization<'db>)> {
self.class_and_specialization_of_optional(db, None)
/// If this type is a class instance, returns its specialization.
pub(crate) fn class_specialization(self, db: &'db dyn Db) -> Option<Specialization<'db>> {
self.specialization_of_optional(db, None)
}
fn class_and_specialization_of_optional(
fn specialization_of_optional(
self,
db: &'db dyn Db,
expected_class: Option<ClassLiteral<'_>>,
) -> Option<(ClassLiteral<'db>, Specialization<'db>)> {
expected_class: Option<StaticClassLiteral<'_>>,
) -> Option<Specialization<'db>> {
let class_type = match self {
Type::NominalInstance(instance) => instance,
Type::ProtocolInstance(instance) => instance.to_nominal_instance()?,
@@ -1124,12 +1121,12 @@ impl<'db> Type<'db> {
}
.class(db);
let (class_literal, specialization) = class_type.class_literal(db);
let (class_literal, specialization) = class_type.static_class_literal(db)?;
if expected_class.is_some_and(|expected_class| expected_class != class_literal) {
return None;
}
Some((class_literal, specialization?))
specialization
}
/// Returns the top materialization (or upper bound materialization) of this type, which is the
@@ -2048,7 +2045,10 @@ impl<'db> Type<'db> {
return;
};
let (class_literal, Some(specialization)) = instance.class(db).class_literal(db) else {
let Some((class_literal, Some(specialization))) =
instance.class(db).static_class_literal(db)
else {
return;
};
let generic_context = specialization.generic_context(db);
@@ -3248,11 +3248,14 @@ impl<'db> Type<'db> {
Type::NominalInstance(instance)
if matches!(name_str, "value" | "_value_")
&& is_single_member_enum(db, instance.class(db).class_literal(db).0) =>
&& is_single_member_enum(db, instance.class_literal(db)) =>
{
enum_metadata(db, instance.class(db).class_literal(db).0)
.and_then(|metadata| metadata.members.get_index(0).map(|(_, v)| *v))
.map_or(Place::Undefined, Place::bound)
enum_metadata(db, instance.class_literal(db))
.and_then(|metadata| {
let (_, ty) = metadata.members.get_index(0)?;
Some(Place::bound(*ty))
})
.unwrap_or_default()
.into()
}
@@ -3298,7 +3301,7 @@ impl<'db> Type<'db> {
)
.map(|outcome| Place::bound(outcome.return_type(db)))
// TODO: Handle call errors here.
.unwrap_or(Place::Undefined)
.unwrap_or_default()
.into()
};
@@ -3319,7 +3322,7 @@ impl<'db> Type<'db> {
)
.map(|outcome| Place::bound(outcome.return_type(db)))
// TODO: Handle call errors here.
.unwrap_or(Place::Undefined)
.unwrap_or_default()
.into()
};
@@ -3356,14 +3359,15 @@ impl<'db> Type<'db> {
}
Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => {
if let Some(enum_class) = match self {
let enum_class = match self {
Type::ClassLiteral(literal) => Some(literal),
Type::SubclassOf(subclass_of) => subclass_of
.subclass_of()
.into_class(db)
.map(|class| class.class_literal(db).0),
.map(|class| class.class_literal(db)),
_ => None,
} {
};
if let Some(enum_class) = enum_class {
if let Some(metadata) = enum_metadata(db, enum_class) {
if let Some(resolved_name) = metadata.resolve_member(&name) {
return Place::bound(Type::EnumLiteral(EnumLiteralType::new(
@@ -4001,6 +4005,33 @@ impl<'db> Type<'db> {
.into()
}
// collections.namedtuple(typename, field_names, ...)
Some(KnownFunction::NamedTuple) => Binding::single(
self,
Signature::new(
Parameters::new(
db,
[
Parameter::positional_or_keyword(Name::new_static("typename"))
.with_annotated_type(KnownClass::Str.to_instance(db)),
Parameter::positional_or_keyword(Name::new_static("field_names"))
.with_annotated_type(Type::any()),
// Additional optional parameters have defaults.
Parameter::keyword_only(Name::new_static("rename"))
.with_annotated_type(KnownClass::Bool.to_instance(db))
.with_default_type(Type::BooleanLiteral(false)),
Parameter::keyword_only(Name::new_static("defaults"))
.with_annotated_type(Type::any())
.with_default_type(Type::none(db)),
Parameter::keyword_only(Name::new_static("module"))
.with_default_type(Type::none(db)),
],
),
KnownClass::NamedTupleFallback.to_class_literal(db),
),
)
.into(),
_ => CallableBinding::from_overloads(
self,
function_type.signature(db).overloads.iter().cloned(),
@@ -4431,7 +4462,23 @@ impl<'db> Type<'db> {
}
Type::SpecialForm(SpecialFormType::NamedTuple) => {
Binding::single(self, Signature::todo("functional `NamedTuple` syntax")).into()
// typing.NamedTuple(typename: str, fields: ...)
Binding::single(
self,
Signature::new(
Parameters::new(
db,
[
Parameter::positional_or_keyword(Name::new_static("typename"))
.with_annotated_type(KnownClass::Str.to_instance(db)),
Parameter::positional_or_keyword(Name::new_static("fields"))
.with_annotated_type(Type::any()),
],
),
KnownClass::NamedTupleFallback.to_class_literal(db),
),
)
.into()
}
Type::GenericAlias(_) => {
@@ -5096,7 +5143,9 @@ impl<'db> Type<'db> {
let from_class_base = |base: ClassBase<'db>| {
let class = base.into_class()?;
if class.is_known(db, KnownClass::Generator) {
if let Some(specialization) = class.class_literal_specialized(db, None).1 {
if let Some((_, Some(specialization))) =
class.static_class_literal_specialized(db, None)
{
if let [_, _, return_ty] = specialization.types(db) {
return Some(*return_ty);
}
@@ -5623,9 +5672,11 @@ impl<'db> Type<'db> {
});
};
Ok(typing_self(db, scope_id, typevar_binding_context, class)
.map(Type::TypeVar)
.unwrap_or(*self))
Ok(
typing_self(db, scope_id, typevar_binding_context, class.into())
.map(Type::TypeVar)
.unwrap_or(*self),
)
}
// We ensure that `typing.TypeAlias` used in the expected position (annotating an
// annotated assignment statement) doesn't reach here. Using it in any other type
@@ -6521,13 +6572,9 @@ impl<'db> Type<'db> {
Some(TypeDefinition::Function(function.definition(db)))
}
Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))),
Self::ClassLiteral(class_literal) => {
Some(TypeDefinition::Class(class_literal.definition(db)))
}
Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))),
Self::NominalInstance(instance) => {
Some(TypeDefinition::Class(instance.class(db).definition(db)))
}
Self::ClassLiteral(class_literal) => class_literal.type_definition(db),
Self::GenericAlias(alias) => Some(TypeDefinition::StaticClass(alias.definition(db))),
Self::NominalInstance(instance) => instance.class(db).type_definition(db),
Self::KnownInstance(instance) => match instance {
KnownInstanceType::TypeVar(var) => {
Some(TypeDefinition::TypeVar(var.definition(db)?))
@@ -6540,9 +6587,11 @@ impl<'db> Type<'db> {
},
Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
SubclassOfInner::Class(class) => Some(TypeDefinition::Class(class.definition(db))),
SubclassOfInner::Dynamic(_) => None,
SubclassOfInner::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)),
SubclassOfInner::Class(class) => class.type_definition(db),
SubclassOfInner::TypeVar(bound_typevar) => {
Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?))
}
},
Self::TypeAlias(alias) => alias.value_type(db).definition(db),
@@ -6569,13 +6618,11 @@ impl<'db> Type<'db> {
Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)),
Self::ProtocolInstance(protocol) => match protocol.inner {
Protocol::FromClass(class) => Some(TypeDefinition::Class(class.definition(db))),
Protocol::FromClass(class) => class.type_definition(db),
Protocol::Synthesized(_) => None,
},
Self::TypedDict(typed_dict) => {
typed_dict.definition(db).map(TypeDefinition::Class)
}
Self::TypedDict(typed_dict) => typed_dict.type_definition(db),
Self::Union(_) | Self::Intersection(_) => None,
@@ -6656,7 +6703,7 @@ impl<'db> Type<'db> {
}
}
pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option<ClassLiteral<'db>> {
pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option<StaticClassLiteral<'db>> {
match self {
Type::GenericAlias(generic) => Some(generic.origin(db)),
Type::NominalInstance(instance) => {
@@ -8985,7 +9032,12 @@ impl<'db> UnionTypeInstance<'db> {
) -> Result<impl Iterator<Item = Type<'db>> + 'db, InvalidTypeExpressionError<'db>> {
let to_class_literal = |ty: Type<'db>| {
ty.as_nominal_instance()
.map(|instance| Type::ClassLiteral(instance.class(db).class_literal(db).0))
.and_then(|instance| {
instance
.class(db)
.static_class_literal(db)
.map(|(lit, _)| Type::ClassLiteral(lit.into()))
})
.unwrap_or_else(Type::unknown)
};
@@ -11718,7 +11770,7 @@ impl<'db> TypeAliasType<'db> {
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(super) struct MetaclassCandidate<'db> {
metaclass: ClassType<'db>,
explicit_metaclass_of: ClassLiteral<'db>,
explicit_metaclass_of: StaticClassLiteral<'db>,
}
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
@@ -12925,7 +12977,7 @@ impl<'db> TypeGuardLike<'db> for TypeGuardType<'db> {
/// being added to the given class.
pub(super) fn determine_upper_bound<'db>(
db: &'db dyn Db,
class_literal: ClassLiteral<'db>,
class_literal: StaticClassLiteral<'db>,
specialization: Option<Specialization<'db>>,
is_known_base: impl Fn(ClassBase<'db>) -> bool,
) -> Type<'db> {

View File

@@ -392,7 +392,7 @@ impl<'db> BoundSuperType<'db> {
typevar: TypeVarInstance<'db>,
make_owner: fn(BoundTypeVarInstance<'db>, ClassType<'db>) -> SuperOwnerKind<'db>|
-> Result<Type<'db>, BoundSuperError<'db>> {
let pivot_class_literal = pivot_class.into_class().map(|c| c.class_literal(db).0);
let pivot_class_literal = pivot_class.into_class().map(|c| c.class_literal(db));
let mut builder = UnionBuilder::new(db);
for constraint in constraints.elements(db) {
let class = match constraint {
@@ -409,7 +409,7 @@ impl<'db> BoundSuperType<'db> {
| ClassBase::Protocol
| ClassBase::TypedDict => false,
ClassBase::Class(superclass) => {
superclass.class_literal(db).0 == pivot
superclass.class_literal(db) == pivot
}
}) {
return Err(BoundSuperError::FailingConditionCheck {
@@ -627,11 +627,11 @@ impl<'db> BoundSuperType<'db> {
if let Some(pivot_class) = pivot_class.into_class()
&& let Some(owner_class) = owner.into_class(db)
{
let pivot_class = pivot_class.class_literal(db).0;
let pivot_class = pivot_class.class_literal(db);
if !owner_class.iter_mro(db).any(|superclass| match superclass {
ClassBase::Dynamic(_) => true,
ClassBase::Generic | ClassBase::Protocol | ClassBase::TypedDict => false,
ClassBase::Class(superclass) => superclass.class_literal(db).0 == pivot_class,
ClassBase::Class(superclass) => superclass.class_literal(db) == pivot_class,
}) {
return Err(BoundSuperError::FailingConditionCheck {
pivot_class: pivot_class_type,
@@ -748,7 +748,7 @@ impl<'db> BoundSuperType<'db> {
}
};
let (class_literal, _) = class.class_literal(db);
let class_literal = class.class_literal(db);
// TODO properly support super() with generic types
// * requires a fix for https://github.com/astral-sh/ruff/issues/17432
// * also requires understanding how we should handle cases like this:

View File

@@ -352,7 +352,7 @@ pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
.any(|element| is_expandable_type(db, element)),
Tuple::Variable(_) => false,
})
|| enum_metadata(db, class.class_literal(db).0).is_some()
|| enum_metadata(db, class.class_literal(db)).is_some()
}
Type::Union(_) => true,
_ => false,
@@ -403,7 +403,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
};
}
if let Some(enum_members) = enum_member_literals(db, class.class_literal(db).0, None) {
if let Some(enum_members) = enum_member_literals(db, class.class_literal(db), None) {
return Some(enum_members.collect());
}

View File

@@ -1187,11 +1187,6 @@ impl<'db> Bindings<'db> {
}
}
Some(KnownFunction::NamedTuple) => {
overload
.set_return_type(todo_type!("Support for functional `namedtuple`"));
}
_ => {
// Ideally, either the implementation, or exactly one of the overloads
// of the function can have the dataclass_transform decorator applied.

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,13 @@
use crate::Db;
use crate::types::class::CodeGeneratorKind;
use crate::types::generics::{ApplySpecialization, Specialization};
use crate::types::mro::MroIterator;
use crate::types::tuple::TupleType;
use crate::types::{
ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType,
MaterializationKind, MroError, MroIterator, NormalizedVisitor, SpecialFormType, Type,
TypeContext, TypeMapping, todo_type,
MaterializationKind, MroError, NormalizedVisitor, SpecialFormType, Type, TypeContext,
TypeMapping, todo_type,
};
/// Enumeration of the possible kinds of types we allow in class bases.
@@ -245,7 +247,8 @@ impl<'db> ClassBase<'db> {
SpecialFormType::Generic => Some(Self::Generic),
SpecialFormType::NamedTuple => {
let fields = subclass.own_fields(db, None, CodeGeneratorKind::NamedTuple);
let class = subclass.as_static()?;
let fields = class.own_fields(db, None, CodeGeneratorKind::NamedTuple);
Self::try_from_type(
db,
TupleType::heterogeneous(
@@ -309,6 +312,16 @@ impl<'db> ClassBase<'db> {
}
}
/// Return the metaclass of this class base.
pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
match self {
Self::Class(class) => class.metaclass(db),
Self::Dynamic(dynamic) => Type::Dynamic(dynamic),
// TODO: all `Protocol` classes actually have `_ProtocolMeta` as their metaclass.
Self::Protocol | Self::Generic | Self::TypedDict => KnownClass::Type.to_instance(db),
}
}
fn apply_type_mapping_impl<'a>(
self,
db: &'db dyn Db,
@@ -359,7 +372,13 @@ impl<'db> ClassBase<'db> {
pub(super) fn has_cyclic_mro(self, db: &'db dyn Db) -> bool {
match self {
ClassBase::Class(class) => {
let (class_literal, specialization) = class.class_literal(db);
let Some((class_literal, specialization)) = class.static_class_literal(db) else {
// Dynamic classes can't have cyclic MRO since their bases must
// already exist at creation time. Unlike statement classes, we do not
// permit dynamic classes to have forward references in their
// bases list.
return false;
};
class_literal
.try_mro(db, specialization)
.is_err_and(MroError::is_cycle)

View File

@@ -9,7 +9,10 @@ use ty_module_resolver::Module;
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum TypeDefinition<'db> {
Module(Module<'db>),
Class(Definition<'db>),
/// A class created via a `class` statement.
StaticClass(Definition<'db>),
/// A class created dynamically via `type(name, bases, dict)`.
DynamicClass(Definition<'db>),
Function(Definition<'db>),
TypeVar(Definition<'db>),
TypeAlias(Definition<'db>),
@@ -21,7 +24,8 @@ impl TypeDefinition<'_> {
pub fn focus_range(&self, db: &dyn Db) -> Option<FileRange> {
match self {
Self::Module(_) => None,
Self::Class(definition)
Self::StaticClass(definition)
| Self::DynamicClass(definition)
| Self::Function(definition)
| Self::TypeVar(definition)
| Self::TypeAlias(definition)
@@ -40,7 +44,8 @@ impl TypeDefinition<'_> {
let source = source_text(db, file);
Some(FileRange::new(file, TextRange::up_to(source.text_len())))
}
Self::Class(definition)
Self::StaticClass(definition)
| Self::DynamicClass(definition)
| Self::Function(definition)
| Self::TypeVar(definition)
| Self::TypeAlias(definition)

View File

@@ -2,7 +2,7 @@ use super::call::CallErrorKind;
use super::context::InferContext;
use super::mro::DuplicateBaseError;
use super::{
CallArguments, CallDunderError, ClassBase, ClassLiteral, KnownClass,
CallArguments, CallDunderError, ClassBase, ClassLiteral, KnownClass, StaticClassLiteral,
add_inferred_python_version_hint_to_diagnostic,
};
use crate::diagnostic::did_you_mean;
@@ -2800,12 +2800,12 @@ pub(super) fn report_implicit_return_type(
"Only classes that directly inherit from `typing.Protocol` \
or `typing_extensions.Protocol` are considered protocol classes",
);
sub_diagnostic.annotate(
Annotation::primary(class.header_span(db)).message(format_args!(
sub_diagnostic.annotate(Annotation::primary(class.definition_span(db)).message(
format_args!(
"`Protocol` not present in `{class}`'s immediate bases",
class = class.name(db)
)),
);
),
));
diagnostic.sub(sub_diagnostic);
diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#");
@@ -2974,7 +2974,7 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast:
pub(crate) fn report_instance_layout_conflict(
context: &InferContext,
class: ClassLiteral,
class: StaticClassLiteral,
node: &ast::StmtClassDef,
disjoint_bases: &IncompatibleBases,
) {
@@ -3009,7 +3009,7 @@ pub(crate) fn report_instance_layout_conflict(
let span = context.span(&node.bases()[*node_index]);
let mut annotation = Annotation::secondary(span.clone());
if disjoint_base.class == *originating_base {
if *originating_base == disjoint_base.class {
match disjoint_base.kind {
DisjointBaseKind::DefinesSlots => {
annotation = annotation.message(format_args!(
@@ -3060,6 +3060,32 @@ pub(crate) fn report_instance_layout_conflict(
diagnostic.sub(subdiagnostic);
}
/// Emit a diagnostic for a metaclass conflict where both conflicting metaclasses
/// are inherited from base classes.
pub(super) fn report_conflicting_metaclass_from_bases(
context: &InferContext,
node: AnyNodeRef,
class_name: &str,
metaclass1: ClassType,
base1: impl std::fmt::Display,
metaclass2: ClassType,
base2: impl std::fmt::Display,
) {
let Some(builder) = context.report_lint(&CONFLICTING_METACLASS, node) else {
return;
};
let db = context.db();
builder.into_diagnostic(format_args!(
"The metaclass of a derived class (`{class_name}`) \
must be a subclass of the metaclasses of all its bases, \
but `{metaclass1}` (metaclass of base class `{base1}`) \
and `{metaclass2}` (metaclass of base class `{base2}`) \
have no subclass relationship",
metaclass1 = metaclass1.name(db),
metaclass2 = metaclass2.name(db),
));
}
/// Information regarding the conflicting disjoint bases a class is inferred to have in its MRO.
///
/// For each disjoint base, we record information about which element in the class's bases list
@@ -3107,11 +3133,10 @@ impl<'db> IncompatibleBases<'db> {
.keys()
.filter(|other_base| other_base != disjoint_base)
.all(|other_base| {
!disjoint_base.class.is_subclass_of(
db,
None,
other_base.class.default_specialization(db),
)
!disjoint_base
.class
.default_specialization(db)
.is_subclass_of(db, other_base.class.default_specialization(db))
})
})
.map(|(base, info)| (*base, *info))
@@ -3232,9 +3257,9 @@ pub(crate) fn report_bad_argument_to_protocol_interface(
class.name(db)
),
);
class_def_diagnostic.annotate(Annotation::primary(
class.class_literal(db).0.header_span(db),
));
if let Some((class_literal, _)) = class.static_class_literal(db) {
class_def_diagnostic.annotate(Annotation::primary(class_literal.header_span(db)));
}
diagnostic.sub(class_def_diagnostic);
}
@@ -3293,7 +3318,7 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol(
),
);
class_def_diagnostic.annotate(
Annotation::primary(protocol.header_span(db))
Annotation::primary(protocol.definition_span(db))
.message(format_args!("`{class_name}` declared here")),
);
diagnostic.sub(class_def_diagnostic);
@@ -3324,7 +3349,7 @@ pub(crate) fn report_attempted_protocol_instantiation(
format_args!("Protocol classes cannot be instantiated"),
);
class_def_diagnostic.annotate(
Annotation::primary(protocol.header_span(db))
Annotation::primary(protocol.definition_span(db))
.message(format_args!("`{class_name}` declared as a protocol here")),
);
diagnostic.sub(class_def_diagnostic);
@@ -3412,7 +3437,7 @@ pub(crate) fn report_undeclared_protocol_member(
leads to an ambiguous interface",
);
class_def_diagnostic.annotate(
Annotation::primary(protocol_class.header_span(db))
Annotation::primary(protocol_class.definition_span(db))
.message(format_args!("`{class_name}` declared as a protocol here",)),
);
diagnostic.sub(class_def_diagnostic);
@@ -3425,7 +3450,7 @@ pub(crate) fn report_undeclared_protocol_member(
pub(crate) fn report_duplicate_bases(
context: &InferContext,
class: ClassLiteral,
class: StaticClassLiteral,
duplicate_base_error: &DuplicateBaseError,
bases_list: &[ast::Expr],
) {
@@ -3472,7 +3497,7 @@ pub(crate) fn report_invalid_or_unsupported_base(
context: &InferContext,
base_node: &ast::Expr,
base_type: Type,
class: ClassLiteral,
class: StaticClassLiteral,
) {
let db = context.db();
let instance_of_type = KnownClass::Type.to_instance(db);
@@ -3582,7 +3607,7 @@ fn report_unsupported_base(
context: &InferContext,
base_node: &ast::Expr,
base_type: Type,
class: ClassLiteral,
class: StaticClassLiteral,
) {
let Some(builder) = context.report_lint(&UNSUPPORTED_BASE, base_node) else {
return;
@@ -3605,7 +3630,7 @@ fn report_invalid_base<'ctx, 'db>(
context: &'ctx InferContext<'db, '_>,
base_node: &ast::Expr,
base_type: Type<'db>,
class: ClassLiteral<'db>,
class: StaticClassLiteral<'db>,
) -> Option<LintDiagnosticGuard<'ctx, 'db>> {
let builder = context.report_lint(&INVALID_BASE, base_node)?;
let mut diagnostic = builder.into_diagnostic(format_args!(
@@ -3701,7 +3726,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'db>(
context: &InferContext<'db, '_>,
class: ClassLiteral<'db>,
class: StaticClassLiteral<'db>,
(field, field_def): (&str, Option<Definition<'db>>),
(field_with_default, field_with_default_def): &(Name, Option<Definition<'db>>),
) {
@@ -3750,7 +3775,7 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'
pub(super) fn report_named_tuple_field_with_leading_underscore<'db>(
context: &InferContext<'db, '_>,
class: ClassLiteral<'db>,
class: StaticClassLiteral<'db>,
field_name: &str,
field_definition: Option<Definition<'db>>,
) {
@@ -3874,7 +3899,7 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>(
pub(crate) fn report_invalid_type_param_order<'db>(
context: &InferContext<'db, '_>,
class: ClassLiteral<'db>,
class: StaticClassLiteral<'db>,
node: &ast::StmtClassDef,
typevar_with_default: TypeVarInstance<'db>,
invalid_later_typevars: &[TypeVarInstance<'db>],
@@ -3959,7 +3984,7 @@ pub(crate) fn report_invalid_type_param_order<'db>(
pub(crate) fn report_rebound_typevar<'db>(
context: &InferContext<'db, '_>,
typevar_name: &ast::name::Name,
class: ClassLiteral<'db>,
class: StaticClassLiteral<'db>,
class_node: &ast::StmtClassDef,
other_typevar: BoundTypeVarInstance<'db>,
) {
@@ -4034,10 +4059,8 @@ pub(super) fn report_invalid_method_override<'db>(
let superclass_name = superclass.name(db);
let overridden_method = if class_name == superclass_name {
format!(
"{superclass}.{member}",
superclass = superclass.qualified_name(db),
)
let qualified_name = superclass.qualified_name(db);
format!("{qualified_name}.{member}")
} else {
format!("{superclass_name}.{member}")
};
@@ -4090,7 +4113,10 @@ pub(super) fn report_invalid_method_override<'db>(
);
}
let superclass_scope = superclass.class_literal(db).0.body_scope(db);
let Some((superclass_literal, _)) = superclass.static_class_literal(db) else {
return;
};
let superclass_scope = superclass_literal.body_scope(db);
match superclass_method_kind {
MethodKind::NotSynthesized => {
@@ -4157,7 +4183,7 @@ pub(super) fn report_invalid_method_override<'db>(
};
sub.annotate(
Annotation::primary(superclass.header_span(db))
Annotation::primary(superclass.definition_span(db))
.message(format_args!("Definition of `{superclass_name}`")),
);
diagnostic.sub(sub);
@@ -4277,9 +4303,10 @@ pub(super) fn report_overridden_final_method<'db>(
// but you'd want to delete the `@my_property.deleter` as well as the getter and the deleter,
// and we don't model property deleters at all right now.
if let Type::FunctionLiteral(function) = subclass_type {
let class_node = subclass
.class_literal(db)
.0
let Some((subclass_literal, _)) = subclass.static_class_literal(db) else {
return;
};
let class_node = subclass_literal
.body_scope(db)
.node(db)
.expect_class()
@@ -4577,9 +4604,9 @@ fn report_unsupported_binary_operation_impl<'a>(
pub(super) fn report_bad_frozen_dataclass_inheritance<'db>(
context: &InferContext<'db, '_>,
class: ClassLiteral<'db>,
class: StaticClassLiteral<'db>,
class_node: &ast::StmtClassDef,
base_class: ClassLiteral<'db>,
base_class: StaticClassLiteral<'db>,
base_class_node: &ast::Expr,
base_class_params: DataclassFlags,
) {

View File

@@ -432,8 +432,12 @@ impl<'db> TypeVisitor<'db> for AmbiguousClassCollector<'db> {
fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {
match ty {
Type::ClassLiteral(class) => self.record_class(db, class),
Type::EnumLiteral(literal) => self.record_class(db, literal.enum_class(db)),
Type::GenericAlias(alias) => self.record_class(db, alias.origin(db)),
Type::EnumLiteral(literal) => {
self.record_class(db, literal.enum_class(db));
}
Type::GenericAlias(alias) => {
self.record_class(db, ClassLiteral::Static(alias.origin(db)));
}
// Visit the class (as if it were a nominal-instance type)
// rather than the protocol members, if it is a class-based protocol.
// (For the purposes of displaying the type, we'll use the class name.)
@@ -558,13 +562,15 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> {
let ty = Type::ClassLiteral(self.class);
if qualification_level.is_some() {
write!(f.with_type(ty), "{}", self.class.qualified_name(self.db))?;
let qualified_name = self.class.qualified_name(self.db);
write!(f.with_type(ty), "{qualified_name}")?;
} else {
write!(f.with_type(ty), "{}", self.class.name(self.db))?;
}
if qualification_level == Some(&QualificationLevel::FileAndLineNumber) {
let file = self.class.file(self.db);
let class_offset = self.class.header_range(self.db).start();
let path = file.path(self.db);
let path = match path {
FilePath::System(path) => Cow::Owned(FilePath::System(
@@ -575,7 +581,6 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> {
FilePath::Vendored(_) | FilePath::SystemVirtual(_) => Cow::Borrowed(path),
};
let line_index = line_index(self.db, file);
let class_offset = self.class.header_range(self.db).start();
let line_number = line_index.line_index(class_offset);
f.set_invalid_type_annotation();
write!(f, " @ {path}:{line_number}")?;
@@ -1287,7 +1292,7 @@ impl<'db> GenericAlias<'db> {
settings: DisplaySettings<'db>,
) -> DisplayGenericAlias<'db> {
DisplayGenericAlias {
origin: self.origin(db),
origin: ClassLiteral::Static(self.origin(db)),
specialization: self.specialization(db),
db,
settings,

View File

@@ -1,5 +1,6 @@
use ruff_python_ast::name::Name;
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use crate::{
Db, FxIndexMap,
@@ -9,7 +10,7 @@ use crate::{
semantic_index::{place_table, use_def_map},
types::{
ClassBase, ClassLiteral, DynamicType, EnumLiteralType, KnownClass, MemberLookupPolicy,
Type, TypeQualifiers,
StaticClassLiteral, Type, TypeQualifiers,
},
};
@@ -54,6 +55,24 @@ pub(crate) fn enum_metadata<'db>(
db: &'db dyn Db,
class: ClassLiteral<'db>,
) -> Option<EnumMetadata<'db>> {
let class = match class {
ClassLiteral::Static(class) => class,
ClassLiteral::Dynamic(..) => {
// Classes created via `type` cannot be enums; the following fails at runtime:
// ```python
// import enum
//
// class BaseEnum(enum.Enum):
// pass
//
// MyEnum = type("MyEnum", (BaseEnum,), {"A": 1, "B": 2})
// ```
// TODO: Add a diagnostic for including an enum in a `type(...)` call.
return None;
}
ClassLiteral::DynamicNamedTuple(..) => return None,
};
// This is a fast path to avoid traversing the MRO of known classes
if class
.known(db)
@@ -139,42 +158,43 @@ pub(crate) fn enum_metadata<'db>(
auto_counter += 1;
// `StrEnum`s have different `auto()` behaviour to enums inheriting from `(str, Enum)`
let auto_value_ty = if Type::ClassLiteral(class)
.is_subtype_of(db, KnownClass::StrEnum.to_subclass_of(db))
{
Type::string_literal(db, &name.to_lowercase())
} else {
let custom_mixins: smallvec::SmallVec<[Option<KnownClass>; 1]> =
class
.iter_mro(db, None)
.skip(1)
.filter_map(ClassBase::into_class)
.filter(|class| {
!Type::from(*class).is_subtype_of(
db,
KnownClass::Enum.to_subclass_of(db),
)
})
.map(|class| class.known(db))
.filter(|class| {
!matches!(class, Some(KnownClass::Object))
})
.collect();
// `IntEnum`s have the same `auto()` behaviour to enums inheriting from `(int, Enum)`,
// and `IntEnum`s also have `int` in their MROs, so both cases are handled here.
//
// In general, the `auto()` behaviour for enums with non-`int` mixins is hard to predict,
// so we fall back to `Any` in those cases.
if matches!(
custom_mixins.as_slice(),
[] | [Some(KnownClass::Int)]
) {
Type::IntLiteral(auto_counter)
let auto_value_ty =
if Type::ClassLiteral(ClassLiteral::Static(class))
.is_subtype_of(db, KnownClass::StrEnum.to_subclass_of(db))
{
Type::string_literal(db, &name.to_lowercase())
} else {
Type::any()
}
};
let custom_mixins: SmallVec<[Option<KnownClass>; 1]> =
class
.iter_mro(db, None)
.skip(1)
.filter_map(ClassBase::into_class)
.filter(|class| {
!Type::from(*class).is_subtype_of(
db,
KnownClass::Enum.to_subclass_of(db),
)
})
.map(|class| class.known(db))
.filter(|class| {
!matches!(class, Some(KnownClass::Object))
})
.collect();
// `IntEnum`s have the same `auto()` behaviour to enums inheriting from `(int, Enum)`,
// and `IntEnum`s also have `int` in their MROs, so both cases are handled here.
//
// In general, the `auto()` behaviour for enums with non-`int` mixins is hard to predict,
// so we fall back to `Any` in those cases.
if matches!(
custom_mixins.as_slice(),
[] | [Some(KnownClass::Int)]
) {
Type::IntLiteral(auto_counter)
} else {
Type::any()
}
};
Some(auto_value_ty)
}
@@ -308,8 +328,12 @@ pub(crate) fn is_enum_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
///
/// This is a lighter-weight check than `enum_metadata`, which additionally
/// verifies that the class has members.
pub(crate) fn is_enum_class_by_inheritance<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> bool {
Type::ClassLiteral(class).is_subtype_of(db, KnownClass::Enum.to_subclass_of(db))
pub(crate) fn is_enum_class_by_inheritance<'db>(
db: &'db dyn Db,
class: StaticClassLiteral<'db>,
) -> bool {
Type::ClassLiteral(ClassLiteral::Static(class))
.is_subtype_of(db, KnownClass::Enum.to_subclass_of(db))
|| class
.metaclass(db)
.is_subtype_of(db, KnownClass::EnumType.to_subclass_of(db))

View File

@@ -564,10 +564,11 @@ impl<'db> OverloadLiteral<'db> {
let index = semantic_index(db, scope_id.file(db));
let class = nearest_enclosing_class(db, index, scope_id).unwrap();
let typing_self = typing_self(db, scope_id, typevar_binding_context, class).expect(
"We should always find the surrounding class \
let typing_self = typing_self(db, scope_id, typevar_binding_context, class.into())
.expect(
"We should always find the surrounding class \
for an implicit self: Self annotation",
);
);
if self.is_classmethod(db) {
Some(SubclassOfType::from(
@@ -1227,10 +1228,7 @@ fn is_instance_truthiness<'db>(
.class(db)
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|c| match c {
ClassType::Generic(c) => c.origin(db) == class,
ClassType::NonGeneric(c) => c == class,
})
.any(|c| c.class_literal(db) == class)
{
return true;
}
@@ -2023,7 +2021,7 @@ impl KnownFunction {
if !class.has_ordering_method_in_mro(db) {
report_invalid_total_ordering_call(
context,
class.class_literal(db).0,
class.class_literal(db),
call_expression,
);
}

View File

@@ -96,7 +96,7 @@ pub(crate) fn typing_self<'db>(
let identity = TypeVarIdentity::new(
db,
ast::name::Name::new_static("Self"),
Some(class.definition(db)),
class.definition(db),
TypeVarKind::TypingSelf,
);
let bounds = TypeVarBoundOrConstraints::UpperBound(Type::instance(

View File

@@ -8,7 +8,8 @@ use crate::semantic_index::{attribute_scopes, global_scope, semantic_index, use_
use crate::types::call::{CallArguments, MatchedArgument};
use crate::types::signatures::{ParameterKind, Signature};
use crate::types::{
CallDunderError, CallableTypes, ClassBase, KnownUnion, Type, TypeContext, UnionType,
CallDunderError, CallableTypes, ClassBase, ClassLiteral, ClassType, KnownUnion, Type,
TypeContext, UnionType,
};
use crate::{Db, DisplaySettings, HasType, SemanticModel};
use ruff_db::files::FileRange;
@@ -168,9 +169,9 @@ pub fn definitions_for_name<'db>(
// instead of `int` (hover only shows the docstring of the first definition).
.rev()
.filter_map(|ty| ty.as_nominal_instance())
.map(|instance| {
let definition = instance.class_literal(db).definition(db);
ResolvedDefinition::Definition(definition)
.filter_map(|instance| {
let definition = instance.class_literal(db).definition(db)?;
Some(ResolvedDefinition::Definition(definition))
})
.collect();
}
@@ -266,7 +267,10 @@ pub fn definitions_for_attribute<'db>(
let class_literal = match meta_type {
Type::ClassLiteral(class_literal) => class_literal,
Type::SubclassOf(subclass) => match subclass.subclass_of().into_class(db) {
Some(cls) => cls.class_literal(db).0,
Some(cls) => match cls.static_class_literal(db) {
Some((lit, _)) => ClassLiteral::Static(lit),
None => continue,
},
None => continue,
},
_ => continue,
@@ -274,9 +278,9 @@ pub fn definitions_for_attribute<'db>(
// Walk the MRO: include class and its ancestors, but stop when we find a match
'scopes: for ancestor in class_literal
.iter_mro(db, None)
.iter_mro(db)
.filter_map(ClassBase::into_class)
.map(|cls| cls.class_literal(db).0)
.filter_map(|cls: ClassType<'db>| cls.static_class_literal(db).map(|(lit, _)| lit))
{
let class_scope = ancestor.body_scope(db);
let class_place_table = crate::semantic_index::place_table(db, class_scope);

View File

@@ -53,7 +53,8 @@ use crate::types::function::FunctionType;
use crate::types::generics::Specialization;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
ClassLiteral, KnownClass, Truthiness, Type, TypeAndQualifiers, declaration_type,
ClassLiteral, KnownClass, StaticClassLiteral, Truthiness, Type, TypeAndQualifiers,
declaration_type,
};
use crate::unpack::Unpack;
use builder::TypeInferenceBuilder;
@@ -465,7 +466,7 @@ pub(crate) fn nearest_enclosing_class<'db>(
db: &'db dyn Db,
semantic: &SemanticIndex<'db>,
scope: ScopeId,
) -> Option<ClassLiteral<'db>> {
) -> Option<StaticClassLiteral<'db>> {
semantic
.ancestor_scopes(scope.file_scope_id(db))
.find_map(|(_, ancestor_scope)| {
@@ -474,6 +475,7 @@ pub(crate) fn nearest_enclosing_class<'db>(
declaration_type(db, definition)
.inner_type()
.as_class_literal()
.and_then(ClassLiteral::as_static)
})
}

View File

@@ -53,32 +53,38 @@ use crate::semantic_index::{
use crate::subscript::{PyIndex, PySlice};
use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex};
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator};
use crate::types::class::DynamicNamedTupleLiteral;
use crate::types::class::{
ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, DynamicMetaclassConflict, FieldKind,
MetaclassErrorKind, MethodDecorator,
};
use crate::types::context::{InNoTypeCheck, InferContext};
use crate::types::cyclic::CycleDetector;
use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY,
INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS,
INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY,
INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE,
INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL,
INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE,
DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT,
INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS,
INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE,
INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC,
INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases,
NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL,
UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
UNSUPPORTED_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict,
report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
report_instance_layout_conflict, report_invalid_arguments_to_annotated,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_invalid_exception_tuple_caught,
report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_total_ordering,
report_conflicting_metaclass_from_bases, report_duplicate_bases, report_implicit_return_type,
report_index_out_of_bounds, report_instance_layout_conflict,
report_invalid_arguments_to_annotated, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_exception_caught,
report_invalid_exception_cause, report_invalid_exception_raised,
report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type,
report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base,
report_invalid_return_type, report_invalid_total_ordering,
report_invalid_type_checking_constant, report_invalid_type_param_order,
report_named_tuple_field_with_leading_underscore,
report_namedtuple_field_without_default_after_field_with_default, report_not_subscriptable,
@@ -96,7 +102,7 @@ use crate::types::generics::{
};
use crate::types::infer::nearest_enclosing_function;
use crate::types::instance::SliceLiteral;
use crate::types::mro::MroErrorKind;
use crate::types::mro::{DynamicMroErrorKind, MroErrorKind};
use crate::types::newtype::NewType;
use crate::types::subclass_of::SubclassOfInner;
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleType};
@@ -107,12 +113,12 @@ use crate::types::typed_dict::{
use crate::types::visitor::any_over_type;
use crate::types::{
BoundTypeVarIdentity, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType,
CallableTypeKind, ClassLiteral, ClassType, DataclassParams, DynamicType, InternedType,
IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion,
LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType,
SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers,
TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
CallableTypeKind, ClassType, DataclassParams, DynamicType, InternedType, IntersectionBuilder,
IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard,
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter,
ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType,
TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext,
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types,
todo_type,
@@ -578,27 +584,32 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
if self.db().should_check_file(self.file()) {
self.check_class_definitions();
self.check_static_class_definitions();
self.check_overloaded_functions(node);
}
}
/// Iterate over all class definitions to check that the definition will not cause an exception
/// to be raised at runtime. This needs to be done after most other types in the scope have been
/// inferred, due to the fact that base classes can be deferred. If it looks like a class
/// definition is invalid in some way, issue a diagnostic.
/// Iterate over all static class definitions (created using `class` statements) to check that
/// the definition will not cause an exception to be raised at runtime. This needs to be done
/// after most other types in the scope have been inferred, due to the fact that base classes
/// can be deferred. If it looks like a class definition is invalid in some way, issue a
/// diagnostic.
///
/// Note: Dynamic classes created via `type()` calls are checked separately during type
/// inference of the call expression.
///
/// Among the things we check for in this method are whether Python will be able to determine a
/// consistent "[method resolution order]" and [metaclass] for each class.
///
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
/// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses
fn check_class_definitions(&mut self) {
fn check_static_class_definitions(&mut self) {
let class_definitions = self.declarations.iter().filter_map(|(definition, ty)| {
// Filter out class literals that result from imports
if let DefinitionKind::Class(class) = definition.kind(self.db()) {
ty.inner_type()
.as_class_literal()
.and_then(ClassLiteral::as_static)
.map(|class_literal| (class_literal, class.node(self.module())))
} else {
None
@@ -625,7 +636,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
continue;
}
let is_named_tuple = CodeGeneratorKind::NamedTuple.matches(self.db(), class, None);
let is_named_tuple =
CodeGeneratorKind::NamedTuple.matches(self.db(), class.into(), None);
// (2) If it's a `NamedTuple` class, check that no field without a default value
// appears after a field with a default value.
@@ -679,6 +691,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
base_class,
Type::SpecialForm(SpecialFormType::NamedTuple)
| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_))
| Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(_))
)
{
if let Some(builder) = self
@@ -729,7 +742,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
};
if let Some(disjoint_base) = base_class.nearest_disjoint_base(self.db()) {
disjoint_bases.insert(disjoint_base, i, base_class.class_literal(self.db()).0);
disjoint_bases.insert(disjoint_base, i, base_class.class_literal(self.db()));
}
if is_protocol
@@ -760,12 +773,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
let (base_class_literal, _) = base_class.class_literal(self.db());
if let (Some(base_params), Some(class_params)) = (
base_class_literal.dataclass_params(self.db()),
class.dataclass_params(self.db()),
) {
if let Some((base_class_literal, _)) = base_class.static_class_literal(self.db())
&& let (Some(base_params), Some(class_params)) = (
base_class_literal.dataclass_params(self.db()),
class.dataclass_params(self.db()),
)
{
let base_params = base_params.flags(self.db());
let class_is_frozen = class_params.flags(self.db()).is_frozen();
@@ -864,7 +877,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
function.is_known(self.db(), KnownFunction::TotalOrdering)
})
}) {
report_invalid_total_ordering(&self.context, class, decorator);
report_invalid_total_ordering(
&self.context,
ClassLiteral::Static(class),
decorator,
);
}
}
@@ -915,35 +932,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
},
candidate1_is_base_class,
} => {
if let Some(builder) =
if *candidate1_is_base_class {
report_conflicting_metaclass_from_bases(
&self.context,
class_node.into(),
class.name(self.db()),
*metaclass1,
class1.name(self.db()),
*metaclass2,
class2.name(self.db()),
);
} else if let Some(builder) =
self.context.report_lint(&CONFLICTING_METACLASS, class_node)
{
if *candidate1_is_base_class {
builder.into_diagnostic(format_args!(
"The metaclass of a derived class (`{class}`) \
must be a subclass of the metaclasses of all its bases, \
but `{metaclass1}` (metaclass of base class `{base1}`) \
and `{metaclass2}` (metaclass of base class `{base2}`) \
have no subclass relationship",
class = class.name(self.db()),
metaclass1 = metaclass1.name(self.db()),
base1 = class1.name(self.db()),
metaclass2 = metaclass2.name(self.db()),
base2 = class2.name(self.db()),
));
} else {
builder.into_diagnostic(format_args!(
"The metaclass of a derived class (`{class}`) \
builder.into_diagnostic(format_args!(
"The metaclass of a derived class (`{class}`) \
must be a subclass of the metaclasses of all its bases, \
but `{metaclass_of_class}` (metaclass of `{class}`) \
and `{metaclass_of_base}` (metaclass of base class `{base}`) \
have no subclass relationship",
class = class.name(self.db()),
metaclass_of_class = metaclass1.name(self.db()),
metaclass_of_base = metaclass2.name(self.db()),
base = class2.name(self.db()),
));
}
class = class.name(self.db()),
metaclass_of_class = metaclass1.name(self.db()),
metaclass_of_base = metaclass2.name(self.db()),
base = class2.name(self.db()),
));
}
}
}
@@ -1030,7 +1042,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// (7) Check that a dataclass does not have more than one `KW_ONLY`.
if let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) =
CodeGeneratorKind::from_class(self.db(), class, None)
CodeGeneratorKind::from_class(self.db(), class.into(), None)
{
let specialization = None;
@@ -2979,7 +2991,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::SpecialForm(SpecialFormType::NamedTuple)
}
(None, "Any") if in_typing_module() => Type::SpecialForm(SpecialFormType::Any),
_ => Type::from(ClassLiteral::new(
_ => Type::from(StaticClassLiteral::new(
self.db(),
name.id.clone(),
body_scope,
@@ -4285,10 +4297,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if object_ty
.as_nominal_instance()
.and_then(|instance| {
file_to_module(
db,
instance.class(db).class_literal(db).0.file(db),
)
instance.class(db).static_class_literal(db)
})
.and_then(|(class_literal, _)| {
file_to_module(db, class_literal.file(db))
})
.and_then(|module| module.search_path(db))
.is_some_and(ty_module_resolver::SearchPath::is_first_party)
@@ -4625,9 +4637,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
// Check if class-level attribute already has a value
{
let class_definition = class_ty.class_literal(db).0;
let class_scope_id = class_definition.body_scope(db).file_scope_id(db);
if let Some((class_literal, _)) = class_ty.static_class_literal(db) {
let class_scope_id = class_literal.body_scope(db).file_scope_id(db);
let place_table = builder.index.place_table(class_scope_id);
if let Some(symbol) = place_table.symbol_by_name(attribute) {
@@ -5399,6 +5410,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Some(KnownClass::NewType) => {
self.infer_newtype_expression(target, call_expr, definition)
}
Some(KnownClass::Type) => {
// Try to extract the dynamic class with definition.
// This returns `None` if it's not a three-arg call to `type()`,
// signalling that we must fall back to normal call inference.
self.infer_dynamic_type_expression(call_expr, Some(definition))
.unwrap_or_else(|| {
self.infer_call_expression_impl(call_expr, callable_type, tcx)
})
}
Some(_) | None => {
self.infer_call_expression_impl(call_expr, callable_type, tcx)
}
@@ -6002,6 +6022,619 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
/// Try to infer a 3-argument `type(name, bases, dict)` call expression, capturing the definition.
///
/// This is called when we detect a `type()` call in assignment context and want to
/// associate the resulting `DynamicClassLiteral` with its definition for go-to-definition.
///
/// Returns `None` if any keywords were provided or the number of arguments is not three,
/// signalling that no types were stored for any AST sub-expressions and that we should
/// therefore fallback to normal call binding for error reporting.
fn infer_dynamic_type_expression(
&mut self,
call_expr: &ast::ExprCall,
definition: Option<Definition<'db>>,
) -> Option<Type<'db>> {
let db = self.db();
let ast::Arguments {
args,
keywords,
range: _,
node_index: _,
} = &call_expr.arguments;
if !keywords.is_empty() {
return None;
}
let [name_arg, bases_arg, namespace_arg] = &**args else {
return None;
};
// Infer the argument types.
let name_type = self.infer_expression(name_arg, TypeContext::default());
let bases_type = self.infer_expression(bases_arg, TypeContext::default());
// Extract members from the namespace dict (third argument).
// Infer the whole dict first to avoid double-inferring individual values.
let namespace_type = self.infer_expression(namespace_arg, TypeContext::default());
let (members, has_dynamic_namespace): (Box<[(ast::name::Name, Type<'db>)]>, bool) =
if let ast::Expr::Dict(dict) = namespace_arg {
// Check if all keys are string literal types. If any key is not a string literal
// type or is missing (spread), the namespace is considered dynamic.
let all_keys_are_string_literals = dict.items.iter().all(|item| {
item.key
.as_ref()
.is_some_and(|k| matches!(self.expression_type(k), Type::StringLiteral(_)))
});
let members = dict
.items
.iter()
.filter_map(|item| {
// Only extract items with string literal keys.
let key_expr = item.key.as_ref()?;
let key_name = match self.expression_type(key_expr) {
Type::StringLiteral(s) => ast::name::Name::new(s.value(db)),
_ => return None,
};
// Get the already-inferred type from when we inferred the dict above.
let value_ty = self.expression_type(&item.value);
Some((key_name, value_ty))
})
.collect();
(members, !all_keys_are_string_literals)
} else if let Type::TypedDict(typed_dict) = namespace_type {
// Namespace is a TypedDict instance. Extract known keys as members.
// TypedDicts are "open" (can have additional string keys), so this
// is still a dynamic namespace for unknown attributes.
let members: Box<[(ast::name::Name, Type<'db>)]> = typed_dict
.items(db)
.iter()
.map(|(name, field)| (name.clone(), field.declared_ty))
.collect();
(members, true)
} else {
// Namespace is not a dict literal, so it's dynamic.
(Box::new([]), true)
};
if !matches!(namespace_type, Type::TypedDict(_))
&& !namespace_type.is_assignable_to(
db,
KnownClass::Dict
.to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]),
)
&& let Some(builder) = self
.context
.report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg)
{
let mut diagnostic = builder
.into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`");
diagnostic.set_primary_message(format_args!(
"Expected `dict[str, Any]`, found `{}`",
namespace_type.display(db)
));
}
// Extract name and base classes.
let name = if let Type::StringLiteral(literal) = name_type {
ast::name::Name::new(literal.value(db))
} else {
if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db))
&& let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg)
{
let mut diagnostic =
builder.into_diagnostic("Invalid argument to parameter 1 (`name`) of `type()`");
diagnostic.set_primary_message(format_args!(
"Expected `str`, found `{}`",
name_type.display(db)
));
}
ast::name::Name::new_static("<unknown>")
};
let bases = self.extract_dynamic_type_bases(bases_arg, bases_type, &name);
let file = self.file();
let file_scope = self.scope().file_scope_id(db);
let node_index = call_expr.node_index().load();
let dynamic_class = DynamicClassLiteral::new(
db,
name,
bases,
members,
file,
file_scope,
node_index,
definition,
has_dynamic_namespace,
None,
);
// Check for MRO errors.
if let Err(error) = dynamic_class.try_mro(db) {
match error.reason() {
DynamicMroErrorKind::DuplicateBases(duplicates) => {
if let Some(builder) = self.context.report_lint(&DUPLICATE_BASE, call_expr) {
builder.into_diagnostic(format_args!(
"Duplicate base class{maybe_s} {dupes} in class `{class}`",
maybe_s = if duplicates.len() == 1 { "" } else { "es" },
dupes = duplicates
.iter()
.map(|base: &ClassBase<'_>| base.display(db))
.join(", "),
class = dynamic_class.name(db),
));
}
}
DynamicMroErrorKind::UnresolvableMro => {
if let Some(builder) = self.context.report_lint(&INCONSISTENT_MRO, call_expr) {
builder.into_diagnostic(format_args!(
"Cannot create a consistent method resolution order (MRO) \
for class `{}` with bases `[{}]`",
dynamic_class.name(db),
dynamic_class
.bases(db)
.iter()
.map(|base| base.display(db))
.join(", ")
));
}
}
}
}
// Check for metaclass conflicts.
if let Err(DynamicMetaclassConflict {
metaclass1,
base1,
metaclass2,
base2,
}) = dynamic_class.try_metaclass(db)
{
report_conflicting_metaclass_from_bases(
&self.context,
call_expr.into(),
dynamic_class.name(db),
metaclass1,
base1.display(db),
metaclass2,
base2.display(db),
);
}
Some(Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)))
}
/// Try to infer a `typing.NamedTuple(typename, fields)` or `collections.namedtuple(typename, field_names)` call.
///
/// Returns `None` if the call doesn't match a namedtuple pattern, signalling that
/// we should fall back to normal call binding.
#[allow(clippy::type_complexity)]
fn infer_namedtuple_call_expression(
&mut self,
call_expr: &ast::ExprCall,
callable_type: Type<'db>,
definition: Option<Definition<'db>>,
) -> Option<Type<'db>> {
let db = self.db();
let ast::Arguments {
args,
keywords,
range: _,
node_index: _,
} = &call_expr.arguments;
// Check if this is a `typing.NamedTuple` or `collections.namedtuple` call.
let is_typing_namedtuple = matches!(
callable_type,
Type::SpecialForm(SpecialFormType::NamedTuple)
);
let is_collections_namedtuple = callable_type
.as_function_literal()
.and_then(|f| f.known(db))
== Some(KnownFunction::NamedTuple);
if !is_typing_namedtuple && !is_collections_namedtuple {
return None;
}
// Need at least typename and fields/field_names.
if args.len() < 2 {
return None;
}
let name_arg = &args[0];
let fields_arg = &args[1];
// Infer name argument type.
let name_type = self.infer_expression(name_arg, TypeContext::default());
// Infer keyword arguments.
let mut defaults_count = 0usize;
let mut rename_type = None;
for kw in keywords {
if let Some(arg) = &kw.arg {
match arg.id.as_str() {
"defaults" => {
// First try to retrieve the count from the AST (for list and tuple literals).
defaults_count = match &kw.value {
ast::Expr::List(list) => list.elts.len(),
ast::Expr::Tuple(tuple) => tuple.elts.len(),
_ => {
// Fall back to inferring the type.
let ty = self.infer_expression(&kw.value, TypeContext::default());
ty.exact_tuple_instance_spec(db)
.and_then(|spec| spec.len().maximum())
.unwrap_or(0)
}
};
// Make sure to infer list and tuple elements.
if let ast::Expr::List(list) = &kw.value {
for elt in &list.elts {
self.infer_expression(elt, TypeContext::default());
}
} else if let ast::Expr::Tuple(tuple) = &kw.value {
for elt in &tuple.elts {
self.infer_expression(elt, TypeContext::default());
}
}
}
"rename" => {
rename_type =
Some(self.infer_expression(&kw.value, TypeContext::default()));
}
_ => {
self.infer_expression(&kw.value, TypeContext::default());
}
}
} else {
self.infer_expression(&kw.value, TypeContext::default());
}
}
// Extract name.
let name = if let Type::StringLiteral(literal) = name_type {
ast::name::Name::new(literal.value(db))
} else {
// Name must be a string literal for us to synthesize a proper type.
return None;
};
// Handle fields based on which namedtuple variant.
let (fields, has_known_fields): (
Box<[(ast::name::Name, Type<'db>, Option<Type<'db>>)]>,
bool,
) = if is_typing_namedtuple {
// `typing.NamedTuple`: `fields` is a list or tuple of (name, type) pairs.
// First try to extract from the AST directly (for list or tuple literals).
if let Some(fields) = self.extract_typing_namedtuple_fields_from_ast(fields_arg) {
(fields, true)
} else {
// Otherwise, infer the type and try to extract from that.
let fields_type = self.infer_expression(fields_arg, TypeContext::default());
if let Some(fields) = self.extract_typing_namedtuple_fields(fields_type) {
(fields, true)
} else {
// Couldn't determine fields statically; attribute lookups will return Any.
(Box::new([]), false)
}
}
} else {
// `collections.namedtuple`: `field_names` is a list or tuple of strings, or a space or
// comma-separated string.
// Check for `rename=True`.
let rename = matches!(rename_type, Some(Type::BooleanLiteral(true)));
// Extract field names, first from the AST, then from the inferred type.
let maybe_field_names: Option<Box<[ast::name::Name]>> = if let Some(names) =
self.extract_collections_namedtuple_fields_from_ast(fields_arg)
{
Some(names)
} else {
let fields_type = self.infer_expression(fields_arg, TypeContext::default());
if let Some(string_literal) = fields_type.as_string_literal() {
// Handle space/comma-separated string.
let field_str = string_literal.value(db);
Some(
field_str
.replace(',', " ")
.split_whitespace()
.map(ast::name::Name::new)
.collect(),
)
} else if let Some(tuple_spec) = fields_type.exact_tuple_instance_spec(db) {
// Handle list/tuple of strings.
tuple_spec
.fixed_elements()
.map(|elt| {
elt.as_string_literal()
.map(|s| ast::name::Name::new(s.value(db)))
})
.collect()
} else {
// Couldn't determine field names statically.
None
}
};
if let Some(mut field_names) = maybe_field_names {
// Apply rename logic, if `rename=True`.
if rename {
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_python_stdlib::keyword::is_keyword;
use rustc_hash::FxHashSet;
let mut seen_names = FxHashSet::<&str>::default();
for (i, field_name) in field_names.iter_mut().enumerate() {
let name_str = field_name.as_str();
let needs_rename = name_str.starts_with('_')
|| is_keyword(name_str)
|| !is_identifier(name_str)
|| seen_names.contains(name_str);
if needs_rename {
*field_name = ast::name::Name::new(format!("_{i}"));
}
seen_names.insert(field_name.as_str());
}
}
// Build fields with `Any` type and optional defaults.
let num_fields = field_names.len();
let fields = field_names
.iter()
.enumerate()
.map(|(i, field_name)| {
let default = if defaults_count > 0 && i >= num_fields - defaults_count {
Some(Type::any())
} else {
None
};
(field_name.clone(), Type::any(), default)
})
.collect();
(fields, true)
} else {
// Couldn't determine fields statically; attribute lookups will return Any.
(Box::new([]), false)
}
};
let file = self.file();
let file_scope = self.scope().file_scope_id(db);
let namedtuple = DynamicNamedTupleLiteral::new(
db,
name,
fields,
has_known_fields,
file,
file_scope,
definition,
call_expr.range,
);
Some(Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(
namedtuple,
)))
}
/// Extract fields from a typing.NamedTuple fields argument.
#[allow(clippy::type_complexity)]
fn extract_typing_namedtuple_fields(
&mut self,
fields_type: Type<'db>,
) -> Option<Box<[(ast::name::Name, Type<'db>, Option<Type<'db>>)]>> {
let db = self.db();
// Try to extract from a tuple/list type.
let tuple_spec = fields_type.exact_tuple_instance_spec(db)?;
let fields: Option<Box<[_]>> = tuple_spec
.fixed_elements()
.map(|field_tuple| {
let field_spec = field_tuple.exact_tuple_instance_spec(db)?;
let elements: Vec<_> = field_spec.fixed_elements().collect();
if elements.len() != 2 {
return None;
}
let field_name = elements[0]
.as_string_literal()
.map(|s| ast::name::Name::new(s.value(db)))?;
let field_ty = elements[1];
// Convert class literals to instances.
let resolved_ty = match field_ty {
Type::ClassLiteral(class) => class.to_non_generic_instance(db),
Type::GenericAlias(alias) => Type::instance(db, ClassType::Generic(*alias)),
Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() {
crate::types::SubclassOfInner::Class(class) => Type::instance(db, class),
_ => *field_ty,
},
ty => *ty,
};
Some((field_name, resolved_ty, None))
})
.collect();
fields
}
/// Extract fields from a typing.NamedTuple fields argument by looking at the AST directly.
/// This handles list/tuple literals that contain (name, type) pairs.
#[allow(clippy::type_complexity)]
fn extract_typing_namedtuple_fields_from_ast(
&mut self,
fields_arg: &ast::Expr,
) -> Option<Box<[(ast::name::Name, Type<'db>, Option<Type<'db>>)]>> {
let db = self.db();
// Get the elements from the list or tuple literal.
let elements: &[ast::Expr] = match fields_arg {
ast::Expr::List(list) => &list.elts,
ast::Expr::Tuple(tuple) => &tuple.elts,
_ => return None,
};
let fields: Option<Box<[_]>> = elements
.iter()
.map(|elt| {
// Each element should be a tuple like ("field_name", type).
let tuple_expr = elt.as_tuple_expr()?;
if tuple_expr.elts.len() != 2 {
return None;
}
// First element: field name (string literal).
let field_name_expr = &tuple_expr.elts[0];
let field_name_ty = self.infer_expression(field_name_expr, TypeContext::default());
let field_name_lit = field_name_ty.as_string_literal()?;
let field_name = ast::name::Name::new(field_name_lit.value(db));
// Second element: field type (infer as type expression).
let field_type_expr = &tuple_expr.elts[1];
let field_ty = self.infer_type_expression(field_type_expr);
Some((field_name, field_ty, None))
})
.collect();
fields
}
/// Extract field names from a collections.namedtuple fields argument by looking at the AST directly.
/// This handles list/tuple literals that contain string literals.
fn extract_collections_namedtuple_fields_from_ast(
&mut self,
fields_arg: &ast::Expr,
) -> Option<Box<[ast::name::Name]>> {
let db = self.db();
// Get the elements from the list or tuple literal.
let elements: &[ast::Expr] = match fields_arg {
ast::Expr::List(list) => &list.elts,
ast::Expr::Tuple(tuple) => &tuple.elts,
_ => return None,
};
let field_names: Option<Box<[_]>> = elements
.iter()
.map(|elt| {
// Each element should be a string literal.
let field_ty = self.infer_expression(elt, TypeContext::default());
let field_lit = field_ty.as_string_literal()?;
Some(ast::name::Name::new(field_lit.value(db)))
})
.collect();
field_names
}
/// Extract base classes from the second argument of a `type()` call.
///
/// If any bases were invalid, diagnostics are emitted and the dynamic
/// class is inferred as inheriting from `Unknown`.
fn extract_dynamic_type_bases(
&mut self,
bases_node: &ast::Expr,
bases_type: Type<'db>,
name: &ast::name::Name,
) -> Box<[ClassBase<'db>]> {
let db = self.db();
// Get AST nodes for base expressions (for diagnostics).
let bases_tuple_elts = bases_node.as_tuple_expr().map(|t| t.elts.as_slice());
// We use a placeholder class literal for try_from_type (the subclass parameter is only
// used for Protocol/TypedDict detection which doesn't apply here).
let placeholder_class: ClassLiteral<'db> =
KnownClass::Object.try_to_class_literal(db).unwrap().into();
bases_type
.tuple_instance_spec(db)
.as_deref()
.and_then(|spec| spec.as_fixed_length())
.map(|tuple| {
// Fixed-length tuple: extract each base class
tuple
.elements_slice()
.iter()
.enumerate()
.map(|(idx, base)| {
// First try the standard conversion.
if let Some(class_base) =
ClassBase::try_from_type(db, *base, placeholder_class)
{
return class_base;
}
let diagnostic_node = bases_tuple_elts
.and_then(|elts| elts.get(idx))
.unwrap_or(bases_node);
// If that fails, check if the type is "type-like" (e.g., `type[Base]`).
// For type-like bases we emit `unsupported-base` and use `Unknown` to
// avoid cascading errors. For non-type-like bases (like integers),
// we return `None` to fall through to regular call binding which will
// emit `invalid-argument-type`.
let instance_of_type = KnownClass::Type.to_instance(db);
if base.is_assignable_to(db, instance_of_type) {
if let Some(builder) =
self.context.report_lint(&UNSUPPORTED_BASE, diagnostic_node)
{
let mut diagnostic =
builder.into_diagnostic("Unsupported class base");
diagnostic.set_primary_message(format_args!(
"Has type `{}`",
base.display(db)
));
diagnostic.info(format_args!(
"ty cannot determine a MRO for class `{name}` due to this base"
));
diagnostic.info(
"Only class objects or `Any` are supported as class bases",
);
}
} else {
if let Some(builder) =
self.context.report_lint(&INVALID_BASE, diagnostic_node)
{
let mut diagnostic = builder.into_diagnostic(format_args!(
"Invalid class base with type `{}`",
base.display(db)
));
if bases_tuple_elts.is_none() {
diagnostic.info(format_args!(
"Element {} of the tuple is invalid",
idx + 1
));
}
}
}
ClassBase::unknown()
})
.collect()
})
.unwrap_or_else(|| {
if !bases_type.is_assignable_to(
db,
Type::homogeneous_tuple(db, KnownClass::Type.to_instance(db)),
) && let Some(builder) =
self.context.report_lint(&INVALID_ARGUMENT_TYPE, bases_node)
{
let mut diagnostic = builder
.into_diagnostic("Invalid argument to parameter 2 (`bases`) of `type()`");
diagnostic.set_primary_message(format_args!(
"Expected `tuple[type, ...]`, found `{}`",
bases_type.display(db)
));
}
Box::from([ClassBase::unknown()])
})
}
fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) {
if assignment.target.is_name_expr() {
self.infer_definition(assignment);
@@ -6145,14 +6778,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let class_literal = infer_definition_types(db, class_definition)
.declaration_type(class_definition)
.inner_type()
.as_class_literal()?;
.as_class_literal()?
.as_static()?;
class_literal
.dataclass_params(db)
.map(|params| SmallVec::from(params.field_specifiers(db)))
.or_else(|| {
Some(SmallVec::from(
CodeGeneratorKind::from_class(db, class_literal, None)?
CodeGeneratorKind::from_class(db, class_literal.into(), None)?
.dataclass_transformer_params()?
.field_specifiers(db),
))
@@ -8821,6 +9455,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
return Type::TypedDict(typed_dict);
}
// Handle 3-argument `type(name, bases, dict)`.
if let Type::ClassLiteral(class) = callable_type
&& class.is_known(self.db(), KnownClass::Type)
&& let Some(dynamic_type) = self.infer_dynamic_type_expression(call_expression, None)
{
return dynamic_type;
}
// Handle `typing.NamedTuple(typename, fields)` and `collections.namedtuple(typename, field_names)`.
if let Some(namedtuple_type) =
self.infer_namedtuple_call_expression(call_expression, callable_type, None)
{
return namedtuple_type;
}
// We don't call `Type::try_call`, because we want to perform type inference on the
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.
@@ -8962,11 +9611,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// are handled by the default constructor-call logic (we synthesize a `__new__` method for them
// in `ClassType::own_class_member()`).
class.is_known(self.db(), KnownClass::Tuple) && !class.is_generic()
) || CodeGeneratorKind::TypedDict.matches(
self.db(),
class.class_literal(self.db()).0,
class.class_literal(self.db()).1,
);
) || class
.static_class_literal(self.db())
.is_some_and(|(class_literal, specialization)| {
CodeGeneratorKind::TypedDict.matches(
self.db(),
class_literal.into(),
specialization,
)
});
// temporary special-casing for all subclasses of `enum.Enum`
// until we support the functional syntax for creating enum classes
@@ -11936,7 +12589,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
));
}
if let Some(generic_context) = class.generic_context(self.db()) {
if let Some(generic_context) = class.generic_context(self.db())
&& let Some(class) = class.as_static()
{
return self.infer_explicit_class_specialization(
subscript,
value_ty,
@@ -12273,7 +12928,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
&mut self,
subscript: &ast::ExprSubscript,
value_ty: Type<'db>,
generic_class: ClassLiteral<'db>,
generic_class: StaticClassLiteral<'db>,
generic_context: GenericContext<'db>,
) -> Type<'db> {
let db = self.db();
@@ -12991,7 +13646,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// TODO: properly handle old-style generics; get rid of this temporary hack
if !value_ty
.as_class_literal()
.is_some_and(|class| class.iter_mro(db, None).contains(&ClassBase::Generic))
.is_some_and(|class| class.iter_mro(db).contains(&ClassBase::Generic))
{
report_not_subscriptable(context, subscript, value_ty, "__class_getitem__");
}

View File

@@ -1045,12 +1045,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
value_ty
}
Type::ClassLiteral(class) => {
match class.generic_context(self.db()) {
Some(generic_context) => {
match (class.generic_context(self.db()), class.as_static()) {
(Some(generic_context), Some(static_class)) => {
let specialized_class = self.infer_explicit_class_specialization(
subscript,
value_ty,
class,
static_class,
generic_context,
);
@@ -1062,7 +1062,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
)
.unwrap_or(Type::unknown())
}
None => {
_ => {
// TODO: emit a diagnostic if you try to specialize a non-generic class.
self.infer_type_expression(slice);
todo_type!("specialized non-generic class")

View File

@@ -35,27 +35,42 @@ impl<'db> Type<'db> {
}
pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
let (class_literal, specialization) = class.class_literal(db);
match class_literal.known(db) {
Some(KnownClass::Tuple) => Type::tuple(TupleType::new(
db,
specialization
.and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?)))
.unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown())))
.as_ref(),
)),
Some(KnownClass::Object) => Type::object(),
_ => class_literal
.is_typed_dict(db)
.then(|| Type::typed_dict(class))
.or_else(|| {
class.into_protocol_class(db).map(|protocol_class| {
Self::ProtocolInstance(ProtocolInstanceType::from_class(protocol_class))
})
})
.unwrap_or(Type::NominalInstance(NominalInstanceType(
NominalInstanceInner::NonTuple(class),
))),
match class.class_literal(db) {
// Dynamic classes created via `type()` don't have special instance types.
// TODO: When we add functional TypedDict support, this branch should check
// for TypedDict and return `Type::typed_dict(class)` for that case.
ClassLiteral::Dynamic(_) => {
Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple(class)))
}
ClassLiteral::DynamicNamedTuple(_) => {
Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple(class)))
}
ClassLiteral::Static(class_literal) => {
let specialization = class.into_generic_alias().map(|g| g.specialization(db));
match class_literal.known(db) {
Some(KnownClass::Tuple) => Type::tuple(TupleType::new(
db,
specialization
.and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?)))
.unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown())))
.as_ref(),
)),
Some(KnownClass::Object) => Type::object(),
_ => class_literal
.is_typed_dict(db)
.then(|| Type::typed_dict(class))
.or_else(|| {
class.into_protocol_class(db).map(|protocol_class| {
Self::ProtocolInstance(ProtocolInstanceType::from_class(
protocol_class,
))
})
})
.unwrap_or(Type::NominalInstance(NominalInstanceType(
NominalInstanceInner::NonTuple(class),
))),
}
}
}
}
@@ -225,18 +240,9 @@ impl<'db> NominalInstanceType<'db> {
}
}
/// Returns the class literal for this instance.
pub(super) fn class_literal(&self, db: &'db dyn Db) -> ClassLiteral<'db> {
let class = match self.0 {
NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db),
NominalInstanceInner::NonTuple(class) => class,
NominalInstanceInner::Object => {
return KnownClass::Object
.try_to_class_literal(db)
.expect("Typeshed should always have a `object` class in `builtins.pyi`");
}
};
let (class_literal, _) = class.class_literal(db);
class_literal
self.class(db).class_literal(db)
}
/// Returns the [`KnownClass`] that this is a nominal instance of, or `None` if it is not an
@@ -275,7 +281,7 @@ impl<'db> NominalInstanceType<'db> {
.find_map(|class| match class.known(db)? {
// N.B. this is a pure optimisation: iterating through the MRO would give us
// the correct tuple spec for `sys._version_info`, since we special-case the class
// in `ClassLiteral::explicit_bases()` so that it is inferred as inheriting from
// in `StmtClassLiteral::explicit_bases()` so that it is inferred as inheriting from
// a tuple type with the correct spec for the user's configured Python version and platform.
KnownClass::VersionInfo => {
Some(Cow::Owned(TupleSpec::version_info_spec(db)))
@@ -337,10 +343,9 @@ impl<'db> NominalInstanceType<'db> {
NominalInstanceInner::ExactTuple(_) | NominalInstanceInner::Object => return None,
NominalInstanceInner::NonTuple(class) => class,
};
let (class, Some(specialization)) = class.class_literal(db) else {
return None;
};
if !class.is_known(db, KnownClass::Slice) {
let (class_literal, specialization) = class.static_class_literal(db)?;
let specialization = specialization?;
if !class_literal.is_known(db, KnownClass::Slice) {
return None;
}
let [start, stop, step] = specialization.types(db) else {
@@ -480,8 +485,13 @@ impl<'db> NominalInstanceType<'db> {
}
}
}
result.or(db, || {
ConstraintSet::from(!(self.class(db)).could_coexist_in_mro_with(db, other.class(db)))
ConstraintSet::from(
!self
.class(db)
.could_coexist_in_mro_with(db, other.class(db)),
)
})
}
@@ -496,7 +506,7 @@ impl<'db> NominalInstanceType<'db> {
NominalInstanceInner::NonTuple(class) => class
.known(db)
.map(KnownClass::is_singleton)
.unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db).0)),
.unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db))),
}
}
@@ -508,7 +518,7 @@ impl<'db> NominalInstanceType<'db> {
.known(db)
.and_then(KnownClass::is_single_valued)
.or_else(|| Some(self.tuple_spec(db)?.is_single_valued(db)))
.unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db).0)),
.unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db))),
}
}
@@ -621,7 +631,7 @@ pub(super) fn walk_protocol_instance_type<'db, V: super::visitor::TypeVisitor<'d
} else {
match protocol.inner {
Protocol::FromClass(class) => {
if let Some(specialization) = class.class_literal(db).1 {
if let Some((_, Some(specialization))) = class.static_class_literal(db) {
walk_specialization(db, specialization, visitor);
}
}

View File

@@ -21,8 +21,9 @@ use crate::{
semantic_index, use_def_map,
},
types::{
ClassBase, ClassLiteral, KnownClass, KnownInstanceType, SubclassOfInner, Type,
TypeVarBoundOrConstraints, class::CodeGeneratorKind, generics::Specialization,
ClassBase, ClassLiteral, KnownClass, KnownInstanceType, StaticClassLiteral,
SubclassOfInner, Type, TypeVarBoundOrConstraints, class::CodeGeneratorKind,
generics::Specialization,
},
};
@@ -201,9 +202,20 @@ impl<'db> AllMembers<'db> {
),
Type::NominalInstance(instance) => {
let (class_literal, specialization) = instance.class(db).class_literal(db);
self.extend_with_instance_members(db, ty, class_literal);
self.extend_with_synthetic_members(db, ty, class_literal, specialization);
let class = instance.class(db);
if let Some((class_literal, specialization)) = class.static_class_literal(db) {
self.extend_with_instance_members(db, ty, class_literal);
self.extend_with_synthetic_members(
db,
ty,
ClassLiteral::Static(class_literal),
specialization,
);
} else {
// For dynamic classes, we can't enumerate instance members (requires body scope),
// but we can still add synthetic members for dataclass-like classes.
self.extend_with_synthetic_members(db, ty, class.class_literal(db), None);
}
}
Type::NewTypeInstance(newtype) => {
@@ -232,8 +244,13 @@ impl<'db> AllMembers<'db> {
Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db);
self.extend_with_class_members(db, ty, class_literal);
self.extend_with_synthetic_members(db, ty, class_literal, None);
self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal));
self.extend_with_synthetic_members(
db,
ty,
ClassLiteral::Static(class_literal),
None,
);
if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) {
self.extend_with_class_members(db, ty, metaclass);
}
@@ -245,11 +262,23 @@ impl<'db> AllMembers<'db> {
}
_ => {
if let Some(class_type) = subclass_of_type.subclass_of().into_class(db) {
let (class_literal, specialization) = class_type.class_literal(db);
self.extend_with_class_members(db, ty, class_literal);
self.extend_with_synthetic_members(db, ty, class_literal, specialization);
if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) {
self.extend_with_class_members(db, ty, metaclass);
if let Some((class_literal, specialization)) =
class_type.static_class_literal(db)
{
self.extend_with_class_members(
db,
ty,
ClassLiteral::Static(class_literal),
);
self.extend_with_synthetic_members(
db,
ty,
ClassLiteral::Static(class_literal),
specialization,
);
if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) {
self.extend_with_class_members(db, ty, metaclass);
}
}
}
}
@@ -308,13 +337,15 @@ impl<'db> AllMembers<'db> {
self.extend_with_class_members(db, ty, class_literal);
}
Type::SubclassOf(subclass_of) => {
if let Some(class) = subclass_of.subclass_of().into_class(db) {
self.extend_with_class_members(db, ty, class.class_literal(db).0);
if let Some(class) = subclass_of.subclass_of().into_class(db)
&& let Some((class_literal, _)) = class.static_class_literal(db)
{
self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal));
}
}
Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db);
self.extend_with_class_members(db, ty, class_literal);
self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal));
}
_ => {}
},
@@ -324,7 +355,7 @@ impl<'db> AllMembers<'db> {
self.extend_with_class_members(db, ty, class_literal);
}
if let Type::ClassLiteral(class) =
if let Type::ClassLiteral(ClassLiteral::Static(class)) =
KnownClass::TypedDictFallback.to_class_literal(db)
{
self.extend_with_instance_members(db, ty, class);
@@ -430,9 +461,9 @@ impl<'db> AllMembers<'db> {
class_literal: ClassLiteral<'db>,
) {
for parent in class_literal
.iter_mro(db, None)
.iter_mro(db)
.filter_map(ClassBase::into_class)
.map(|class| class.class_literal(db).0)
.filter_map(|class| class.static_class_literal(db).map(|(lit, _)| lit))
{
let parent_scope = parent.body_scope(db);
for memberdef in all_end_of_scope_members(db, parent_scope) {
@@ -448,52 +479,64 @@ impl<'db> AllMembers<'db> {
}
}
fn extend_with_instance_members(
/// Extend with instance members from a single class (not its MRO).
fn extend_with_instance_members_for_class(
&mut self,
db: &'db dyn Db,
ty: Type<'db>,
class_literal: ClassLiteral<'db>,
class_literal: StaticClassLiteral<'db>,
) {
for parent in class_literal
.iter_mro(db, None)
.filter_map(ClassBase::into_class)
.map(|class| class.class_literal(db).0)
{
let class_body_scope = parent.body_scope(db);
let file = class_body_scope.file(db);
let index = semantic_index(db, file);
for function_scope_id in attribute_scopes(db, class_body_scope) {
for place_expr in index.place_table(function_scope_id).members() {
let Some(name) = place_expr.as_instance_attribute() else {
continue;
};
let result = ty.member(db, name);
let Some(ty) = result.place.ignore_possibly_undefined() else {
continue;
};
self.members.insert(Member {
name: Name::new(name),
ty,
});
}
}
// This is very similar to `extend_with_class_members`,
// but uses the type of the class instance to query the
// class member. This gets us the right type for each
// member, e.g., `SomeClass.__delattr__` is not a bound
// method, but `instance_of_SomeClass.__delattr__` is.
for memberdef in all_end_of_scope_members(db, class_body_scope) {
let result = ty.member(db, memberdef.member.name.as_str());
let class_body_scope = class_literal.body_scope(db);
let file = class_body_scope.file(db);
let index = semantic_index(db, file);
for function_scope_id in attribute_scopes(db, class_body_scope) {
for place_expr in index.place_table(function_scope_id).members() {
let Some(name) = place_expr.as_instance_attribute() else {
continue;
};
let result = ty.member(db, name);
let Some(ty) = result.place.ignore_possibly_undefined() else {
continue;
};
self.members.insert(Member {
name: memberdef.member.name,
name: Name::new(name),
ty,
});
}
}
// This is very similar to `extend_with_class_members`,
// but uses the type of the class instance to query the
// class member. This gets us the right type for each
// member, e.g., `SomeClass.__delattr__` is not a bound
// method, but `instance_of_SomeClass.__delattr__` is.
for memberdef in all_end_of_scope_members(db, class_body_scope) {
let result = ty.member(db, memberdef.member.name.as_str());
let Some(ty) = result.place.ignore_possibly_undefined() else {
continue;
};
self.members.insert(Member {
name: memberdef.member.name,
ty,
});
}
}
/// Extend with instance members from a class and all classes in its MRO.
fn extend_with_instance_members(
&mut self,
db: &'db dyn Db,
ty: Type<'db>,
class_literal: StaticClassLiteral<'db>,
) {
for class in class_literal
.iter_mro(db, None)
.filter_map(ClassBase::into_class)
{
if let Some((class_literal, _)) = class.static_class_literal(db) {
self.extend_with_instance_members_for_class(db, ty, class_literal);
}
}
}
fn extend_with_synthetic_members(

View File

@@ -2,17 +2,20 @@ use std::collections::VecDeque;
use std::ops::Deref;
use indexmap::IndexMap;
use rustc_hash::FxBuildHasher;
use rustc_hash::{FxBuildHasher, FxHashSet};
use crate::Db;
use crate::types::class_base::ClassBase;
use crate::types::generics::Specialization;
use crate::types::{ClassLiteral, ClassType, KnownClass, KnownInstanceType, SpecialFormType, Type};
use crate::types::{
ClassLiteral, ClassType, DynamicClassLiteral, KnownInstanceType, SpecialFormType,
StaticClassLiteral, Type,
};
/// The inferred method resolution order of a given class.
///
/// An MRO cannot contain non-specialized generic classes. (This is why [`ClassBase`] contains a
/// [`ClassType`], not a [`ClassLiteral`].) Any generic classes in a base class list are always
/// [`ClassType`], not a [`StaticClassLiteral`].) Any generic classes in a base class list are always
/// specialized — either because the class is explicitly specialized if there is a subscript
/// expression, or because we create the default specialization if there isn't.
///
@@ -29,12 +32,12 @@ use crate::types::{ClassLiteral, ClassType, KnownClass, KnownInstanceType, Speci
///
/// See [`ClassType::iter_mro`] for more details.
#[derive(PartialEq, Eq, Clone, Debug, salsa::Update, get_size2::GetSize)]
pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>);
pub(crate) struct Mro<'db>(Box<[ClassBase<'db>]>);
impl<'db> Mro<'db> {
/// Attempt to resolve the MRO of a given class. Because we derive the MRO from the list of
/// base classes in the class definition, this operation is performed on a [class
/// literal][ClassLiteral], not a [class type][ClassType]. (You can _also_ get the MRO of a
/// literal][StaticClassLiteral], not a [class type][ClassType]. (You can _also_ get the MRO of a
/// class type, but this is done by first getting the MRO of the underlying class literal, and
/// specializing each base class as needed if the class type is a generic alias.)
///
@@ -46,35 +49,11 @@ impl<'db> Mro<'db> {
///
/// (We emit a diagnostic warning about the runtime `TypeError` in
/// [`super::infer::infer_scope_types`].)
pub(super) fn of_class(
pub(super) fn of_static_class(
db: &'db dyn Db,
class_literal: ClassLiteral<'db>,
class_literal: StaticClassLiteral<'db>,
specialization: Option<Specialization<'db>>,
) -> Result<Self, MroError<'db>> {
let class = class_literal.apply_optional_specialization(db, specialization);
// Special-case `NotImplementedType`: typeshed says that it inherits from `Any`,
// but this causes more problems than it fixes.
if class_literal.is_known(db, KnownClass::NotImplementedType) {
return Ok(Self::from([ClassBase::Class(class), ClassBase::object(db)]));
}
Self::of_class_impl(db, class, class_literal.explicit_bases(db), specialization)
.map_err(|err| err.into_mro_error(db, class))
}
pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self {
Self::from([
ClassBase::Class(class),
ClassBase::unknown(),
ClassBase::object(db),
])
}
fn of_class_impl(
db: &'db dyn Db,
class: ClassType<'db>,
original_bases: &[Type<'db>],
specialization: Option<Specialization<'db>>,
) -> Result<Self, MroErrorKind<'db>> {
/// Possibly add `Generic` to the resolved bases list.
///
/// This function is called in two cases:
@@ -106,6 +85,10 @@ impl<'db> Mro<'db> {
resolved_bases.push(ClassBase::Generic);
}
let class = class_literal.apply_optional_specialization(db, specialization);
let original_bases = class_literal.explicit_bases(db);
match original_bases {
// `builtins.object` is the special case:
// the only class in Python that has an MRO with length <2
@@ -156,18 +139,20 @@ impl<'db> Mro<'db> {
)
) =>
{
ClassBase::try_from_type(db, *single_base, class.class_literal(db).0).map_or_else(
|| Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
|single_base| {
if single_base.has_cyclic_mro(db) {
Err(MroErrorKind::InheritanceCycle)
} else {
Ok(std::iter::once(ClassBase::Class(class))
.chain(single_base.mro(db, specialization))
.collect())
}
},
)
ClassBase::try_from_type(db, *single_base, ClassLiteral::Static(class_literal))
.map_or_else(
|| Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
|single_base| {
if single_base.has_cyclic_mro(db) {
Err(MroErrorKind::InheritanceCycle)
} else {
Ok(std::iter::once(ClassBase::Class(class))
.chain(single_base.mro(db, specialization))
.collect())
}
},
)
.map_err(|err| err.into_mro_error(db, class))
}
// The class has multiple explicit bases.
@@ -191,7 +176,11 @@ impl<'db> Mro<'db> {
&original_bases[i + 1..],
);
} else {
match ClassBase::try_from_type(db, *base, class.class_literal(db).0) {
match ClassBase::try_from_type(
db,
*base,
ClassLiteral::Static(class_literal),
) {
Some(valid_base) => resolved_bases.push(valid_base),
None => invalid_bases.push((i, *base)),
}
@@ -199,7 +188,8 @@ impl<'db> Mro<'db> {
}
if !invalid_bases.is_empty() {
return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()));
return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice())
.into_mro_error(db, class));
}
// `Generic` is implicitly added to the bases list of a class that has PEP-695 type parameters
@@ -211,7 +201,7 @@ impl<'db> Mro<'db> {
let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])];
for base in &resolved_bases {
if base.has_cyclic_mro(db) {
return Err(MroErrorKind::InheritanceCycle);
return Err(MroErrorKind::InheritanceCycle.into_mro_error(db, class));
}
seqs.push(base.mro(db, specialization).collect());
}
@@ -239,7 +229,9 @@ impl<'db> Mro<'db> {
)
})
{
return Err(MroErrorKind::Pep695ClassWithGenericInheritance);
return Err(
MroErrorKind::Pep695ClassWithGenericInheritance.into_mro_error(db, class)
);
}
let mut duplicate_dynamic_bases = false;
@@ -258,9 +250,11 @@ impl<'db> Mro<'db> {
// `inconsistent-mro` diagnostic (which would be accurate -- but not nearly as
// precise!).
for (index, base) in original_bases.iter().enumerate() {
let Some(base) =
ClassBase::try_from_type(db, *base, class.class_literal(db).0)
else {
let Some(base) = ClassBase::try_from_type(
db,
*base,
ClassLiteral::Static(class_literal),
) else {
continue;
};
base_to_indices.entry(base).or_default().push(index);
@@ -299,16 +293,118 @@ impl<'db> Mro<'db> {
} else {
Err(MroErrorKind::UnresolvableMro {
bases_list: original_bases.iter().copied().collect(),
})
}
.into_mro_error(db, class))
}
} else {
Err(MroErrorKind::DuplicateBases(
duplicate_bases.into_boxed_slice(),
))
Err(
MroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice())
.into_mro_error(db, class),
)
}
}
}
}
pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self {
Self::from([
ClassBase::Class(class),
ClassBase::unknown(),
ClassBase::object(db),
])
}
/// Attempt to resolve the MRO of a dynamic class (created via `type(name, bases, dict)`).
///
/// Uses C3 linearization when possible, returning an error if the MRO cannot be resolved.
pub(super) fn of_dynamic_class(
db: &'db dyn Db,
dynamic: DynamicClassLiteral<'db>,
) -> Result<Self, DynamicMroError<'db>> {
let bases = dynamic.bases(db);
// Check for duplicate bases first, but skip dynamic bases like `Unknown` or `Any`.
let mut seen = FxHashSet::default();
let mut duplicates = Vec::new();
for base in bases {
if matches!(base, ClassBase::Dynamic(_)) {
continue;
}
if !seen.insert(*base) {
duplicates.push(*base);
}
}
if !duplicates.is_empty() {
return Err(
DynamicMroErrorKind::DuplicateBases(duplicates.into_boxed_slice())
.into_error(db, dynamic),
);
}
// Check if any bases are dynamic, like `Unknown` or `Any`.
let has_dynamic_bases = bases
.iter()
.any(|base| matches!(base, ClassBase::Dynamic(_)));
// Compute MRO using C3 linearization.
let mro_bases = if bases.is_empty() {
// Empty bases: MRO is just `object`.
Some(vec![ClassBase::object(db)])
} else if bases.len() == 1 {
// Single base: MRO is just that base's MRO.
Some(bases[0].mro(db, None).collect())
} else {
// Multiple bases: use C3 merge algorithm.
let mut seqs: Vec<VecDeque<ClassBase<'db>>> = Vec::with_capacity(bases.len() + 1);
// Add each base's MRO.
for base in bases {
seqs.push(base.mro(db, None).collect());
}
// Add the list of bases in order.
seqs.push(bases.iter().copied().collect());
c3_merge(seqs).map(|mro| mro.iter().copied().collect())
};
match mro_bases {
Some(mro) => {
let mut result = vec![ClassBase::Class(ClassType::NonGeneric(dynamic.into()))];
result.extend(mro);
Ok(Self::from(result))
}
None => {
// C3 merge failed. If there are dynamic bases, use the fallback MRO.
// Otherwise, report an error.
if has_dynamic_bases {
Ok(Self::dynamic_fallback(db, dynamic))
} else {
Err(DynamicMroErrorKind::UnresolvableMro.into_error(db, dynamic))
}
}
}
}
/// Compute a fallback MRO for a dynamic class when `of_dynamic_class` fails.
///
/// Iterates over base MROs sequentially with deduplication.
pub(super) fn dynamic_fallback(db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>) -> Self {
let self_base = ClassBase::Class(ClassType::NonGeneric(dynamic.into()));
let mut result = vec![self_base];
let mut seen = FxHashSet::default();
seen.insert(self_base);
for base in dynamic.bases(db) {
for item in base.mro(db, None) {
if seen.insert(item) {
result.push(item);
}
}
}
Self::from(result)
}
}
impl<'db, const N: usize> From<[ClassBase<'db>; N]> for Mro<'db> {
@@ -354,8 +450,8 @@ impl<'db> FromIterator<ClassBase<'db>> for Mro<'db> {
///
/// Even for first-party code, where we will have to resolve the MRO for every class we encounter,
/// loading the cached MRO comes with a certain amount of overhead, so it's best to avoid calling the
/// Salsa-tracked [`ClassLiteral::try_mro`] method unless it's absolutely necessary.
pub(super) struct MroIterator<'db> {
/// Salsa-tracked [`StaticClassLiteral::try_mro`] method unless it's absolutely necessary.
pub(crate) struct MroIterator<'db> {
db: &'db dyn Db,
/// The class whose MRO we're iterating over
@@ -390,19 +486,47 @@ impl<'db> MroIterator<'db> {
}
}
fn first_element(&self) -> ClassBase<'db> {
match self.class {
ClassLiteral::Static(literal) => ClassBase::Class(
literal.apply_optional_specialization(self.db, self.specialization),
),
ClassLiteral::Dynamic(literal) => {
ClassBase::Class(ClassType::NonGeneric(literal.into()))
}
ClassLiteral::DynamicNamedTuple(literal) => {
ClassBase::Class(ClassType::NonGeneric(literal.into()))
}
}
}
/// Materialize the full MRO of the class.
/// Return an iterator over that MRO which skips the first element of the MRO.
fn full_mro_except_first_element(&mut self) -> impl Iterator<Item = ClassBase<'db>> + '_ {
fn full_mro_except_first_element(&mut self) -> &mut std::slice::Iter<'db, ClassBase<'db>> {
self.subsequent_elements
.get_or_insert_with(|| {
let mut full_mro_iter = match self.class.try_mro(self.db, self.specialization) {
Ok(mro) => mro.iter(),
Err(error) => error.fallback_mro().iter(),
};
full_mro_iter.next();
full_mro_iter
.get_or_insert_with(|| match self.class {
ClassLiteral::Static(literal) => {
let mut full_mro_iter = match literal.try_mro(self.db, self.specialization) {
Ok(mro) => mro.iter(),
Err(error) => error.fallback_mro().iter(),
};
full_mro_iter.next();
full_mro_iter
}
ClassLiteral::Dynamic(literal) => {
let mut full_mro_iter = match literal.try_mro(self.db) {
Ok(mro) => mro.iter(),
Err(error) => error.fallback_mro().iter(),
};
full_mro_iter.next();
full_mro_iter
}
ClassLiteral::DynamicNamedTuple(literal) => {
let mut full_mro_iter = literal.mro(self.db).iter();
full_mro_iter.next();
full_mro_iter
}
})
.copied()
}
}
@@ -412,12 +536,9 @@ impl<'db> Iterator for MroIterator<'db> {
fn next(&mut self) -> Option<Self::Item> {
if !self.first_element_yielded {
self.first_element_yielded = true;
return Some(ClassBase::Class(
self.class
.apply_optional_specialization(self.db, self.specialization),
));
return Some(self.first_element());
}
self.full_mro_except_first_element().next()
self.full_mro_except_first_element().next().copied()
}
}
@@ -544,3 +665,49 @@ fn c3_merge(mut sequences: Vec<VecDeque<ClassBase>>) -> Option<Mro> {
}
}
}
/// Error for dynamic class MRO computation with fallback MRO.
///
/// Separate from [`MroError`] because dynamic classes can only have a subset of MRO errors.
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)]
pub(crate) struct DynamicMroError<'db> {
kind: DynamicMroErrorKind<'db>,
fallback_mro: Mro<'db>,
}
impl<'db> DynamicMroError<'db> {
/// Return the error kind describing why we could not resolve the MRO.
pub(crate) fn reason(&self) -> &DynamicMroErrorKind<'db> {
&self.kind
}
/// Return the fallback MRO to use for type inference.
pub(crate) fn fallback_mro(&self) -> &Mro<'db> {
&self.fallback_mro
}
}
/// Error kinds for dynamic class MRO computation.
///
/// These mirror the relevant variants from `MroErrorKind` for static classes.
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)]
pub(crate) enum DynamicMroErrorKind<'db> {
/// The class has duplicate bases in its bases tuple.
DuplicateBases(Box<[ClassBase<'db>]>),
/// The MRO is unresolvable through the C3-merge algorithm.
UnresolvableMro,
}
impl<'db> DynamicMroErrorKind<'db> {
fn into_error(
self,
db: &'db dyn Db,
class_literal: DynamicClassLiteral<'db>,
) -> DynamicMroError<'db> {
DynamicMroError {
kind: self,
fallback_mro: Mro::dynamic_fallback(db, class_literal),
}
}
}

View File

@@ -155,7 +155,7 @@ impl ClassInfoConstraintFunction {
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option<Type<'db>> {
let constraint_fn = |class: ClassLiteral<'db>| match self {
let constraint_from_class_literal = |class: ClassLiteral<'db>| match self {
ClassInfoConstraintFunction::IsInstance => {
Type::instance(db, class.top_materialization(db))
}
@@ -166,9 +166,11 @@ impl ClassInfoConstraintFunction {
match classinfo {
Type::TypeAlias(alias) => self.generate_constraint(db, alias.value_type(db)),
Type::ClassLiteral(class_literal) => Some(constraint_fn(class_literal)),
Type::ClassLiteral(class_literal) => Some(constraint_from_class_literal(class_literal)),
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Class(ClassType::NonGeneric(class)) => Some(constraint_fn(class)),
SubclassOfInner::Class(ClassType::NonGeneric(class_literal)) => {
Some(constraint_from_class_literal(class_literal))
}
// It's not valid to use a generic alias as the second argument to `isinstance()` or `issubclass()`,
// e.g. `isinstance(x, list[int])` fails at runtime.
SubclassOfInner::Class(ClassType::Generic(_)) => None,

View File

@@ -16,7 +16,7 @@ use crate::{
symbol::ScopedSymbolId, use_def_map,
},
types::{
ClassBase, ClassLiteral, ClassType, KnownClass, Type,
ClassBase, ClassType, KnownClass, StaticClassLiteral, Type,
class::CodeGeneratorKind,
context::InferContext,
diagnostic::{
@@ -45,7 +45,10 @@ const PROHIBITED_NAMEDTUPLE_ATTRS: &[&str] = &[
"_source",
];
pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLiteral<'db>) {
// TODO: Support dynamic class literals. If we allow dynamic classes to define attributes in their
// namespace dictionary, we should also check whether those attributes are valid overrides of
// attributes in their superclasses.
pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>) {
let db = context.db();
let configuration = OverrideRulesConfig::from(context);
if configuration.no_rules_enabled() {
@@ -118,15 +121,20 @@ fn check_class_declaration<'db>(
return;
};
let (literal, specialization) = class.class_literal(db);
let class_kind = CodeGeneratorKind::from_class(db, literal, specialization);
let Some((literal, specialization)) = class.static_class_literal(db) else {
return;
};
let class_kind = CodeGeneratorKind::from_class(db, literal.into(), specialization);
// Check for prohibited `NamedTuple` attribute overrides.
//
// `NamedTuple` classes have certain synthesized attributes (like `_asdict`, `_make`, etc.)
// that cannot be overwritten. Attempting to assign to these attributes (without type
// annotations) or define methods with these names will raise an `AttributeError` at runtime.
if class_kind == Some(CodeGeneratorKind::NamedTuple)
//
// This only applies to classes that directly inherit from the `NamedTuple` special form,
// not to classes that inherit from functional namedtuples (which create regular classes).
if literal.directly_inherits_from_named_tuple_special_form(db)
&& configuration.check_prohibited_named_tuple_attrs()
&& PROHIBITED_NAMEDTUPLE_ATTRS.contains(&member.name.as_str())
&& let Some(symbol_id) = place_table(db, class_scope).symbol_id(&member.name)
@@ -171,7 +179,11 @@ fn check_class_declaration<'db>(
ClassBase::Class(class) => class,
};
let (superclass_literal, superclass_specialization) = superclass.class_literal(db);
let Some((superclass_literal, superclass_specialization)) =
superclass.static_class_literal(db)
else {
continue;
};
let superclass_scope = superclass_literal.body_scope(db);
let superclass_symbol_table = place_table(db, superclass_scope);
let superclass_symbol_id = superclass_symbol_table.symbol_id(&member.name);
@@ -191,10 +203,13 @@ fn check_class_declaration<'db>(
{
continue;
}
method_kind =
CodeGeneratorKind::from_class(db, superclass_literal, superclass_specialization)
.map(MethodKind::Synthesized)
.unwrap_or_default();
method_kind = CodeGeneratorKind::from_class(
db,
superclass_literal.into(),
superclass_specialization,
)
.map(MethodKind::Synthesized)
.unwrap_or_default();
}
let Place::Defined(DefinedPlace {

View File

@@ -16,9 +16,9 @@ use crate::{
},
semantic_index::{definition::Definition, place::ScopedPlaceId, place_table, use_def_map},
types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral,
ClassType, FindLegacyTypeVarsVisitor, InstanceFallbackShadowsNonDataDescriptor,
KnownFunction, MemberLookupPolicy, NormalizedVisitor, PropertyInstanceType, Signature,
ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassType,
FindLegacyTypeVarsVisitor, InstanceFallbackShadowsNonDataDescriptor, KnownFunction,
MemberLookupPolicy, NormalizedVisitor, PropertyInstanceType, Signature, StaticClassLiteral,
Type, TypeMapping, TypeQualifiers, TypeVarVariance, VarianceInferable,
constraints::{ConstraintSet, IteratorConstraintsExtension, OptionConstraintsExtension},
context::InferContext,
@@ -29,11 +29,11 @@ use crate::{
},
};
impl<'db> ClassLiteral<'db> {
impl<'db> StaticClassLiteral<'db> {
/// Returns `Some` if this is a protocol class, `None` otherwise.
pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option<ProtocolClass<'db>> {
self.is_protocol(db)
.then_some(ProtocolClass(ClassType::NonGeneric(self)))
.then_some(ProtocolClass(ClassType::NonGeneric(self.into())))
}
}
@@ -76,10 +76,17 @@ impl<'db> ProtocolClass<'db> {
}
pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool {
self.class_literal(db)
.0
.known_function_decorators(db)
.contains(&KnownFunction::RuntimeCheckable)
// Check if this class or any ancestor protocol is decorated with @runtime_checkable.
// Per PEP 544, @runtime_checkable propagates to subclasses.
self.0.iter_mro(db).any(|base| {
base.into_class()
.and_then(|class| class.static_class_literal(db))
.is_some_and(|(class_literal, _)| {
class_literal
.known_function_decorators(db)
.contains(&KnownFunction::RuntimeCheckable)
})
})
}
/// Iterate through the body of the protocol class. Check that all definitions
@@ -88,7 +95,10 @@ impl<'db> ProtocolClass<'db> {
pub(super) fn validate_members(self, context: &InferContext) {
let db = context.db();
let interface = self.interface(db);
let body_scope = self.class_literal(db).0.body_scope(db);
let Some((class_literal, _)) = self.static_class_literal(db) else {
return;
};
let body_scope = class_literal.body_scope(db);
let class_place_table = place_table(db, body_scope);
for (symbol_id, mut bindings_iterator) in
@@ -104,7 +114,11 @@ impl<'db> ProtocolClass<'db> {
self.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|superclass| {
let superclass_scope = superclass.class_literal(db).0.body_scope(db);
let Some((superclass_literal, _)) = superclass.static_class_literal(db)
else {
return false;
};
let superclass_scope = superclass_literal.body_scope(db);
let Some(scoped_symbol_id) =
place_table(db, superclass_scope).symbol_id(symbol_name)
else {
@@ -879,7 +893,7 @@ impl BoundOnClass {
}
}
/// Inner Salsa query for [`ProtocolClassLiteral::interface`].
/// Inner Salsa query for [`ProtocolClass::interface`].
#[salsa::tracked(cycle_initial=proto_interface_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
fn cached_protocol_interface<'db>(
db: &'db dyn Db,
@@ -887,15 +901,16 @@ fn cached_protocol_interface<'db>(
) -> ProtocolInterface<'db> {
let mut members = BTreeMap::default();
for (parent_protocol, specialization) in class
for (parent_scope, specialization) in class
.iter_mro(db)
.filter_map(ClassBase::into_class)
.filter_map(|class| {
let (class, specialization) = class.class_literal(db);
Some((class.into_protocol_class(db)?, specialization))
let (class_literal, specialization) = class.static_class_literal(db)?;
let protocol_class = class_literal.into_protocol_class(db)?;
let parent_scope = protocol_class.static_class_literal(db)?.0.body_scope(db);
Some((parent_scope, specialization))
})
{
let parent_scope = parent_protocol.class_literal(db).0.body_scope(db);
let use_def_map = use_def_map(db, parent_scope);
let place_table = place_table(db, parent_scope);
let mut direct_members = FxHashMap::default();

View File

@@ -7,8 +7,8 @@ use crate::types::protocol_class::ProtocolClass;
use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation};
use crate::types::variance::VarianceInferable;
use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, DynamicType,
FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, MemberLookupPolicy,
ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, ClassType, DynamicClassLiteral,
DynamicType, FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, MemberLookupPolicy,
NormalizedVisitor, SpecialFormType, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints,
TypedDictType, UnionType, todo_type,
};
@@ -334,7 +334,7 @@ impl<'db> SubclassOfType<'db> {
pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
self.subclass_of
.into_class(db)
.is_some_and(|class| class.class_literal(db).0.is_typed_dict(db))
.is_some_and(|class| class.class_literal(db).is_typed_dict(db))
}
}
@@ -534,3 +534,9 @@ impl<'db> From<SubclassOfType<'db>> for Type<'db> {
}
}
}
impl<'db> From<DynamicClassLiteral<'db>> for SubclassOfInner<'db> {
fn from(value: DynamicClassLiteral<'db>) -> Self {
SubclassOfInner::Class(ClassType::NonGeneric(ClassLiteral::Dynamic(value)))
}
}

View File

@@ -1313,6 +1313,14 @@ pub enum Tuple<T> {
}
impl<T> Tuple<T> {
/// Returns the inner fixed-length tuple if this is a `Tuple::Fixed` variant.
pub(crate) fn as_fixed_length(&self) -> Option<&FixedLengthTuple<T>> {
match self {
Tuple::Fixed(tuple) => Some(tuple),
Tuple::Variable(_) => None,
}
}
pub(crate) const fn homogeneous(element: T) -> Self {
Self::Variable(VariableLengthTuple::homogeneous(element))
}

View File

@@ -19,6 +19,7 @@ use super::diagnostic::{
use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor};
use crate::Db;
use crate::semantic_index::definition::Definition;
use crate::types::TypeDefinition;
use crate::types::class::FieldKind;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::generics::InferableTypeVars;
@@ -73,7 +74,9 @@ impl<'db> TypedDictType<'db> {
pub(crate) fn items(self, db: &'db dyn Db) -> &'db TypedDictSchema<'db> {
#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)]
fn class_based_items<'db>(db: &'db dyn Db, class: ClassType<'db>) -> TypedDictSchema<'db> {
let (class_literal, specialization) = class.class_literal(db);
let Some((class_literal, specialization)) = class.static_class_literal(db) else {
return TypedDictSchema::default();
};
class_literal
.fields(db, specialization, CodeGeneratorKind::TypedDict)
.into_iter()
@@ -293,7 +296,14 @@ impl<'db> TypedDictType<'db> {
pub fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
match self {
TypedDictType::Class(defining_class) => Some(defining_class.definition(db)),
TypedDictType::Class(defining_class) => defining_class.definition(db),
TypedDictType::Synthesized(_) => None,
}
}
pub fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
match self {
TypedDictType::Class(defining_class) => defining_class.type_definition(db),
TypedDictType::Synthesized(_) => None,
}
}

View File

@@ -60,13 +60,9 @@ class NamedTupleFallback(tuple[Any, ...]):
if sys.version_info >= (3, 12):
__orig_bases__: ClassVar[tuple[Any, ...]]
@overload
def __init__(self, typename: str, fields: Iterable[tuple[str, Any]], /) -> None: ...
@overload
@typing_extensions.deprecated(
"Creating a typing.NamedTuple using keyword arguments is deprecated and support will be removed in Python 3.15"
)
def __init__(self, typename: str, fields: None = None, /, **kwargs: Any) -> None: ...
# For instance construction when field names are unknown: Point(1, 2).
def __new__(cls, *args: Any, **kwargs: Any) -> typing_extensions.Self: ...
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
@classmethod
def _make(cls, iterable: Iterable[Any]) -> typing_extensions.Self: ...
def _asdict(self) -> dict[str, Any]: ...