mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] validate constructor call of TypedDict
(#19810)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## Summary Implement validation for `TypedDict` constructor calls and dictionary literal assignments, including support for `total=False` and proper field management. Also add support for `Required` and `NotRequired` type qualifiers in `TypedDict` classes, along with proper inheritance behavior and the `total=` parameter. Support both constructor calls and dict literal syntax part of https://github.com/astral-sh/ty/issues/154 ### Basic Required Field Validation ```py class Person(TypedDict): name: str age: int | None # Error: Missing required field 'name' in TypedDict `Person` constructor incomplete = Person(age=25) # Error: Invalid argument to key "name" with declared type `str` on TypedDict `Person` wrong_type = Person(name=123, age=25) # Error: Invalid key access on TypedDict `Person`: Unknown key "extra" extra_field = Person(name="Bob", age=25, extra=True) ``` <img width="773" height="191" alt="Screenshot 2025-08-07 at 17 59 22" src="https://github.com/user-attachments/assets/79076d98-e85f-4495-93d6-a731aa72a5c9" /> ### Support for `total=False` ```py class OptionalPerson(TypedDict, total=False): name: str age: int | None # All valid - all fields are optional with total=False charlie = OptionalPerson() david = OptionalPerson(name="David") emily = OptionalPerson(age=30) frank = OptionalPerson(name="Frank", age=25) # But type validation and extra fields still apply invalid_type = OptionalPerson(name=123) # Error: Invalid argument type invalid_extra = OptionalPerson(extra=True) # Error: Invalid key access ``` ### Dictionary Literal Validation ```py # Type checking works for both constructors and dict literals person: Person = {"name": "Alice", "age": 30} reveal_type(person["name"]) # revealed: str reveal_type(person["age"]) # revealed: int | None # Error: Invalid key access on TypedDict `Person`: Unknown key "non_existing" reveal_type(person["non_existing"]) # revealed: Unknown ``` ### `Required`, `NotRequired`, `total` ```python from typing import TypedDict from typing_extensions import Required, NotRequired class PartialUser(TypedDict, total=False): name: Required[str] # Required despite total=False age: int # Optional due to total=False email: NotRequired[str] # Explicitly optional (redundant) class User(TypedDict): name: Required[str] # Explicitly required (redundant) age: int # Required due to total=True bio: NotRequired[str] # Optional despite total=True # Valid constructions partial = PartialUser(name="Alice") # name required, age optional full = User(name="Bob", age=25) # name and age required, bio optional # Inheritance maintains original field requirements class Employee(PartialUser): department: str # Required (new field) # name: still Required (inherited) # age: still optional (inherited) emp = Employee(name="Charlie", department="Engineering") # ✅ Employee(department="Engineering") # ❌ e: Employee = {"age": 1} # ❌ ``` <img width="898" height="683" alt="Screenshot 2025-08-11 at 22 02 57" src="https://github.com/user-attachments/assets/4c1b18cd-cb2e-493a-a948-51589d121738" /> ## Implementation The implementation reuses existing validation logic done in https://github.com/astral-sh/ruff/pull/19782 ### ℹ️ Why I did NOT synthesize an `__init__` for `TypedDict`: `TypedDict` inherits `dict.__init__(self, *args, **kwargs)` that accepts all arguments. The type resolution system finds this inherited signature **before** looking for synthesized members. So `own_synthesized_member()` is never called because a signature already exists. To force synthesis, you'd have to override Python’s inheritance mechanism, which would break compatibility with the existing ecosystem. This is why I went with ad-hoc validation. IMO it's the only viable approach that respects Python’s inheritance semantics while providing the required validation. ### Refacto of `Field` **Before:** ```rust struct Field<'db> { declared_ty: Type<'db>, default_ty: Option<Type<'db>>, // NamedTuple and dataclass only init_only: bool, // dataclass only init: bool, // dataclass only is_required: Option<bool>, // TypedDict only } ``` **After:** ```rust struct Field<'db> { declared_ty: Type<'db>, kind: FieldKind<'db>, } enum FieldKind<'db> { NamedTuple { default_ty: Option<Type<'db>> }, Dataclass { default_ty: Option<Type<'db>>, init_only: bool, init: bool }, TypedDict { is_required: bool }, } ``` ## Test Plan Updated Markdown tests --------- Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
parent
376e3ff395
commit
f9bbee33f6
14 changed files with 1113 additions and 248 deletions
|
@ -33,6 +33,10 @@ when necessary, e.g. to watch for file system changes or a dedicated UI thread.
|
||||||
|
|
||||||
ty also reads the following externally defined environment variables:
|
ty also reads the following externally defined environment variables:
|
||||||
|
|
||||||
|
### `CONDA_DEFAULT_ENV`
|
||||||
|
|
||||||
|
Used to determine if an active Conda environment is the base environment or not.
|
||||||
|
|
||||||
### `CONDA_PREFIX`
|
### `CONDA_PREFIX`
|
||||||
|
|
||||||
Used to detect an activated Conda environment location.
|
Used to detect an activated Conda environment location.
|
||||||
|
|
157
crates/ty/docs/rules.md
generated
157
crates/ty/docs/rules.md
generated
|
@ -36,7 +36,7 @@ def test(): -> "int":
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L109)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L110)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L153)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L154)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -88,7 +88,7 @@ f(int) # error
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L179)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L180)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -117,7 +117,7 @@ a = 1
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L204)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L205)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -147,7 +147,7 @@ class C(A, B): ...
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L230)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L231)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -177,7 +177,7 @@ class B(A): ...
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L295)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L296)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -202,7 +202,7 @@ class B(A, A): ...
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L316)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L317)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L519)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L520)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -334,7 +334,7 @@ class C(A, B): ...
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L543)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L544)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L348)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L349)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -445,7 +445,7 @@ an atypical memory layout.
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L588)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L589)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L629)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -496,7 +496,7 @@ a: int = ''
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1662)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1663)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L651)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -562,7 +562,7 @@ asyncio.run(main())
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L680)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L681)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base]
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L731)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L732)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -609,7 +609,7 @@ with 1:
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L752)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L753)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -636,7 +636,7 @@ a: str
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L775)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L776)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -678,7 +678,7 @@ except ZeroDivisionError:
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L812)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -709,7 +709,7 @@ class C[U](Generic[T]): ...
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L563)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L564)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height'
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L837)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L838)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ...
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L886)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L887)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -803,7 +803,7 @@ class B(metaclass=f): ...
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L494)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -833,7 +833,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L913)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L914)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -881,7 +881,7 @@ def foo(x: int) -> int: ...
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L957)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -905,7 +905,7 @@ def f(a: int = ''): ...
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L430)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L431)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -937,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L976)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L977)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
Checks for `raise` statements that raise non-exceptions or use invalid
|
Checks for `raise` statements that raise non-exceptions or use invalid
|
||||||
|
@ -984,7 +984,7 @@ def g():
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L609)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L610)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1007,7 +1007,7 @@ def func() -> int:
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1019)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1020)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1061,7 +1061,7 @@ TODO #14889
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L866)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1086,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1058)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1059)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1114,7 +1114,7 @@ TYPE_CHECKING = ''
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1082)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1083)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1142,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1134)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1135)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1174,7 +1174,7 @@ f(10) # Error
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1106)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1206,7 +1206,7 @@ class C:
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1162)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1163)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1239,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1191)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1192)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1257,12 +1257,43 @@ def func(x: int): ...
|
||||||
func() # TypeError: func() missing 1 required positional argument: 'x'
|
func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `missing-typed-dict-key`
|
||||||
|
|
||||||
|
<small>
|
||||||
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key) ·
|
||||||
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1762)
|
||||||
|
</small>
|
||||||
|
|
||||||
|
**What it does**
|
||||||
|
|
||||||
|
Detects missing required keys in `TypedDict` constructor calls.
|
||||||
|
|
||||||
|
**Why is this bad?**
|
||||||
|
|
||||||
|
`TypedDict` requires all non-optional keys to be provided during construction.
|
||||||
|
Missing items can lead to a `KeyError` at runtime.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
|
||||||
|
alice: Person = {"name": "Alice"} # missing required key 'age'
|
||||||
|
|
||||||
|
alice["age"] # KeyError
|
||||||
|
```
|
||||||
|
|
||||||
## `no-matching-overload`
|
## `no-matching-overload`
|
||||||
|
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1210)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1211)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1289,7 +1320,7 @@ func("string") # error: [no-matching-overload]
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1233)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1234)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1311,7 +1342,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1251)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1252)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1335,7 +1366,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1302)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1303)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1389,7 +1420,7 @@ def test(): -> "int":
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1638)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1639)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1417,7 +1448,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1393)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1444,7 +1475,7 @@ class B(A): ... # Error raised here
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1438)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1439)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1469,7 +1500,7 @@ f("foo") # Error raised here
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1416)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1417)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1495,7 +1526,7 @@ def _(x: int):
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1459)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1460)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1539,7 +1570,7 @@ class A:
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1516)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1517)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1564,7 +1595,7 @@ f(x=1, y=2) # Error raised here
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1537)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1538)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1590,7 +1621,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1559)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1560)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1613,7 +1644,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1578)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1579)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1636,7 +1667,7 @@ print(x) # NameError: name 'x' is not defined
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1271)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1272)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1671,7 +1702,7 @@ b1 < b2 < b1 # exception raised here
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1597)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1598)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1697,7 +1728,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
||||||
<small>
|
<small>
|
||||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1619)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1620)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1720,7 +1751,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
|
||||||
<small>
|
<small>
|
||||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L458)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L459)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1759,7 +1790,7 @@ class SubProto(BaseProto, Protocol):
|
||||||
<small>
|
<small>
|
||||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L274)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L275)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1812,7 +1843,7 @@ a = 20 / 0 # type: ignore
|
||||||
<small>
|
<small>
|
||||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1324)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1838,7 +1869,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||||
<small>
|
<small>
|
||||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L127)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L128)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1868,7 +1899,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||||
<small>
|
<small>
|
||||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1345)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1346)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1898,7 +1929,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||||
<small>
|
<small>
|
||||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1690)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1691)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1923,7 +1954,7 @@ cast(int, f()) # Redundant
|
||||||
<small>
|
<small>
|
||||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1498)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1499)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -1974,7 +2005,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
|
||||||
<small>
|
<small>
|
||||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1711)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1712)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -2028,7 +2059,7 @@ def g():
|
||||||
<small>
|
<small>
|
||||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L698)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L699)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -2065,7 +2096,7 @@ class D(C): ... # error: [unsupported-base]
|
||||||
<small>
|
<small>
|
||||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L257)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
@ -2087,7 +2118,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||||
<small>
|
<small>
|
||||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
|
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
|
||||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371)
|
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1372)
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
**What it does**
|
**What it does**
|
||||||
|
|
|
@ -6,14 +6,12 @@ Several type qualifiers are unsupported by ty currently. However, we also don't
|
||||||
errors if you use one in an annotation:
|
errors if you use one in an annotation:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict
|
from typing_extensions import Final, ReadOnly, TypedDict
|
||||||
|
|
||||||
X: Final = 42
|
X: Final = 42
|
||||||
Y: Final[int] = 42
|
Y: Final[int] = 42
|
||||||
|
|
||||||
class Bar(TypedDict):
|
class Bar(TypedDict):
|
||||||
x: Required[int]
|
|
||||||
y: NotRequired[str]
|
|
||||||
z: ReadOnly[bytes]
|
z: ReadOnly[bytes]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -25,23 +23,16 @@ One thing that is supported is error messages for using type qualifiers in type
|
||||||
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
|
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
|
||||||
|
|
||||||
def _(
|
def _(
|
||||||
a: (
|
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
|
||||||
Final # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
|
a: Final | int,
|
||||||
| int
|
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
|
||||||
),
|
b: ClassVar | int,
|
||||||
b: (
|
# error: [invalid-type-form] "Type qualifier `typing.ReadOnly` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
||||||
ClassVar # error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
|
c: ReadOnly | int,
|
||||||
| int
|
|
||||||
),
|
|
||||||
c: Required, # error: [invalid-type-form] "Type qualifier `typing.Required` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
|
||||||
d: NotRequired, # error: [invalid-type-form] "Type qualifier `typing.NotRequired` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
|
||||||
e: ReadOnly, # error: [invalid-type-form] "Type qualifier `typing.ReadOnly` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
|
||||||
) -> None:
|
) -> None:
|
||||||
reveal_type(a) # revealed: Unknown | int
|
reveal_type(a) # revealed: Unknown | int
|
||||||
reveal_type(b) # revealed: Unknown | int
|
reveal_type(b) # revealed: Unknown | int
|
||||||
reveal_type(c) # revealed: Unknown
|
reveal_type(c) # revealed: Unknown | int
|
||||||
reveal_type(d) # revealed: Unknown
|
|
||||||
reveal_type(e) # revealed: Unknown
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inheritance
|
## Inheritance
|
||||||
|
@ -53,7 +44,5 @@ from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
|
||||||
|
|
||||||
class A(Final): ... # error: [invalid-base]
|
class A(Final): ... # error: [invalid-base]
|
||||||
class B(ClassVar): ... # error: [invalid-base]
|
class B(ClassVar): ... # error: [invalid-base]
|
||||||
class C(Required): ... # error: [invalid-base]
|
class C(ReadOnly): ... # error: [invalid-base]
|
||||||
class D(NotRequired): ... # error: [invalid-base]
|
|
||||||
class E(ReadOnly): ... # error: [invalid-base]
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -21,12 +21,10 @@ inferred based on the `TypedDict` definition:
|
||||||
```py
|
```py
|
||||||
alice: Person = {"name": "Alice", "age": 30}
|
alice: Person = {"name": "Alice", "age": 30}
|
||||||
|
|
||||||
# TODO: this should be `str`
|
reveal_type(alice["name"]) # revealed: str
|
||||||
reveal_type(alice["name"]) # revealed: Unknown
|
reveal_type(alice["age"]) # revealed: int | None
|
||||||
# TODO: this should be `int | None`
|
|
||||||
reveal_type(alice["age"]) # revealed: Unknown
|
|
||||||
|
|
||||||
# TODO: this should reveal `Unknown`, and it should emit an error
|
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
|
||||||
reveal_type(alice["non_existing"]) # revealed: Unknown
|
reveal_type(alice["non_existing"]) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -51,23 +49,26 @@ bob.update(age=26)
|
||||||
The construction of a `TypedDict` is checked for type correctness:
|
The construction of a `TypedDict` is checked for type correctness:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: these should be errors (invalid argument type)
|
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`"
|
||||||
eve1a: Person = {"name": b"Eve", "age": None}
|
eve1a: Person = {"name": b"Eve", "age": None}
|
||||||
|
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`"
|
||||||
eve1b = Person(name=b"Eve", age=None)
|
eve1b = Person(name=b"Eve", age=None)
|
||||||
|
|
||||||
# TODO: these should be errors (missing required key)
|
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
|
||||||
eve2a: Person = {"age": 22}
|
eve2a: Person = {"age": 22}
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
|
||||||
eve2b = Person(age=22)
|
eve2b = Person(age=22)
|
||||||
|
|
||||||
# TODO: these should be errors (additional key)
|
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||||
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
|
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
|
||||||
|
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||||
eve3b = Person(name="Eve", age=25, extra=True)
|
eve3b = Person(name="Eve", age=25, extra=True)
|
||||||
```
|
```
|
||||||
|
|
||||||
Assignments to keys are also validated:
|
Assignments to keys are also validated:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: this should be an error
|
# error: [invalid-assignment] "Invalid assignment to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||||
alice["name"] = None
|
alice["name"] = None
|
||||||
|
|
||||||
# error: [invalid-assignment] "Invalid assignment to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
# error: [invalid-assignment] "Invalid assignment to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||||
|
@ -77,13 +78,221 @@ bob["name"] = None
|
||||||
Assignments to non-existing keys are disallowed:
|
Assignments to non-existing keys are disallowed:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: this should be an error
|
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||||
alice["extra"] = True
|
alice["extra"] = True
|
||||||
|
|
||||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||||
bob["extra"] = True
|
bob["extra"] = True
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Validation of `TypedDict` construction
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
age: int | None
|
||||||
|
|
||||||
|
class House:
|
||||||
|
owner: Person
|
||||||
|
|
||||||
|
house = House()
|
||||||
|
|
||||||
|
def accepts_person(p: Person) -> None:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
The following constructions of `Person` are all valid:
|
||||||
|
|
||||||
|
```py
|
||||||
|
alice1: Person = {"name": "Alice", "age": 30}
|
||||||
|
Person(name="Alice", age=30)
|
||||||
|
Person({"name": "Alice", "age": 30})
|
||||||
|
|
||||||
|
accepts_person({"name": "Alice", "age": 30})
|
||||||
|
house.owner = {"name": "Alice", "age": 30}
|
||||||
|
```
|
||||||
|
|
||||||
|
All of these are missing the required `age` field:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||||
|
alice2: Person = {"name": "Alice"}
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||||
|
Person(name="Alice")
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||||
|
Person({"name": "Alice"})
|
||||||
|
|
||||||
|
# TODO: this should be an error, similar to the above
|
||||||
|
accepts_person({"name": "Alice"})
|
||||||
|
# TODO: this should be an error, similar to the above
|
||||||
|
house.owner = {"name": "Alice"}
|
||||||
|
```
|
||||||
|
|
||||||
|
All of these have an invalid type for the `name` field:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||||
|
alice3: Person = {"name": None, "age": 30}
|
||||||
|
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||||
|
Person(name=None, age=30)
|
||||||
|
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||||
|
Person({"name": None, "age": 30})
|
||||||
|
|
||||||
|
# TODO: this should be an error, similar to the above
|
||||||
|
accepts_person({"name": None, "age": 30})
|
||||||
|
# TODO: this should be an error, similar to the above
|
||||||
|
house.owner = {"name": None, "age": 30}
|
||||||
|
```
|
||||||
|
|
||||||
|
All of these have an extra field that is not defined in the `TypedDict`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||||
|
alice4: Person = {"name": "Alice", "age": 30, "extra": True}
|
||||||
|
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||||
|
Person(name="Alice", age=30, extra=True)
|
||||||
|
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||||
|
Person({"name": "Alice", "age": 30, "extra": True})
|
||||||
|
|
||||||
|
# TODO: this should be an error
|
||||||
|
accepts_person({"name": "Alice", "age": 30, "extra": True})
|
||||||
|
# TODO: this should be an error
|
||||||
|
house.owner = {"name": "Alice", "age": 30, "extra": True}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type ignore compatibility issues
|
||||||
|
|
||||||
|
Users should be able to ignore TypedDict validation errors with `# type: ignore`
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
|
||||||
|
alice_bad: Person = {"name": None} # type: ignore
|
||||||
|
Person(name=None, age=30) # type: ignore
|
||||||
|
Person(name="Alice", age=30, extra=True) # type: ignore
|
||||||
|
```
|
||||||
|
|
||||||
|
## Positional dictionary constructor pattern
|
||||||
|
|
||||||
|
The positional dictionary constructor pattern (used by libraries like strawberry) should work
|
||||||
|
correctly:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class User(TypedDict):
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
|
||||||
|
# Valid usage - all required fields provided
|
||||||
|
user1 = User({"name": "Alice", "age": 30})
|
||||||
|
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `User` constructor"
|
||||||
|
user2 = User({"name": "Bob"})
|
||||||
|
|
||||||
|
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `User`: value of type `None`"
|
||||||
|
user3 = User({"name": None, "age": 25})
|
||||||
|
|
||||||
|
# error: [invalid-key] "Invalid key access on TypedDict `User`: Unknown key "extra""
|
||||||
|
user4 = User({"name": "Charlie", "age": 30, "extra": True})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional fields with `total=False`
|
||||||
|
|
||||||
|
By default, all fields in a `TypedDict` are required (`total=True`). You can make all fields
|
||||||
|
optional by setting `total=False`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class OptionalPerson(TypedDict, total=False):
|
||||||
|
name: str
|
||||||
|
age: int | None
|
||||||
|
|
||||||
|
# All fields are optional with total=False
|
||||||
|
charlie = OptionalPerson()
|
||||||
|
david = OptionalPerson(name="David")
|
||||||
|
emily = OptionalPerson(age=30)
|
||||||
|
frank = OptionalPerson(name="Frank", age=25)
|
||||||
|
|
||||||
|
# TODO: we could emit an error here, because these fields are not guaranteed to exist
|
||||||
|
reveal_type(charlie["name"]) # revealed: str
|
||||||
|
reveal_type(david["age"]) # revealed: int | None
|
||||||
|
```
|
||||||
|
|
||||||
|
Type validation still applies to provided fields:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `OptionalPerson`"
|
||||||
|
invalid = OptionalPerson(name=123)
|
||||||
|
```
|
||||||
|
|
||||||
|
Extra fields are still not allowed, even with `total=False`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [invalid-key] "Invalid key access on TypedDict `OptionalPerson`: Unknown key "extra""
|
||||||
|
invalid_extra = OptionalPerson(name="George", extra=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## `Required` and `NotRequired`
|
||||||
|
|
||||||
|
You can have fine-grained control over field requirements using `Required` and `NotRequired`
|
||||||
|
qualifiers, which override the class-level `total=` setting:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing_extensions import TypedDict, Required, NotRequired
|
||||||
|
|
||||||
|
# total=False by default, but id is explicitly Required
|
||||||
|
class Message(TypedDict, total=False):
|
||||||
|
id: Required[int] # Always required, even though total=False
|
||||||
|
content: str # Optional due to total=False
|
||||||
|
timestamp: NotRequired[str] # Explicitly optional (redundant here)
|
||||||
|
|
||||||
|
# total=True by default, but content is explicitly NotRequired
|
||||||
|
class User(TypedDict):
|
||||||
|
name: str # Required due to total=True (default)
|
||||||
|
email: Required[str] # Explicitly required (redundant here)
|
||||||
|
bio: NotRequired[str] # Optional despite total=True
|
||||||
|
|
||||||
|
# Valid Message constructions
|
||||||
|
msg1 = Message(id=1) # id required, content optional
|
||||||
|
msg2 = Message(id=2, content="Hello") # both provided
|
||||||
|
msg3 = Message(id=3, timestamp="2024-01-01") # id required, timestamp optional
|
||||||
|
|
||||||
|
# Valid User constructions
|
||||||
|
user1 = User(name="Alice", email="alice@example.com") # required fields
|
||||||
|
user2 = User(name="Bob", email="bob@example.com", bio="Developer") # with optional bio
|
||||||
|
|
||||||
|
reveal_type(msg1["id"]) # revealed: int
|
||||||
|
reveal_type(msg1["content"]) # revealed: str
|
||||||
|
reveal_type(user1["name"]) # revealed: str
|
||||||
|
reveal_type(user1["bio"]) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
Constructor validation respects `Required`/`NotRequired` overrides:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'id' in TypedDict `Message` constructor"
|
||||||
|
invalid_msg = Message(content="Hello") # Missing required id
|
||||||
|
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `User` constructor"
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'email' in TypedDict `User` constructor"
|
||||||
|
invalid_user = User(bio="No name provided") # Missing required name and email
|
||||||
|
```
|
||||||
|
|
||||||
|
Type validation still applies to all fields when provided:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [invalid-argument-type] "Invalid argument to key "id" with declared type `int` on TypedDict `Message`"
|
||||||
|
invalid_type = Message(id="not-an-int", content="Hello")
|
||||||
|
```
|
||||||
|
|
||||||
## Structural assignability
|
## Structural assignability
|
||||||
|
|
||||||
Assignability between `TypedDict` types is structural, that is, it is based on the presence of keys
|
Assignability between `TypedDict` types is structural, that is, it is based on the presence of keys
|
||||||
|
@ -134,8 +343,7 @@ alice: Person = {"name": "Alice"}
|
||||||
# TODO: this should be an invalid-assignment error
|
# TODO: this should be an invalid-assignment error
|
||||||
dangerous(alice)
|
dangerous(alice)
|
||||||
|
|
||||||
# TODO: this should be `str`
|
reveal_type(alice["name"]) # revealed: str
|
||||||
reveal_type(alice["name"]) # revealed: Unknown
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key-based access
|
## Key-based access
|
||||||
|
@ -224,6 +432,20 @@ def _(person: Person, unknown_key: Any):
|
||||||
person[unknown_key] = "Eve"
|
person[unknown_key] = "Eve"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `ReadOnly`
|
||||||
|
|
||||||
|
`ReadOnly` is not supported yet, but this test makes sure that we do not emit any false positive
|
||||||
|
diagnostics:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing_extensions import TypedDict, ReadOnly, Required
|
||||||
|
|
||||||
|
class Person(TypedDict, total=False):
|
||||||
|
id: ReadOnly[Required[int]]
|
||||||
|
name: str
|
||||||
|
age: int | None
|
||||||
|
```
|
||||||
|
|
||||||
## Methods on `TypedDict`
|
## Methods on `TypedDict`
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
@ -340,10 +562,79 @@ class Employee(Person):
|
||||||
|
|
||||||
alice: Employee = {"name": "Alice", "employee_id": 1}
|
alice: Employee = {"name": "Alice", "employee_id": 1}
|
||||||
|
|
||||||
# TODO: this should be an error (missing required key)
|
# error: [missing-typed-dict-key] "Missing required key 'employee_id' in TypedDict `Employee` constructor"
|
||||||
eve: Employee = {"name": "Eve"}
|
eve: Employee = {"name": "Eve"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When inheriting from a `TypedDict` with a different `total` setting, inherited fields maintain their
|
||||||
|
original requirement status, while new fields follow the child class's `total` setting:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
# Case 1: total=True parent, total=False child
|
||||||
|
class PersonBase(TypedDict):
|
||||||
|
id: int # required (from total=True)
|
||||||
|
name: str # required (from total=True)
|
||||||
|
|
||||||
|
class PersonOptional(PersonBase, total=False):
|
||||||
|
age: int # optional (from total=False)
|
||||||
|
email: str # optional (from total=False)
|
||||||
|
|
||||||
|
# Inherited fields keep their original requirement status
|
||||||
|
person1 = PersonOptional(id=1, name="Alice") # Valid - id/name still required
|
||||||
|
person2 = PersonOptional(id=1, name="Alice", age=25) # Valid - age optional
|
||||||
|
person3 = PersonOptional(id=1, name="Alice", email="alice@test.com") # Valid
|
||||||
|
|
||||||
|
# These should be errors - missing required inherited fields
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'id' in TypedDict `PersonOptional` constructor"
|
||||||
|
person_invalid1 = PersonOptional(name="Bob")
|
||||||
|
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `PersonOptional` constructor"
|
||||||
|
person_invalid2 = PersonOptional(id=2)
|
||||||
|
|
||||||
|
# Case 2: total=False parent, total=True child
|
||||||
|
class PersonBaseOptional(TypedDict, total=False):
|
||||||
|
id: int # optional (from total=False)
|
||||||
|
name: str # optional (from total=False)
|
||||||
|
|
||||||
|
class PersonRequired(PersonBaseOptional): # total=True by default
|
||||||
|
age: int # required (from total=True)
|
||||||
|
|
||||||
|
# New fields in child are required, inherited fields stay optional
|
||||||
|
person4 = PersonRequired(age=30) # Valid - only age required, id/name optional
|
||||||
|
person5 = PersonRequired(id=1, name="Charlie", age=35) # Valid - all provided
|
||||||
|
|
||||||
|
# This should be an error - missing required new field
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `PersonRequired` constructor"
|
||||||
|
person_invalid3 = PersonRequired(id=3, name="David")
|
||||||
|
```
|
||||||
|
|
||||||
|
This also works with `Required` and `NotRequired`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing_extensions import TypedDict, Required, NotRequired
|
||||||
|
|
||||||
|
# Case 3: Mixed inheritance with Required/NotRequired
|
||||||
|
class PersonMixed(TypedDict, total=False):
|
||||||
|
id: Required[int] # required despite total=False
|
||||||
|
name: str # optional due to total=False
|
||||||
|
|
||||||
|
class Employee(PersonMixed): # total=True by default
|
||||||
|
department: str # required due to total=True
|
||||||
|
|
||||||
|
# id stays required (Required override), name stays optional, department is required
|
||||||
|
emp1 = Employee(id=1, department="Engineering") # Valid
|
||||||
|
emp2 = Employee(id=2, name="Eve", department="Sales") # Valid
|
||||||
|
|
||||||
|
# Errors for missing required keys
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'id' in TypedDict `Employee` constructor"
|
||||||
|
emp_invalid1 = Employee(department="HR")
|
||||||
|
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'department' in TypedDict `Employee` constructor"
|
||||||
|
emp_invalid2 = Employee(id=3)
|
||||||
|
```
|
||||||
|
|
||||||
## Generic `TypedDict`
|
## Generic `TypedDict`
|
||||||
|
|
||||||
`TypedDict`s can also be generic.
|
`TypedDict`s can also be generic.
|
||||||
|
@ -362,7 +653,7 @@ class TaggedData(TypedDict, Generic[T]):
|
||||||
p1: TaggedData[int] = {"data": 42, "tag": "number"}
|
p1: TaggedData[int] = {"data": 42, "tag": "number"}
|
||||||
p2: TaggedData[str] = {"data": "Hello", "tag": "text"}
|
p2: TaggedData[str] = {"data": "Hello", "tag": "text"}
|
||||||
|
|
||||||
# TODO: this should be an error (type mismatch)
|
# error: [invalid-argument-type] "Invalid argument to key "data" with declared type `int` on TypedDict `TaggedData`: value of type `Literal["not a number"]`"
|
||||||
p3: TaggedData[int] = {"data": "not a number", "tag": "number"}
|
p3: TaggedData[int] = {"data": "not a number", "tag": "number"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -383,7 +674,7 @@ class TaggedData[T](TypedDict):
|
||||||
p1: TaggedData[int] = {"data": 42, "tag": "number"}
|
p1: TaggedData[int] = {"data": 42, "tag": "number"}
|
||||||
p2: TaggedData[str] = {"data": "Hello", "tag": "text"}
|
p2: TaggedData[str] = {"data": "Hello", "tag": "text"}
|
||||||
|
|
||||||
# TODO: this should be an error (type mismatch)
|
# error: [invalid-argument-type] "Invalid argument to key "data" with declared type `int` on TypedDict `TaggedData`: value of type `Literal["not a number"]`"
|
||||||
p3: TaggedData[int] = {"data": "not a number", "tag": "number"}
|
p3: TaggedData[int] = {"data": "not a number", "tag": "number"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -475,4 +766,54 @@ def write_to_non_literal_string_key(person: Person, str_key: str):
|
||||||
person[str_key] = "Alice" # error: [invalid-key]
|
person[str_key] = "Alice" # error: [invalid-key]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Import aliases
|
||||||
|
|
||||||
|
`TypedDict` can be imported with aliases and should work correctly:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict as TD
|
||||||
|
from typing_extensions import Required
|
||||||
|
|
||||||
|
class UserWithAlias(TD, total=False):
|
||||||
|
name: Required[str]
|
||||||
|
age: int
|
||||||
|
|
||||||
|
user_empty = UserWithAlias(name="Alice") # name is required
|
||||||
|
user_partial = UserWithAlias(name="Alice", age=30)
|
||||||
|
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `UserWithAlias` constructor"
|
||||||
|
user_invalid = UserWithAlias(age=30)
|
||||||
|
|
||||||
|
reveal_type(user_empty["name"]) # revealed: str
|
||||||
|
reveal_type(user_partial["age"]) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shadowing behavior
|
||||||
|
|
||||||
|
When a local class shadows the `TypedDict` import, only the actual `TypedDict` import should be
|
||||||
|
treated as a `TypedDict`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict as TD
|
||||||
|
|
||||||
|
class TypedDict:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NotActualTypedDict(TypedDict, total=True):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class ActualTypedDict(TD, total=True):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
not_td = NotActualTypedDict()
|
||||||
|
reveal_type(not_td) # revealed: NotActualTypedDict
|
||||||
|
|
||||||
|
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `ActualTypedDict` constructor"
|
||||||
|
actual_td = ActualTypedDict()
|
||||||
|
actual_td = ActualTypedDict(name="Alice")
|
||||||
|
reveal_type(actual_td) # revealed: ActualTypedDict
|
||||||
|
reveal_type(actual_td["name"]) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
|
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
|
||||||
|
|
|
@ -574,6 +574,16 @@ impl<'db> PlaceAndQualifiers<'db> {
|
||||||
self.qualifiers.contains(TypeQualifiers::INIT_VAR)
|
self.qualifiers.contains(TypeQualifiers::INIT_VAR)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the place has a `Required` type qualifier.
|
||||||
|
pub(crate) fn is_required(&self) -> bool {
|
||||||
|
self.qualifiers.contains(TypeQualifiers::REQUIRED)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the place has a `NotRequired` type qualifier.
|
||||||
|
pub(crate) fn is_not_required(&self) -> bool {
|
||||||
|
self.qualifiers.contains(TypeQualifiers::NOT_REQUIRED)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type.
|
/// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type.
|
||||||
pub(crate) fn is_bare_final(&self) -> Option<TypeQualifiers> {
|
pub(crate) fn is_bare_final(&self) -> Option<TypeQualifiers> {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
@ -37,7 +37,6 @@ use crate::semantic_index::scope::ScopeId;
|
||||||
use crate::semantic_index::{imported_modules, place_table, semantic_index};
|
use crate::semantic_index::{imported_modules, place_table, semantic_index};
|
||||||
use crate::suppression::check_suppressions;
|
use crate::suppression::check_suppressions;
|
||||||
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
|
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
|
||||||
use crate::types::class::{CodeGeneratorKind, Field};
|
|
||||||
pub(crate) use crate::types::class_base::ClassBase;
|
pub(crate) use crate::types::class_base::ClassBase;
|
||||||
use crate::types::constraints::{
|
use crate::types::constraints::{
|
||||||
Constraints, IteratorConstraintsExtension, OptionConstraintsExtension,
|
Constraints, IteratorConstraintsExtension, OptionConstraintsExtension,
|
||||||
|
@ -63,10 +62,11 @@ use crate::types::mro::{Mro, MroError, MroIterator};
|
||||||
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
|
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
|
||||||
use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature};
|
use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature};
|
||||||
use crate::types::tuple::TupleSpec;
|
use crate::types::tuple::TupleSpec;
|
||||||
|
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
|
||||||
use crate::types::variance::{TypeVarVariance, VarianceInferable};
|
use crate::types::variance::{TypeVarVariance, VarianceInferable};
|
||||||
use crate::unpack::EvaluationMode;
|
use crate::unpack::EvaluationMode;
|
||||||
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
|
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
|
||||||
use crate::{Db, FxOrderMap, FxOrderSet, Module, Program};
|
use crate::{Db, FxOrderSet, Module, Program};
|
||||||
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
|
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
|
||||||
use instance::Protocol;
|
use instance::Protocol;
|
||||||
pub use instance::{NominalInstanceType, ProtocolInstanceType};
|
pub use instance::{NominalInstanceType, ProtocolInstanceType};
|
||||||
|
@ -96,6 +96,7 @@ mod string_annotation;
|
||||||
mod subclass_of;
|
mod subclass_of;
|
||||||
mod tuple;
|
mod tuple;
|
||||||
mod type_ordering;
|
mod type_ordering;
|
||||||
|
mod typed_dict;
|
||||||
mod unpacker;
|
mod unpacker;
|
||||||
mod variance;
|
mod variance;
|
||||||
mod visitor;
|
mod visitor;
|
||||||
|
@ -1039,9 +1040,7 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn typed_dict(defining_class: impl Into<ClassType<'db>>) -> Self {
|
pub(crate) fn typed_dict(defining_class: impl Into<ClassType<'db>>) -> Self {
|
||||||
Self::TypedDict(TypedDictType {
|
Self::TypedDict(TypedDictType::new(defining_class.into()))
|
||||||
defining_class: defining_class.into(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
@ -5899,7 +5898,7 @@ impl<'db> Type<'db> {
|
||||||
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
|
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
|
||||||
Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db),
|
Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db),
|
||||||
Type::ProtocolInstance(protocol) => protocol.to_meta_type(db),
|
Type::ProtocolInstance(protocol) => protocol.to_meta_type(db),
|
||||||
Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class),
|
Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class()),
|
||||||
Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db),
|
Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6366,7 +6365,7 @@ impl<'db> Type<'db> {
|
||||||
},
|
},
|
||||||
|
|
||||||
Type::TypedDict(typed_dict) => {
|
Type::TypedDict(typed_dict) => {
|
||||||
Some(TypeDefinition::Class(typed_dict.defining_class.definition(db)))
|
Some(TypeDefinition::Class(typed_dict.defining_class().definition(db)))
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::Union(_) | Self::Intersection(_) => None,
|
Self::Union(_) | Self::Intersection(_) => None,
|
||||||
|
@ -6879,6 +6878,12 @@ bitflags! {
|
||||||
const FINAL = 1 << 1;
|
const FINAL = 1 << 1;
|
||||||
/// `dataclasses.InitVar`
|
/// `dataclasses.InitVar`
|
||||||
const INIT_VAR = 1 << 2;
|
const INIT_VAR = 1 << 2;
|
||||||
|
/// `typing_extensions.Required`
|
||||||
|
const REQUIRED = 1 << 3;
|
||||||
|
/// `typing_extensions.NotRequired`
|
||||||
|
const NOT_REQUIRED = 1 << 4;
|
||||||
|
/// `typing_extensions.ReadOnly`
|
||||||
|
const READ_ONLY = 1 << 5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6894,6 +6899,8 @@ impl TypeQualifiers {
|
||||||
Self::CLASS_VAR => "ClassVar",
|
Self::CLASS_VAR => "ClassVar",
|
||||||
Self::FINAL => "Final",
|
Self::FINAL => "Final",
|
||||||
Self::INIT_VAR => "InitVar",
|
Self::INIT_VAR => "InitVar",
|
||||||
|
Self::REQUIRED => "Required",
|
||||||
|
Self::NOT_REQUIRED => "NotRequired",
|
||||||
_ => {
|
_ => {
|
||||||
unreachable!("Only a single bit should be set when calling `TypeQualifiers::name`")
|
unreachable!("Only a single bit should be set when calling `TypeQualifiers::name`")
|
||||||
}
|
}
|
||||||
|
@ -9849,43 +9856,6 @@ impl<'db> EnumLiteralType<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type that represents the set of all inhabitants (`dict` instances) that conform to
|
|
||||||
/// a given `TypedDict` schema.
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash, get_size2::GetSize)]
|
|
||||||
pub struct TypedDictType<'db> {
|
|
||||||
/// A reference to the class (inheriting from `typing.TypedDict`) that specifies the
|
|
||||||
/// schema of this `TypedDict`.
|
|
||||||
defining_class: ClassType<'db>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'db> TypedDictType<'db> {
|
|
||||||
pub(crate) fn items(self, db: &'db dyn Db) -> FxOrderMap<Name, Field<'db>> {
|
|
||||||
let (class_literal, specialization) = self.defining_class.class_literal(db);
|
|
||||||
class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn apply_type_mapping_impl<'a>(
|
|
||||||
self,
|
|
||||||
db: &'db dyn Db,
|
|
||||||
type_mapping: &TypeMapping<'a, 'db>,
|
|
||||||
visitor: &ApplyTypeMappingVisitor<'db>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
defining_class: self
|
|
||||||
.defining_class
|
|
||||||
.apply_type_mapping_impl(db, type_mapping, visitor),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
|
||||||
db: &'db dyn Db,
|
|
||||||
typed_dict: TypedDictType<'db>,
|
|
||||||
visitor: &V,
|
|
||||||
) {
|
|
||||||
visitor.visit_type(db, typed_dict.defining_class.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(crate) enum BoundSuperError<'db> {
|
pub(crate) enum BoundSuperError<'db> {
|
||||||
InvalidPivotClassType {
|
InvalidPivotClassType {
|
||||||
|
|
|
@ -27,13 +27,14 @@ use crate::types::generics::{GenericContext, Specialization, walk_specialization
|
||||||
use crate::types::infer::nearest_enclosing_class;
|
use crate::types::infer::nearest_enclosing_class;
|
||||||
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
|
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
|
||||||
use crate::types::tuple::{TupleSpec, TupleType};
|
use crate::types::tuple::{TupleSpec, TupleType};
|
||||||
|
use crate::types::typed_dict::typed_dict_params_from_class_def;
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType,
|
ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType,
|
||||||
DataclassParams, DeprecatedInstance, HasRelationToVisitor, IsEquivalentVisitor,
|
DataclassParams, DeprecatedInstance, HasRelationToVisitor, IsEquivalentVisitor,
|
||||||
KnownInstanceType, ManualPEP695TypeAliasType, NormalizedVisitor, PropertyInstanceType,
|
KnownInstanceType, ManualPEP695TypeAliasType, NormalizedVisitor, PropertyInstanceType,
|
||||||
StringLiteralType, TypeAliasType, TypeMapping, TypeRelation, TypeVarBoundOrConstraints,
|
StringLiteralType, TypeAliasType, TypeMapping, TypeRelation, TypeVarBoundOrConstraints,
|
||||||
TypeVarInstance, TypeVarKind, VarianceInferable, declaration_type, infer_definition_types,
|
TypeVarInstance, TypeVarKind, TypedDictParams, VarianceInferable, declaration_type,
|
||||||
todo_type,
|
infer_definition_types, todo_type,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
Db, FxIndexMap, FxOrderSet, Program,
|
Db, FxIndexMap, FxOrderSet, Program,
|
||||||
|
@ -1241,24 +1242,51 @@ impl MethodDecorator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Kind-specific metadata for different types of fields
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) enum FieldKind<'db> {
|
||||||
|
/// `NamedTuple` field metadata
|
||||||
|
NamedTuple { default_ty: Option<Type<'db>> },
|
||||||
|
/// dataclass field metadata
|
||||||
|
Dataclass {
|
||||||
|
/// The type of the default value for this field
|
||||||
|
default_ty: Option<Type<'db>>,
|
||||||
|
/// Whether or not this field is "init-only". If this is true, it only appears in the
|
||||||
|
/// `__init__` signature, but is not accessible as a real field
|
||||||
|
init_only: bool,
|
||||||
|
/// Whether or not this field should appear in the signature of `__init__`.
|
||||||
|
init: bool,
|
||||||
|
/// Whether or not this field can only be passed as a keyword argument to `__init__`.
|
||||||
|
kw_only: Option<bool>,
|
||||||
|
},
|
||||||
|
/// `TypedDict` field metadata
|
||||||
|
TypedDict {
|
||||||
|
/// Whether this field is required
|
||||||
|
is_required: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/// Metadata regarding a dataclass field/attribute or a `TypedDict` "item" / key-value pair.
|
/// Metadata regarding a dataclass field/attribute or a `TypedDict` "item" / key-value pair.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(crate) struct Field<'db> {
|
pub(crate) struct Field<'db> {
|
||||||
/// The declared type of the field
|
/// The declared type of the field
|
||||||
pub(crate) declared_ty: Type<'db>,
|
pub(crate) declared_ty: Type<'db>,
|
||||||
|
/// Kind-specific metadata for this field
|
||||||
|
pub(crate) kind: FieldKind<'db>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The type of the default value for this field
|
impl Field<'_> {
|
||||||
pub(crate) default_ty: Option<Type<'db>>,
|
pub(crate) const fn is_required(&self) -> bool {
|
||||||
|
match &self.kind {
|
||||||
/// Whether or not this field is "init-only". If this is true, it only appears in the
|
FieldKind::NamedTuple { default_ty } => default_ty.is_none(),
|
||||||
/// `__init__` signature, but is not accessible as a real field
|
// A dataclass field is NOT required if `default` (or `default_factory`) is set
|
||||||
pub(crate) init_only: bool,
|
// or if `init` has been set to `False`.
|
||||||
|
FieldKind::Dataclass {
|
||||||
/// Whether or not this field should appear in the signature of `__init__`.
|
init, default_ty, ..
|
||||||
pub(crate) init: bool,
|
} => default_ty.is_none() && *init,
|
||||||
|
FieldKind::TypedDict { is_required } => *is_required,
|
||||||
/// Whether or not this field can only be passed as a keyword argument to `__init__`.
|
}
|
||||||
pub(crate) kw_only: Option<bool>,
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> Field<'db> {
|
impl<'db> Field<'db> {
|
||||||
|
@ -1664,6 +1692,17 @@ impl<'db> ClassLiteral<'db> {
|
||||||
.any(|base| matches!(base, ClassBase::TypedDict))
|
.any(|base| matches!(base, ClassBase::TypedDict))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute `TypedDict` parameters dynamically based on MRO detection and AST parsing.
|
||||||
|
fn typed_dict_params(self, db: &'db dyn Db) -> Option<TypedDictParams> {
|
||||||
|
if !self.is_typed_dict(db) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let module = parsed_module(db, self.file(db)).load(db);
|
||||||
|
let class_stmt = self.node(db, &module);
|
||||||
|
Some(typed_dict_params_from_class_def(class_stmt))
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the explicit `metaclass` of this class, if one is defined.
|
/// Return the explicit `metaclass` of this class, if one is defined.
|
||||||
///
|
///
|
||||||
/// ## Note
|
/// ## Note
|
||||||
|
@ -1967,7 +2006,10 @@ impl<'db> ClassLiteral<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if CodeGeneratorKind::NamedTuple.matches(db, self) {
|
if CodeGeneratorKind::NamedTuple.matches(db, self) {
|
||||||
if let Some(field) = self.own_fields(db, specialization).get(name) {
|
if let Some(field) = self
|
||||||
|
.own_fields(db, specialization, CodeGeneratorKind::NamedTuple)
|
||||||
|
.get(name)
|
||||||
|
{
|
||||||
let property_getter_signature = Signature::new(
|
let property_getter_signature = Signature::new(
|
||||||
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]),
|
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]),
|
||||||
Some(field.declared_ty),
|
Some(field.declared_ty),
|
||||||
|
@ -2033,17 +2075,19 @@ impl<'db> ClassLiteral<'db> {
|
||||||
Type::instance(db, self.apply_optional_specialization(db, specialization));
|
Type::instance(db, self.apply_optional_specialization(db, specialization));
|
||||||
|
|
||||||
let signature_from_fields = |mut parameters: Vec<_>, return_ty: Option<Type<'db>>| {
|
let signature_from_fields = |mut parameters: Vec<_>, return_ty: Option<Type<'db>>| {
|
||||||
for (
|
for (field_name, field) in self.fields(db, specialization, field_policy) {
|
||||||
field_name,
|
let (init, mut default_ty, kw_only) = match field.kind {
|
||||||
field @ Field {
|
FieldKind::NamedTuple { default_ty } => (true, default_ty, None),
|
||||||
declared_ty: mut field_ty,
|
FieldKind::Dataclass {
|
||||||
mut default_ty,
|
|
||||||
init_only: _,
|
|
||||||
init,
|
init,
|
||||||
|
default_ty,
|
||||||
kw_only,
|
kw_only,
|
||||||
},
|
..
|
||||||
) in self.fields(db, specialization, field_policy)
|
} => (init, default_ty, kw_only),
|
||||||
{
|
FieldKind::TypedDict { .. } => continue,
|
||||||
|
};
|
||||||
|
let mut field_ty = field.declared_ty;
|
||||||
|
|
||||||
if name == "__init__" && !init {
|
if name == "__init__" && !init {
|
||||||
// Skip fields with `init=False`
|
// Skip fields with `init=False`
|
||||||
continue;
|
continue;
|
||||||
|
@ -2351,7 +2395,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
if field_policy == CodeGeneratorKind::NamedTuple {
|
if field_policy == CodeGeneratorKind::NamedTuple {
|
||||||
// NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the
|
// NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the
|
||||||
// fields of this class only.
|
// fields of this class only.
|
||||||
return self.own_fields(db, specialization);
|
return self.own_fields(db, specialization, field_policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
let matching_classes_in_mro: Vec<_> = self
|
let matching_classes_in_mro: Vec<_> = self
|
||||||
|
@ -2374,7 +2418,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
matching_classes_in_mro
|
matching_classes_in_mro
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.rev()
|
.rev()
|
||||||
.flat_map(|(class, specialization)| class.own_fields(db, specialization))
|
.flat_map(|(class, specialization)| class.own_fields(db, specialization, field_policy))
|
||||||
// We collect into a FxOrderMap here to deduplicate attributes
|
// We collect into a FxOrderMap here to deduplicate attributes
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -2394,6 +2438,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
self,
|
self,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
specialization: Option<Specialization<'db>>,
|
specialization: Option<Specialization<'db>>,
|
||||||
|
field_policy: CodeGeneratorKind,
|
||||||
) -> FxOrderMap<Name, Field<'db>> {
|
) -> FxOrderMap<Name, Field<'db>> {
|
||||||
let mut attributes = FxOrderMap::default();
|
let mut attributes = FxOrderMap::default();
|
||||||
|
|
||||||
|
@ -2401,7 +2446,10 @@ impl<'db> ClassLiteral<'db> {
|
||||||
let table = place_table(db, class_body_scope);
|
let table = place_table(db, class_body_scope);
|
||||||
|
|
||||||
let use_def = use_def_map(db, class_body_scope);
|
let use_def = use_def_map(db, class_body_scope);
|
||||||
|
|
||||||
|
let typed_dict_params = self.typed_dict_params(db);
|
||||||
let mut kw_only_sentinel_field_seen = false;
|
let mut kw_only_sentinel_field_seen = false;
|
||||||
|
|
||||||
for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() {
|
for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() {
|
||||||
// Here, we exclude all declarations that are not annotated assignments. We need this because
|
// Here, we exclude all declarations that are not annotated assignments. We need this because
|
||||||
// things like function definitions and nested classes would otherwise be considered dataclass
|
// things like function definitions and nested classes would otherwise be considered dataclass
|
||||||
|
@ -2456,12 +2504,34 @@ impl<'db> ClassLiteral<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut field = Field {
|
let kind = match field_policy {
|
||||||
declared_ty: attr_ty.apply_optional_specialization(db, specialization),
|
CodeGeneratorKind::NamedTuple => FieldKind::NamedTuple { default_ty },
|
||||||
|
CodeGeneratorKind::DataclassLike => FieldKind::Dataclass {
|
||||||
default_ty,
|
default_ty,
|
||||||
init_only: attr.is_init_var(),
|
init_only: attr.is_init_var(),
|
||||||
init,
|
init,
|
||||||
kw_only,
|
kw_only,
|
||||||
|
},
|
||||||
|
CodeGeneratorKind::TypedDict => {
|
||||||
|
let is_required = if attr.is_required() {
|
||||||
|
// Explicit Required[T] annotation - always required
|
||||||
|
true
|
||||||
|
} else if attr.is_not_required() {
|
||||||
|
// Explicit NotRequired[T] annotation - never required
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
// No explicit qualifier - use class default (`total` parameter)
|
||||||
|
typed_dict_params
|
||||||
|
.expect("TypedDictParams should be available for CodeGeneratorKind::TypedDict")
|
||||||
|
.contains(TypedDictParams::TOTAL)
|
||||||
|
};
|
||||||
|
FieldKind::TypedDict { is_required }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut field = Field {
|
||||||
|
declared_ty: attr_ty.apply_optional_specialization(db, specialization),
|
||||||
|
kind,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if this is a KW_ONLY sentinel and mark subsequent fields as keyword-only
|
// Check if this is a KW_ONLY sentinel and mark subsequent fields as keyword-only
|
||||||
|
@ -2470,8 +2540,14 @@ impl<'db> ClassLiteral<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no explicit kw_only setting and we've seen KW_ONLY sentinel, mark as keyword-only
|
// If no explicit kw_only setting and we've seen KW_ONLY sentinel, mark as keyword-only
|
||||||
if field.kw_only.is_none() && kw_only_sentinel_field_seen {
|
if kw_only_sentinel_field_seen {
|
||||||
field.kw_only = Some(true);
|
if let FieldKind::Dataclass {
|
||||||
|
kw_only: ref mut kw @ None,
|
||||||
|
..
|
||||||
|
} = field.kind
|
||||||
|
{
|
||||||
|
*kw = Some(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes.insert(symbol.name().clone(), field);
|
attributes.insert(symbol.name().clone(), field);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
|
use crate::types::class::CodeGeneratorKind;
|
||||||
use crate::types::generics::Specialization;
|
use crate::types::generics::Specialization;
|
||||||
use crate::types::tuple::TupleType;
|
use crate::types::tuple::TupleType;
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
|
@ -206,7 +207,7 @@ impl<'db> ClassBase<'db> {
|
||||||
SpecialFormType::Generic => Some(Self::Generic),
|
SpecialFormType::Generic => Some(Self::Generic),
|
||||||
|
|
||||||
SpecialFormType::NamedTuple => {
|
SpecialFormType::NamedTuple => {
|
||||||
let fields = subclass.own_fields(db, None);
|
let fields = subclass.own_fields(db, None, CodeGeneratorKind::NamedTuple);
|
||||||
Self::try_from_type(
|
Self::try_from_type(
|
||||||
db,
|
db,
|
||||||
TupleType::heterogeneous(
|
TupleType::heterogeneous(
|
||||||
|
|
|
@ -96,6 +96,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||||
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
|
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
|
||||||
registry.register_lint(&REDUNDANT_CAST);
|
registry.register_lint(&REDUNDANT_CAST);
|
||||||
registry.register_lint(&UNRESOLVED_GLOBAL);
|
registry.register_lint(&UNRESOLVED_GLOBAL);
|
||||||
|
registry.register_lint(&MISSING_TYPED_DICT_KEY);
|
||||||
|
|
||||||
// String annotations
|
// String annotations
|
||||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||||
|
@ -1758,6 +1759,33 @@ declare_lint! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare_lint! {
|
||||||
|
/// ## What it does
|
||||||
|
/// Detects missing required keys in `TypedDict` constructor calls.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// `TypedDict` requires all non-optional keys to be provided during construction.
|
||||||
|
/// Missing items can lead to a `KeyError` at runtime.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```python
|
||||||
|
/// from typing import TypedDict
|
||||||
|
///
|
||||||
|
/// class Person(TypedDict):
|
||||||
|
/// name: str
|
||||||
|
/// age: int
|
||||||
|
///
|
||||||
|
/// alice: Person = {"name": "Alice"} # missing required key 'age'
|
||||||
|
///
|
||||||
|
/// alice["age"] # KeyError
|
||||||
|
/// ```
|
||||||
|
pub(crate) static MISSING_TYPED_DICT_KEY = {
|
||||||
|
summary: "detects missing required keys in `TypedDict` constructors",
|
||||||
|
status: LintStatus::preview("1.0.0"),
|
||||||
|
default_level: Level::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A collection of type check diagnostics.
|
/// A collection of type check diagnostics.
|
||||||
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
|
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
|
||||||
pub struct TypeCheckDiagnostics {
|
pub struct TypeCheckDiagnostics {
|
||||||
|
@ -2761,18 +2789,18 @@ fn report_invalid_base<'ctx, 'db>(
|
||||||
|
|
||||||
pub(crate) fn report_invalid_key_on_typed_dict<'db>(
|
pub(crate) fn report_invalid_key_on_typed_dict<'db>(
|
||||||
context: &InferContext<'db, '_>,
|
context: &InferContext<'db, '_>,
|
||||||
value_node: AnyNodeRef,
|
typed_dict_node: AnyNodeRef,
|
||||||
slice_node: AnyNodeRef,
|
key_node: AnyNodeRef,
|
||||||
value_ty: Type<'db>,
|
typed_dict_ty: Type<'db>,
|
||||||
slice_ty: Type<'db>,
|
key_ty: Type<'db>,
|
||||||
items: &FxOrderMap<Name, Field<'db>>,
|
items: &FxOrderMap<Name, Field<'db>>,
|
||||||
) {
|
) {
|
||||||
let db = context.db();
|
let db = context.db();
|
||||||
if let Some(builder) = context.report_lint(&INVALID_KEY, slice_node) {
|
if let Some(builder) = context.report_lint(&INVALID_KEY, key_node) {
|
||||||
match slice_ty {
|
match key_ty {
|
||||||
Type::StringLiteral(key) => {
|
Type::StringLiteral(key) => {
|
||||||
let key = key.value(db);
|
let key = key.value(db);
|
||||||
let typed_dict_name = value_ty.display(db);
|
let typed_dict_name = typed_dict_ty.display(db);
|
||||||
|
|
||||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||||
"Invalid key access on TypedDict `{typed_dict_name}`",
|
"Invalid key access on TypedDict `{typed_dict_name}`",
|
||||||
|
@ -2780,7 +2808,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
|
||||||
|
|
||||||
diagnostic.annotate(
|
diagnostic.annotate(
|
||||||
context
|
context
|
||||||
.secondary(value_node)
|
.secondary(typed_dict_node)
|
||||||
.message(format_args!("TypedDict `{typed_dict_name}`")),
|
.message(format_args!("TypedDict `{typed_dict_name}`")),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2799,8 +2827,8 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
|
||||||
}
|
}
|
||||||
_ => builder.into_diagnostic(format_args!(
|
_ => builder.into_diagnostic(format_args!(
|
||||||
"TypedDict `{}` cannot be indexed with a key of type `{}`",
|
"TypedDict `{}` cannot be indexed with a key of type `{}`",
|
||||||
value_ty.display(db),
|
typed_dict_ty.display(db),
|
||||||
slice_ty.display(db),
|
key_ty.display(db),
|
||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2860,6 +2888,21 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn report_missing_typed_dict_key<'db>(
|
||||||
|
context: &InferContext<'db, '_>,
|
||||||
|
constructor_node: AnyNodeRef,
|
||||||
|
typed_dict_ty: Type<'db>,
|
||||||
|
missing_field: &str,
|
||||||
|
) {
|
||||||
|
let db = context.db();
|
||||||
|
if let Some(builder) = context.report_lint(&MISSING_TYPED_DICT_KEY, constructor_node) {
|
||||||
|
let typed_dict_name = typed_dict_ty.display(db);
|
||||||
|
builder.into_diagnostic(format_args!(
|
||||||
|
"Missing required key '{missing_field}' in TypedDict `{typed_dict_name}` constructor",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// This function receives an unresolved `from foo import bar` import,
|
/// This function receives an unresolved `from foo import bar` import,
|
||||||
/// where `foo` can be resolved to a module but that module does not
|
/// where `foo` can be resolved to a module but that module does not
|
||||||
/// have a `bar` member or submodule.
|
/// have a `bar` member or submodule.
|
||||||
|
|
|
@ -315,7 +315,7 @@ impl Display for DisplayRepresentation<'_> {
|
||||||
}
|
}
|
||||||
f.write_str("]")
|
f.write_str("]")
|
||||||
}
|
}
|
||||||
Type::TypedDict(typed_dict) => f.write_str(typed_dict.defining_class.name(self.db)),
|
Type::TypedDict(typed_dict) => f.write_str(typed_dict.defining_class().name(self.db)),
|
||||||
Type::TypeAlias(alias) => f.write_str(alias.name(self.db)),
|
Type::TypeAlias(alias) => f.write_str(alias.name(self.db)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ use crate::semantic_index::{
|
||||||
ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, semantic_index,
|
ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, semantic_index,
|
||||||
};
|
};
|
||||||
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
|
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
|
||||||
use crate::types::class::{CodeGeneratorKind, MetaclassErrorKind};
|
use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind};
|
||||||
use crate::types::diagnostic::{
|
use crate::types::diagnostic::{
|
||||||
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
|
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
|
||||||
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
|
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
|
||||||
|
@ -117,6 +117,10 @@ use crate::types::instance::SliceLiteral;
|
||||||
use crate::types::mro::MroErrorKind;
|
use crate::types::mro::MroErrorKind;
|
||||||
use crate::types::signatures::{CallableSignature, Signature};
|
use crate::types::signatures::{CallableSignature, Signature};
|
||||||
use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder, TupleType};
|
use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder, TupleType};
|
||||||
|
use crate::types::typed_dict::{
|
||||||
|
TypedDictAssignmentKind, validate_typed_dict_constructor, validate_typed_dict_dict_literal,
|
||||||
|
validate_typed_dict_key_assignment,
|
||||||
|
};
|
||||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType,
|
CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType,
|
||||||
|
@ -1118,8 +1122,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
if is_named_tuple {
|
if is_named_tuple {
|
||||||
let mut field_with_default_encountered = None;
|
let mut field_with_default_encountered = None;
|
||||||
|
|
||||||
for (field_name, field) in class.own_fields(self.db(), None) {
|
for (field_name, field) in
|
||||||
if field.default_ty.is_some() {
|
class.own_fields(self.db(), None, CodeGeneratorKind::NamedTuple)
|
||||||
|
{
|
||||||
|
if matches!(
|
||||||
|
field.kind,
|
||||||
|
FieldKind::NamedTuple {
|
||||||
|
default_ty: Some(_)
|
||||||
|
}
|
||||||
|
) {
|
||||||
field_with_default_encountered = Some(field_name);
|
field_with_default_encountered = Some(field_name);
|
||||||
} else if let Some(field_with_default) = field_with_default_encountered.as_ref()
|
} else if let Some(field_with_default) = field_with_default_encountered.as_ref()
|
||||||
{
|
{
|
||||||
|
@ -3804,47 +3815,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
if let Some(typed_dict) = value_ty.into_typed_dict() {
|
if let Some(typed_dict) = value_ty.into_typed_dict() {
|
||||||
if let Some(key) = slice_ty.into_string_literal() {
|
if let Some(key) = slice_ty.into_string_literal() {
|
||||||
let key = key.value(self.db());
|
let key = key.value(self.db());
|
||||||
let items = typed_dict.items(self.db());
|
validate_typed_dict_key_assignment(
|
||||||
if let Some((_, item)) =
|
|
||||||
items.iter().find(|(name, _)| *name == key)
|
|
||||||
{
|
|
||||||
if let Some(builder) =
|
|
||||||
context.report_lint(&INVALID_ASSIGNMENT, rhs)
|
|
||||||
{
|
|
||||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
|
||||||
"Invalid assignment to key \"{key}\" with declared type `{}` on TypedDict `{value_d}`",
|
|
||||||
item.declared_ty.display(db),
|
|
||||||
));
|
|
||||||
|
|
||||||
diagnostic.set_primary_message(format_args!(
|
|
||||||
"value of type `{assigned_d}`"
|
|
||||||
));
|
|
||||||
|
|
||||||
diagnostic.annotate(
|
|
||||||
self.context
|
|
||||||
.secondary(value.as_ref())
|
|
||||||
.message(format_args!("TypedDict `{value_d}`")),
|
|
||||||
);
|
|
||||||
|
|
||||||
diagnostic.annotate(
|
|
||||||
self.context.secondary(slice.as_ref()).message(
|
|
||||||
format_args!(
|
|
||||||
"key has declared type `{}`",
|
|
||||||
item.declared_ty.display(db),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
report_invalid_key_on_typed_dict(
|
|
||||||
&self.context,
|
&self.context,
|
||||||
value.as_ref().into(),
|
typed_dict,
|
||||||
slice.as_ref().into(),
|
key,
|
||||||
value_ty,
|
assigned_ty,
|
||||||
slice_ty,
|
value.as_ref(),
|
||||||
&items,
|
slice.as_ref(),
|
||||||
|
rhs,
|
||||||
|
TypedDictAssignmentKind::Subscript,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Check if the key has a valid type. We only allow string literals, a union of string literals,
|
// Check if the key has a valid type. We only allow string literals, a union of string literals,
|
||||||
// or a dynamic type like `Any`. We can do this by checking assignability to `LiteralString`,
|
// or a dynamic type like `Any`. We can do this by checking assignability to `LiteralString`,
|
||||||
|
@ -4695,7 +4675,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
|
|
||||||
if let Some(value) = value {
|
if let Some(value) = value {
|
||||||
let inferred_ty = self.infer_maybe_standalone_expression(value);
|
let inferred_ty = self.infer_maybe_standalone_expression(value);
|
||||||
let inferred_ty = if target
|
let mut inferred_ty = if target
|
||||||
.as_name_expr()
|
.as_name_expr()
|
||||||
.is_some_and(|name| &name.id == "TYPE_CHECKING")
|
.is_some_and(|name| &name.id == "TYPE_CHECKING")
|
||||||
{
|
{
|
||||||
|
@ -4705,6 +4685,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
} else {
|
} else {
|
||||||
inferred_ty
|
inferred_ty
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Validate `TypedDict` dictionary literal assignments
|
||||||
|
if let Some(typed_dict) = declared.inner_type().into_typed_dict() {
|
||||||
|
if let Some(dict_expr) = value.as_dict_expr() {
|
||||||
|
validate_typed_dict_dict_literal(
|
||||||
|
&self.context,
|
||||||
|
typed_dict,
|
||||||
|
dict_expr,
|
||||||
|
target.into(),
|
||||||
|
|expr| self.expression_type(expr),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Override the inferred type of the dict literal to be the `TypedDict` type
|
||||||
|
// This ensures that the dict literal gets the correct type for key access
|
||||||
|
let typed_dict_type = Type::TypedDict(typed_dict);
|
||||||
|
inferred_ty = typed_dict_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.add_declaration_with_binding(
|
self.add_declaration_with_binding(
|
||||||
target.into(),
|
target.into(),
|
||||||
definition,
|
definition,
|
||||||
|
@ -6269,6 +6268,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
.match_parameters(&call_arguments);
|
.match_parameters(&call_arguments);
|
||||||
self.infer_argument_types(arguments, &mut call_arguments, &bindings.argument_forms);
|
self.infer_argument_types(arguments, &mut call_arguments, &bindings.argument_forms);
|
||||||
|
|
||||||
|
// Validate `TypedDict` constructor calls after argument type inference
|
||||||
|
if let Some(class_literal) = callable_type.into_class_literal() {
|
||||||
|
if class_literal.is_typed_dict(self.db()) {
|
||||||
|
let typed_dict_type = Type::typed_dict(ClassType::NonGeneric(class_literal));
|
||||||
|
if let Some(typed_dict) = typed_dict_type.into_typed_dict() {
|
||||||
|
validate_typed_dict_constructor(
|
||||||
|
&self.context,
|
||||||
|
typed_dict,
|
||||||
|
arguments,
|
||||||
|
func.as_ref().into(),
|
||||||
|
|expr| self.expression_type(expr),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut bindings = match bindings.check_types(self.db(), &call_arguments) {
|
let mut bindings = match bindings.check_types(self.db(), &call_arguments) {
|
||||||
Ok(bindings) => bindings,
|
Ok(bindings) => bindings,
|
||||||
Err(CallError(_, bindings)) => {
|
Err(CallError(_, bindings)) => {
|
||||||
|
@ -9422,6 +9437,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
Type::SpecialForm(SpecialFormType::Final) => {
|
Type::SpecialForm(SpecialFormType::Final) => {
|
||||||
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL)
|
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL)
|
||||||
}
|
}
|
||||||
|
Type::SpecialForm(SpecialFormType::Required) => {
|
||||||
|
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::REQUIRED)
|
||||||
|
}
|
||||||
|
Type::SpecialForm(SpecialFormType::NotRequired) => {
|
||||||
|
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::NOT_REQUIRED)
|
||||||
|
}
|
||||||
|
Type::SpecialForm(SpecialFormType::ReadOnly) => {
|
||||||
|
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::READ_ONLY)
|
||||||
|
}
|
||||||
Type::ClassLiteral(class)
|
Type::ClassLiteral(class)
|
||||||
if class.is_known(self.db(), KnownClass::InitVar) =>
|
if class.is_known(self.db(), KnownClass::InitVar) =>
|
||||||
{
|
{
|
||||||
|
@ -9497,7 +9521,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::SpecialForm(
|
Type::SpecialForm(
|
||||||
type_qualifier @ (SpecialFormType::ClassVar | SpecialFormType::Final),
|
type_qualifier @ (SpecialFormType::ClassVar
|
||||||
|
| SpecialFormType::Final
|
||||||
|
| SpecialFormType::Required
|
||||||
|
| SpecialFormType::NotRequired
|
||||||
|
| SpecialFormType::ReadOnly),
|
||||||
) => {
|
) => {
|
||||||
let arguments = if let ast::Expr::Tuple(tuple) = slice {
|
let arguments = if let ast::Expr::Tuple(tuple) = slice {
|
||||||
&*tuple.elts
|
&*tuple.elts
|
||||||
|
@ -9516,6 +9544,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
SpecialFormType::Final => {
|
SpecialFormType::Final => {
|
||||||
type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL);
|
type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL);
|
||||||
}
|
}
|
||||||
|
SpecialFormType::Required => {
|
||||||
|
type_and_qualifiers.add_qualifier(TypeQualifiers::REQUIRED);
|
||||||
|
}
|
||||||
|
SpecialFormType::NotRequired => {
|
||||||
|
type_and_qualifiers.add_qualifier(TypeQualifiers::NOT_REQUIRED);
|
||||||
|
}
|
||||||
|
SpecialFormType::ReadOnly => {
|
||||||
|
type_and_qualifiers.add_qualifier(TypeQualifiers::READ_ONLY);
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
type_and_qualifiers
|
type_and_qualifiers
|
||||||
|
@ -10802,15 +10839,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
KnownClass::Deque,
|
KnownClass::Deque,
|
||||||
),
|
),
|
||||||
|
|
||||||
SpecialFormType::ReadOnly => {
|
SpecialFormType::ClassVar
|
||||||
self.infer_type_expression(arguments_slice);
|
| SpecialFormType::Final
|
||||||
todo_type!("`ReadOnly[]` type qualifier")
|
| SpecialFormType::Required
|
||||||
}
|
| SpecialFormType::NotRequired
|
||||||
SpecialFormType::NotRequired => {
|
| SpecialFormType::ReadOnly => {
|
||||||
self.infer_type_expression(arguments_slice);
|
|
||||||
todo_type!("`NotRequired[]` type qualifier")
|
|
||||||
}
|
|
||||||
SpecialFormType::ClassVar | SpecialFormType::Final => {
|
|
||||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
|
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
|
||||||
let diag = builder.into_diagnostic(format_args!(
|
let diag = builder.into_diagnostic(format_args!(
|
||||||
"Type qualifier `{special_form}` is not allowed in type expressions \
|
"Type qualifier `{special_form}` is not allowed in type expressions \
|
||||||
|
@ -10820,10 +10853,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
}
|
}
|
||||||
self.infer_type_expression(arguments_slice)
|
self.infer_type_expression(arguments_slice)
|
||||||
}
|
}
|
||||||
SpecialFormType::Required => {
|
|
||||||
self.infer_type_expression(arguments_slice);
|
|
||||||
todo_type!("`Required[]` type qualifier")
|
|
||||||
}
|
|
||||||
SpecialFormType::TypeIs => match arguments_slice {
|
SpecialFormType::TypeIs => match arguments_slice {
|
||||||
ast::Expr::Tuple(_) => {
|
ast::Expr::Tuple(_) => {
|
||||||
self.infer_type_expression(arguments_slice);
|
self.infer_type_expression(arguments_slice);
|
||||||
|
|
|
@ -245,7 +245,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::TypedDict(left), Type::TypedDict(right)) => {
|
(Type::TypedDict(left), Type::TypedDict(right)) => {
|
||||||
left.defining_class.cmp(&right.defining_class)
|
left.defining_class().cmp(&right.defining_class())
|
||||||
}
|
}
|
||||||
(Type::TypedDict(_), _) => Ordering::Less,
|
(Type::TypedDict(_), _) => Ordering::Less,
|
||||||
(_, Type::TypedDict(_)) => Ordering::Greater,
|
(_, Type::TypedDict(_)) => Ordering::Greater,
|
||||||
|
|
361
crates/ty_python_semantic/src/types/typed_dict.rs
Normal file
361
crates/ty_python_semantic/src/types/typed_dict.rs
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
use bitflags::bitflags;
|
||||||
|
use ruff_python_ast::Arguments;
|
||||||
|
use ruff_python_ast::{self as ast, AnyNodeRef, StmtClassDef, name::Name};
|
||||||
|
|
||||||
|
use super::class::{ClassType, CodeGeneratorKind, Field};
|
||||||
|
use super::context::InferContext;
|
||||||
|
use super::diagnostic::{
|
||||||
|
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, report_invalid_key_on_typed_dict,
|
||||||
|
report_missing_typed_dict_key,
|
||||||
|
};
|
||||||
|
use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor};
|
||||||
|
use crate::{Db, FxOrderMap};
|
||||||
|
|
||||||
|
use ordermap::OrderSet;
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Used for `TypedDict` class parameters.
|
||||||
|
/// Keeps track of the keyword arguments that were passed-in during class definition.
|
||||||
|
/// (see https://typing.python.org/en/latest/spec/typeddict.html)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct TypedDictParams: u8 {
|
||||||
|
/// Whether keys are required by default (`total=True`)
|
||||||
|
const TOTAL = 1 << 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl get_size2::GetSize for TypedDictParams {}
|
||||||
|
|
||||||
|
impl Default for TypedDictParams {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::TOTAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type that represents the set of all inhabitants (`dict` instances) that conform to
|
||||||
|
/// a given `TypedDict` schema.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash, get_size2::GetSize)]
|
||||||
|
pub struct TypedDictType<'db> {
|
||||||
|
/// A reference to the class (inheriting from `typing.TypedDict`) that specifies the
|
||||||
|
/// schema of this `TypedDict`.
|
||||||
|
defining_class: ClassType<'db>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> TypedDictType<'db> {
|
||||||
|
pub(crate) fn new(defining_class: ClassType<'db>) -> Self {
|
||||||
|
Self { defining_class }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn defining_class(self) -> ClassType<'db> {
|
||||||
|
self.defining_class
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn items(self, db: &'db dyn Db) -> FxOrderMap<Name, Field<'db>> {
|
||||||
|
let (class_literal, specialization) = self.defining_class.class_literal(db);
|
||||||
|
class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply_type_mapping_impl<'a>(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
type_mapping: &TypeMapping<'a, 'db>,
|
||||||
|
visitor: &ApplyTypeMappingVisitor<'db>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
defining_class: self
|
||||||
|
.defining_class
|
||||||
|
.apply_type_mapping_impl(db, type_mapping, visitor),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||||
|
db: &'db dyn Db,
|
||||||
|
typed_dict: TypedDictType<'db>,
|
||||||
|
visitor: &V,
|
||||||
|
) {
|
||||||
|
visitor.visit_type(db, typed_dict.defining_class.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn typed_dict_params_from_class_def(class_stmt: &StmtClassDef) -> TypedDictParams {
|
||||||
|
let mut typed_dict_params = TypedDictParams::default();
|
||||||
|
|
||||||
|
// Check for `total` keyword argument in the class definition
|
||||||
|
// Note that it is fine to only check for Boolean literals here
|
||||||
|
// (https://typing.python.org/en/latest/spec/typeddict.html#totality)
|
||||||
|
if let Some(arguments) = &class_stmt.arguments {
|
||||||
|
for keyword in &arguments.keywords {
|
||||||
|
if keyword.arg.as_deref() == Some("total")
|
||||||
|
&& matches!(
|
||||||
|
&keyword.value,
|
||||||
|
ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value: false, .. })
|
||||||
|
)
|
||||||
|
{
|
||||||
|
typed_dict_params.remove(TypedDictParams::TOTAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typed_dict_params
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(super) enum TypedDictAssignmentKind {
|
||||||
|
/// For subscript assignments like `d["key"] = value`
|
||||||
|
Subscript,
|
||||||
|
/// For constructor arguments like `MyTypedDict(key=value)`
|
||||||
|
Constructor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypedDictAssignmentKind {
|
||||||
|
fn diagnostic_name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Subscript => "assignment",
|
||||||
|
Self::Constructor => "argument",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnostic_type(self) -> &'static crate::lint::LintMetadata {
|
||||||
|
match self {
|
||||||
|
Self::Subscript => &INVALID_ASSIGNMENT,
|
||||||
|
Self::Constructor => &INVALID_ARGUMENT_TYPE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates assignment of a value to a specific key on a `TypedDict`.
|
||||||
|
/// Returns true if the assignment is valid, false otherwise.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>(
|
||||||
|
context: &InferContext<'db, 'ast>,
|
||||||
|
typed_dict: TypedDictType<'db>,
|
||||||
|
key: &str,
|
||||||
|
value_ty: Type<'db>,
|
||||||
|
typed_dict_node: impl Into<AnyNodeRef<'ast>>,
|
||||||
|
key_node: impl Into<AnyNodeRef<'ast>>,
|
||||||
|
value_node: impl Into<AnyNodeRef<'ast>>,
|
||||||
|
assignment_kind: TypedDictAssignmentKind,
|
||||||
|
) -> bool {
|
||||||
|
let db = context.db();
|
||||||
|
let items = typed_dict.items(db);
|
||||||
|
|
||||||
|
// Check if key exists in `TypedDict`
|
||||||
|
let Some((_, item)) = items.iter().find(|(name, _)| *name == key) else {
|
||||||
|
report_invalid_key_on_typed_dict(
|
||||||
|
context,
|
||||||
|
typed_dict_node.into(),
|
||||||
|
key_node.into(),
|
||||||
|
Type::TypedDict(typed_dict),
|
||||||
|
Type::string_literal(db, key),
|
||||||
|
&items,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Key exists, check if value type is assignable to declared type
|
||||||
|
if value_ty.is_assignable_to(db, item.declared_ty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid assignment - emit diagnostic
|
||||||
|
if let Some(builder) = context.report_lint(assignment_kind.diagnostic_type(), value_node.into())
|
||||||
|
{
|
||||||
|
let typed_dict_ty = Type::TypedDict(typed_dict);
|
||||||
|
let typed_dict_d = typed_dict_ty.display(db);
|
||||||
|
let value_d = value_ty.display(db);
|
||||||
|
let item_type_d = item.declared_ty.display(db);
|
||||||
|
|
||||||
|
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||||
|
"Invalid {} to key \"{key}\" with declared type `{item_type_d}` on TypedDict `{typed_dict_d}`",
|
||||||
|
assignment_kind.diagnostic_name(),
|
||||||
|
));
|
||||||
|
|
||||||
|
diagnostic.set_primary_message(format_args!("value of type `{value_d}`"));
|
||||||
|
|
||||||
|
diagnostic.annotate(
|
||||||
|
context
|
||||||
|
.secondary(typed_dict_node.into())
|
||||||
|
.message(format_args!("TypedDict `{typed_dict_d}`")),
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostic.annotate(
|
||||||
|
context
|
||||||
|
.secondary(key_node.into())
|
||||||
|
.message(format_args!("key has declared type `{item_type_d}`")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that all required keys are provided in a `TypedDict` construction.
|
||||||
|
/// Reports errors for any keys that are required but not provided.
|
||||||
|
pub(super) fn validate_typed_dict_required_keys<'db, 'ast>(
|
||||||
|
context: &InferContext<'db, 'ast>,
|
||||||
|
typed_dict: TypedDictType<'db>,
|
||||||
|
provided_keys: &OrderSet<&str>,
|
||||||
|
error_node: AnyNodeRef<'ast>,
|
||||||
|
) {
|
||||||
|
let db = context.db();
|
||||||
|
let items = typed_dict.items(db);
|
||||||
|
|
||||||
|
let required_keys: OrderSet<&str> = items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(key_name, field)| field.is_required().then_some(key_name.as_str()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for missing_key in required_keys.difference(provided_keys) {
|
||||||
|
report_missing_typed_dict_key(
|
||||||
|
context,
|
||||||
|
error_node,
|
||||||
|
Type::TypedDict(typed_dict),
|
||||||
|
missing_key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn validate_typed_dict_constructor<'db, 'ast>(
|
||||||
|
context: &InferContext<'db, 'ast>,
|
||||||
|
typed_dict: TypedDictType<'db>,
|
||||||
|
arguments: &'ast Arguments,
|
||||||
|
error_node: AnyNodeRef<'ast>,
|
||||||
|
expression_type_fn: impl Fn(&ast::Expr) -> Type<'db>,
|
||||||
|
) {
|
||||||
|
let has_positional_dict = arguments.args.len() == 1 && arguments.args[0].is_dict_expr();
|
||||||
|
|
||||||
|
let provided_keys = if has_positional_dict {
|
||||||
|
validate_from_dict_literal(
|
||||||
|
context,
|
||||||
|
typed_dict,
|
||||||
|
arguments,
|
||||||
|
error_node,
|
||||||
|
&expression_type_fn,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
validate_from_keywords(
|
||||||
|
context,
|
||||||
|
typed_dict,
|
||||||
|
arguments,
|
||||||
|
error_node,
|
||||||
|
&expression_type_fn,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_typed_dict_required_keys(context, typed_dict, &provided_keys, error_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a `TypedDict` constructor call with a single positional dictionary argument
|
||||||
|
/// e.g. `Person({"name": "Alice", "age": 30})`
|
||||||
|
fn validate_from_dict_literal<'db, 'ast>(
|
||||||
|
context: &InferContext<'db, 'ast>,
|
||||||
|
typed_dict: TypedDictType<'db>,
|
||||||
|
arguments: &'ast Arguments,
|
||||||
|
error_node: AnyNodeRef<'ast>,
|
||||||
|
expression_type_fn: &impl Fn(&ast::Expr) -> Type<'db>,
|
||||||
|
) -> OrderSet<&'ast str> {
|
||||||
|
let mut provided_keys = OrderSet::new();
|
||||||
|
|
||||||
|
if let ast::Expr::Dict(dict_expr) = &arguments.args[0] {
|
||||||
|
// Validate dict entries
|
||||||
|
for dict_item in &dict_expr.items {
|
||||||
|
if let Some(ref key_expr) = dict_item.key {
|
||||||
|
if let ast::Expr::StringLiteral(ast::ExprStringLiteral {
|
||||||
|
value: key_value, ..
|
||||||
|
}) = key_expr
|
||||||
|
{
|
||||||
|
let key_str = key_value.to_str();
|
||||||
|
provided_keys.insert(key_str);
|
||||||
|
|
||||||
|
// Get the already-inferred argument type
|
||||||
|
let value_type = expression_type_fn(&dict_item.value);
|
||||||
|
validate_typed_dict_key_assignment(
|
||||||
|
context,
|
||||||
|
typed_dict,
|
||||||
|
key_str,
|
||||||
|
value_type,
|
||||||
|
error_node,
|
||||||
|
key_expr,
|
||||||
|
&dict_item.value,
|
||||||
|
TypedDictAssignmentKind::Constructor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provided_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a `TypedDict` constructor call with keywords
|
||||||
|
/// e.g. `Person(name="Alice", age=30)`
|
||||||
|
fn validate_from_keywords<'db, 'ast>(
|
||||||
|
context: &InferContext<'db, 'ast>,
|
||||||
|
typed_dict: TypedDictType<'db>,
|
||||||
|
arguments: &'ast Arguments,
|
||||||
|
error_node: AnyNodeRef<'ast>,
|
||||||
|
expression_type_fn: &impl Fn(&ast::Expr) -> Type<'db>,
|
||||||
|
) -> OrderSet<&'ast str> {
|
||||||
|
let provided_keys: OrderSet<&str> = arguments
|
||||||
|
.keywords
|
||||||
|
.iter()
|
||||||
|
.filter_map(|kw| kw.arg.as_ref().map(|arg| arg.id.as_str()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Validate that each key is assigned a type that is compatible with the keys's value type
|
||||||
|
for keyword in &arguments.keywords {
|
||||||
|
if let Some(arg_name) = &keyword.arg {
|
||||||
|
// Get the already-inferred argument type
|
||||||
|
let arg_type = expression_type_fn(&keyword.value);
|
||||||
|
validate_typed_dict_key_assignment(
|
||||||
|
context,
|
||||||
|
typed_dict,
|
||||||
|
arg_name.as_str(),
|
||||||
|
arg_type,
|
||||||
|
error_node,
|
||||||
|
keyword,
|
||||||
|
&keyword.value,
|
||||||
|
TypedDictAssignmentKind::Constructor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provided_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a `TypedDict` dictionary literal assignment
|
||||||
|
/// e.g. `person: Person = {"name": "Alice", "age": 30}`
|
||||||
|
pub(super) fn validate_typed_dict_dict_literal<'db, 'ast>(
|
||||||
|
context: &InferContext<'db, 'ast>,
|
||||||
|
typed_dict: TypedDictType<'db>,
|
||||||
|
dict_expr: &'ast ast::ExprDict,
|
||||||
|
error_node: AnyNodeRef<'ast>,
|
||||||
|
expression_type_fn: impl Fn(&ast::Expr) -> Type<'db>,
|
||||||
|
) -> OrderSet<&'ast str> {
|
||||||
|
let mut provided_keys = OrderSet::new();
|
||||||
|
|
||||||
|
// Validate each key-value pair in the dictionary literal
|
||||||
|
for item in &dict_expr.items {
|
||||||
|
if let Some(key_expr) = &item.key {
|
||||||
|
if let ast::Expr::StringLiteral(key_literal) = key_expr {
|
||||||
|
let key_str = key_literal.value.to_str();
|
||||||
|
provided_keys.insert(key_str);
|
||||||
|
|
||||||
|
let value_type = expression_type_fn(&item.value);
|
||||||
|
validate_typed_dict_key_assignment(
|
||||||
|
context,
|
||||||
|
typed_dict,
|
||||||
|
key_str,
|
||||||
|
value_type,
|
||||||
|
error_node,
|
||||||
|
key_expr,
|
||||||
|
&item.value,
|
||||||
|
TypedDictAssignmentKind::Constructor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_typed_dict_required_keys(context, typed_dict, &provided_keys, error_node);
|
||||||
|
|
||||||
|
provided_keys
|
||||||
|
}
|
10
ty.schema.json
generated
10
ty.schema.json
generated
|
@ -721,6 +721,16 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"missing-typed-dict-key": {
|
||||||
|
"title": "detects missing required keys in `TypedDict` constructors",
|
||||||
|
"description": "## What it does\nDetects missing required keys in `TypedDict` constructor calls.\n\n## Why is this bad?\n`TypedDict` requires all non-optional keys to be provided during construction.\nMissing items can lead to a `KeyError` at runtime.\n\n## Example\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice: Person = {\"name\": \"Alice\"} # missing required key 'age'\n\nalice[\"age\"] # KeyError\n```",
|
||||||
|
"default": "error",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Level"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"no-matching-overload": {
|
"no-matching-overload": {
|
||||||
"title": "detects calls that do not match any overload",
|
"title": "detects calls that do not match any overload",
|
||||||
"description": "## What it does\nChecks for calls to an overloaded function that do not match any of the overloads.\n\n## Why is this bad?\nFailing to provide the correct arguments to one of the overloads will raise a `TypeError`\nat runtime.\n\n## Examples\n```python\n@overload\ndef func(x: int): ...\n@overload\ndef func(x: bool): ...\nfunc(\"string\") # error: [no-matching-overload]\n```",
|
"description": "## What it does\nChecks for calls to an overloaded function that do not match any of the overloads.\n\n## Why is this bad?\nFailing to provide the correct arguments to one of the overloads will raise a `TypeError`\nat runtime.\n\n## Examples\n```python\n@overload\ndef func(x: int): ...\n@overload\ndef func(x: bool): ...\nfunc(\"string\") # error: [no-matching-overload]\n```",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue