Compare commits

...

18 Commits

Author SHA1 Message Date
Brent Westbrook
59c6cb521d Bump 0.14.6 (#21558) 2025-11-21 09:00:56 -05:00
Alex Waygood
54dba15088 [ty] Improve debug messages when imports fail (#21555) 2025-11-21 13:45:57 +00:00
Andrew Gallant
1af318534a [ty] Add support for relative import completions
We already supported `from .. import <CURSOR>`, but we didn't support
`from ..<CURSOR>`. This adds support for that.
2025-11-21 08:01:02 -05:00
Andrew Gallant
553e568624 [ty] Refactor detection of import statements for completions
This commit essentially does away of all our old heuristic and piecemeal
code for detecting different kinds of import statements. Instead, we
offer one single state machine that does everything. This on its own
fixes a few bugs. For example, `import collections.abc, unico<CURSOR>`
would previously offer global scope completions instead of module
completions.

For the most part though, this commit is a refactoring that preserves
parity. In the next commit, we'll add support for completions on
relative imports.
2025-11-21 08:01:02 -05:00
Andrew Gallant
cdef3f5ab8 [ty] Use dedicated collector for completions
This is a small refactor that helps centralize the
logic for how we gather, convert and possibly filter
completions.

Some of this logic was spread out before, which
motivated this refactor. Moreover, as part of other
refactoring, I found myself chaffing against the
lack of this abstraction.
2025-11-21 08:01:02 -05:00
Alex Waygood
6178822427 [ty] Attach subdiagnostics to unresolved-import errors for relative imports as well as absolute imports (#21554) 2025-11-21 12:40:53 +00:00
Carl Meyer
6b7adb0537 [ty] support PEP 613 type aliases (#21394)
Refs https://github.com/astral-sh/ty/issues/544

## Summary

Takes a more incremental approach to PEP 613 type alias support (vs
https://github.com/astral-sh/ruff/pull/20107). Instead of eagerly
inferring the RHS of a PEP 613 type alias as a type expression, infer it
as a value expression, just like we do for implicit type aliases, taking
advantage of the same support for e.g. unions and other type special
forms.

The main reason I'm following this path instead of the one in
https://github.com/astral-sh/ruff/pull/20107 is that we've realized that
people do sometimes use PEP 613 type aliases as values, not just as
types (because they are just a normal runtime assignment, unlike PEP 695
type aliases which create an opaque `TypeAliasType`).

This PR doesn't yet provide full support for recursive type aliases
(they don't panic, but they just fall back to `Unknown` at the recursion
point). This is future work.

## Test Plan

Added mdtests.

Many new ecosystem diagnostics, mostly because we
understand new types in lots of places.

Conformance suite changes are correct.

Performance regression is due to understanding lots of new
types; nothing we do in this PR is inherently expensive.
2025-11-20 17:59:35 -08:00
Alex Waygood
06941c1987 [ty] More low-hanging fruit for inlay hint goto-definition (#21548) 2025-11-20 23:15:59 +00:00
Jack O'Connor
eb7c098d6b [ty] implement TypedDict structural assignment (#21467)
Closes https://github.com/astral-sh/ty/issues/1387.
2025-11-20 13:15:28 -08:00
Aria Desires
1b28fc1f14 [ty] Add more random TypeDetails and tests (#21546) 2025-11-20 19:46:17 +00:00
Alex Waygood
290a5720cb [ty] Add goto for Unknown when it appears in an inlay hint (#21545) 2025-11-20 18:55:14 +00:00
Alex Waygood
c4767f5aa8 [ty] Add type definitions for Type::SpecialForms (#21544) 2025-11-20 18:14:30 +00:00
Aria Desires
6e84f4fd7a [ty] Resolve overloads for hovers (#21417)
This is a very conservative minimal implementation of applying overloads
to resolve a callable-type-being-called down to a single function
signature on hover. If we ever encounter a situation where the answer
doesn't simplify down to a single function call, we bail out to preserve
prettier printing of non-raw-Signatures.

The resulting Signatures are still a bit bare, I'm going to try to
improve that in a followup to improve our Signature printing in general.

Fixes https://github.com/astral-sh/ty/issues/73
2025-11-20 12:45:02 -05:00
Aria Desires
78ce17ce8f [ty] Add more TypeDetails to the display code (#21541)
As far as I know this change is largely non-functional, largely because
of https://github.com/astral-sh/ty/issues/1601

It's possible some of these like `Type::KnownInstance` produce something
useful sometimes. `LiteralString` is a new introduction, although its
goto-type jumps to `str` which is a bit sad (considering that part of
the SpecialForm discourse for now).

Also wrt the generics testing followup: turns out the snapshot tests
were full of those already.
2025-11-20 12:08:59 -05:00
David Peter
0761ea42d9 [ty] Eagerly evaluate types.UnionType elements as type expressions (#21531)
## Summary

Eagerly evaluate the elements of a PEP 604 union in value position (e.g.
`IntOrStr = int | str`) as type expressions and store the result (the
corresponding `Type::Union` if all elements are valid type expressions,
or the first encountered `InvalidTypeExpressionError`) on the
`UnionTypeInstance`, such that the `Type::Union(…)` does not need to be
recomputed every time the implicit type alias is used in a type
annotation.

This might lead to performance improvements for large unions, but is
also necessary for correctness, because the elements of the union might
refer to type variables that need to be looked up in the scope of the
type alias, not at the usage site.

## Test Plan

New Markdown tests
2025-11-20 17:28:48 +01:00
Aria Desires
416e2267da [ty] Implement goto-type for inlay type hints (#21533)
This PR generalizes the signature_help system's SignatureWriter which
could get the subspans of function parameters.
We now have TypeDetailsWriter which is threaded between type's display
implementations via a new `fmt_detailed` method that many of the Display
types now have.

With this information we can properly add goto-type targets to our inlay
hints. This also lays groundwork for any future "I want to render a type
but get spans" work.

Also a ton of lifetimes are introduced to avoid things getting conflated
with `'db`.

This PR is broken up into a series of commits:

* Generalizing `SignatureWriter` to `TypeDetailsWriter`, but not using
it anywhere else. This commit was confirmed to be a non-functional
change (no test results changed)
* Introducing `fmt_detailed` everywhere to thread through
`TypeDetailsWriter` and annotate various spans as "being" a given Type
-- this is also where I had to reckon with a ton of erroneous `&'db
self`. This commit was also confirmed to be a non-functional change.
* Finally, actually using the results for goto-type on inlay hints!
* Regenerating snapshots, fixups, etc.
2025-11-20 09:40:40 -05:00
David Peter
02c102da88 [ty] Add tests: types.UnionType in isinstance/issubclass (#21537)
## Summary

Add some tests documenting the fact that we don't support
`types.UnionType` in `isinstance`/`issubclass` at the moment.
2025-11-20 11:59:36 +00:00
David Peter
29c24bc8a6 [ty] Ecosystem analyzer: support custom ty_cmds (#21534)
## Summary

Pulls in
e26ebfb78d
in order to support some projects that now require a custom `ty_cmd`.

## Test Plan

CI on this PR
2025-11-20 09:10:56 +01:00
63 changed files with 6900 additions and 1831 deletions

View File

@@ -67,7 +67,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e5c5f5b2d762af91b28490537fe0077334165693"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
ecosystem-analyzer \
--repository ruff \

View File

@@ -52,7 +52,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e5c5f5b2d762af91b28490537fe0077334165693"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
ecosystem-analyzer \
--verbose \

View File

@@ -1,5 +1,48 @@
# Changelog
## 0.14.6
Released on 2025-11-21.
### Preview features
- \[`flake8-bandit`\] Support new PySNMP API paths (`S508`, `S509`) ([#21374](https://github.com/astral-sh/ruff/pull/21374))
### Bug fixes
- Adjust own-line comment placement between branches ([#21185](https://github.com/astral-sh/ruff/pull/21185))
- Avoid syntax error when formatting attribute expressions with outer parentheses, parenthesized value, and trailing comment on value ([#20418](https://github.com/astral-sh/ruff/pull/20418))
- Fix panic when formatting comments in unary expressions ([#21501](https://github.com/astral-sh/ruff/pull/21501))
- Respect `fmt: skip` for compound statements on a single line ([#20633](https://github.com/astral-sh/ruff/pull/20633))
- \[`refurb`\] Fix `FURB103` autofix ([#21454](https://github.com/astral-sh/ruff/pull/21454))
- \[`ruff`\] Fix false positive for complex conversion specifiers in `logging-eager-conversion` (`RUF065`) ([#21464](https://github.com/astral-sh/ruff/pull/21464))
### Rule changes
- \[`ruff`\] Avoid false positive on `ClassVar` reassignment (`RUF012`) ([#21478](https://github.com/astral-sh/ruff/pull/21478))
### CLI
- Render hyperlinks for lint errors ([#21514](https://github.com/astral-sh/ruff/pull/21514))
- Add a `ruff analyze` option to skip over imports in `TYPE_CHECKING` blocks ([#21472](https://github.com/astral-sh/ruff/pull/21472))
### Documentation
- Limit `eglot-format` hook to eglot-managed Python buffers ([#21459](https://github.com/astral-sh/ruff/pull/21459))
- Mention `force-exclude` in "Configuration > Python file discovery" ([#21500](https://github.com/astral-sh/ruff/pull/21500))
### Contributors
- [@ntBre](https://github.com/ntBre)
- [@dylwil3](https://github.com/dylwil3)
- [@gauthsvenkat](https://github.com/gauthsvenkat)
- [@MichaReiser](https://github.com/MichaReiser)
- [@thamer](https://github.com/thamer)
- [@Ruchir28](https://github.com/Ruchir28)
- [@thejcannon](https://github.com/thejcannon)
- [@danparizher](https://github.com/danparizher)
- [@chirizxc](https://github.com/chirizxc)
## 0.14.5
Released on 2025-11-13.

6
Cargo.lock generated
View File

@@ -2859,7 +2859,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.5"
version = "0.14.6"
dependencies = [
"anyhow",
"argfile",
@@ -3117,7 +3117,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.5"
version = "0.14.6"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3472,7 +3472,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.5"
version = "0.14.6"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -147,8 +147,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.5/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.6/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.6/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -181,7 +181,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.5
rev: v0.14.6
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.14.5"
version = "0.14.6"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -143,7 +143,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
525,
600,
);
static PANDAS: Benchmark = Benchmark::new(
@@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
3000,
4000,
);
static PYDANTIC: Benchmark = Benchmark::new(
@@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
5000,
7000,
);
static SYMPY: Benchmark = Benchmark::new(

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.14.5"
version = "0.14.6"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.14.5"
version = "0.14.6"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -8,13 +8,12 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
///
/// Returns symbols from all files in the workspace and dependencies, filtered
/// by the query.
pub fn all_symbols<'db>(db: &'db dyn Db, query: &str) -> Vec<AllSymbolInfo<'db>> {
pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolInfo<'db>> {
// If the query is empty, return immediately to avoid expensive file scanning
if query.is_empty() {
if query.will_match_everything() {
return Vec::new();
}
let query = QueryPattern::new(query);
let results = std::sync::Mutex::new(Vec::new());
{
let modules = all_modules(db);
@@ -144,7 +143,7 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
impl CursorTest {
fn all_symbols(&self, query: &str) -> String {
let symbols = all_symbols(&self.db, query);
let symbols = all_symbols(&self.db, &QueryPattern::new(query));
if symbols.is_empty() {
return "No symbols found".to_string();

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::types::Type;
use ty_python_semantic::types::ide_support::{
call_signature_details, definitions_for_keyword_argument,
call_signature_details, call_type_simplified_by_overloads, definitions_for_keyword_argument,
};
use ty_python_semantic::{
HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol,
@@ -213,6 +213,7 @@ impl<'db> DefinitionsOrTargets<'db> {
| ty_python_semantic::types::TypeDefinition::Function(definition)
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
| ty_python_semantic::types::TypeDefinition::SpecialForm(definition)
| ty_python_semantic::types::TypeDefinition::NewType(definition) => {
ResolvedDefinition::Definition(definition)
}
@@ -326,6 +327,18 @@ impl GotoTarget<'_> {
Some(ty)
}
/// Try to get a simplified display of this callable type by resolving overloads
pub(crate) fn call_type_simplified_by_overloads(
&self,
model: &SemanticModel,
) -> Option<String> {
if let GotoTarget::Call { call, .. } = self {
call_type_simplified_by_overloads(model.db(), model, call)
} else {
None
}
}
/// Gets the definitions for this goto target.
///
/// The `alias_resolution` parameter controls whether import aliases

View File

@@ -68,6 +68,134 @@ mod tests {
");
}
#[test]
fn goto_type_of_typing_dot_literal() {
let test = cursor_test(
r#"
from typing import Literal
a<CURSOR>b = Literal
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/typing.pyi:351:1
|
349 | Final: _SpecialForm
350 |
351 | Literal: _SpecialForm
| ^^^^^^^
352 | TypedDict: _SpecialForm
|
info: Source
--> main.py:4:1
|
2 | from typing import Literal
3 |
4 | ab = Literal
| ^^
|
");
}
// this is a slightly different case to the one above,
// since `Any` is a class in typeshed rather than a variable
#[test]
fn goto_type_of_typing_dot_any() {
let test = cursor_test(
r#"
from typing import Any
a<CURSOR>b = Any
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/typing.pyi:166:7
|
164 | # from _typeshed import AnnotationForm
165 |
166 | class Any:
| ^^^
167 | """Special type indicating an unconstrained type.
|
info: Source
--> main.py:4:1
|
2 | from typing import Any
3 |
4 | ab = Any
| ^^
|
"#);
}
// Similarly, `Generic` is a `type[]` type in typeshed
#[test]
fn goto_type_of_typing_dot_generic() {
let test = cursor_test(
r#"
from typing import Generic
a<CURSOR>b = Generic
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/typing.pyi:770:1
|
768 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ...
769 |
770 | Generic: type[_Generic]
| ^^^^^^^
771 |
772 | class _ProtocolMeta(ABCMeta):
|
info: Source
--> main.py:4:1
|
2 | from typing import Generic
3 |
4 | ab = Generic
| ^^
|
");
}
#[test]
fn goto_type_of_ty_extensions_special_form() {
let test = cursor_test(
r#"
from ty_extensions import AlwaysTruthy
a<CURSOR>b = AlwaysTruthy
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:21:1
|
19 | # Types
20 | Unknown = object()
21 | AlwaysTruthy = object()
| ^^^^^^^^^^^^
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:4:1
|
2 | from ty_extensions import AlwaysTruthy
3 |
4 | ab = AlwaysTruthy
| ^^
|
");
}
#[test]
fn goto_type_of_expression_with_function_type() {
let test = cursor_test(

View File

@@ -20,7 +20,6 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
}
let model = SemanticModel::new(db, file);
let ty = goto_target.inferred_type(&model);
let docs = goto_target
.get_definition_targets(
file,
@@ -30,9 +29,10 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
.and_then(|definitions| definitions.docstring(db))
.map(HoverContent::Docstring);
// TODO: Render the symbol's signature instead of just its type.
let mut contents = Vec::new();
if let Some(ty) = ty {
if let Some(signature) = goto_target.call_type_simplified_by_overloads(&model) {
contents.push(HoverContent::Signature(signature));
} else if let Some(ty) = goto_target.inferred_type(&model) {
tracing::debug!("Inferred type of covering node is {}", ty.display(db));
contents.push(match ty {
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => typevar
@@ -62,7 +62,7 @@ pub struct Hover<'db> {
impl<'db> Hover<'db> {
/// Renders the hover to a string using the specified markup kind.
pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> {
pub const fn display<'a>(&'a self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHover<'db, 'a> {
DisplayHover {
db,
hover: self,
@@ -93,13 +93,13 @@ impl<'a, 'db> IntoIterator for &'a Hover<'db> {
}
}
pub struct DisplayHover<'a> {
db: &'a dyn Db,
hover: &'a Hover<'a>,
pub struct DisplayHover<'db, 'a> {
db: &'db dyn Db,
hover: &'a Hover<'db>,
kind: MarkupKind,
}
impl fmt::Display for DisplayHover<'_> {
impl fmt::Display for DisplayHover<'_, '_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut first = true;
for content in &self.hover.contents {
@@ -115,8 +115,9 @@ impl fmt::Display for DisplayHover<'_> {
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone)]
pub enum HoverContent<'db> {
Signature(String),
Type(Type<'db>, Option<TypeVarVariance>),
Docstring(Docstring),
}
@@ -140,6 +141,9 @@ pub(crate) struct DisplayHoverContent<'a, 'db> {
impl fmt::Display for DisplayHoverContent<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.content {
HoverContent::Signature(signature) => {
self.kind.fenced_code_block(&signature, "python").fmt(f)
}
HoverContent::Type(ty, variance) => {
let variance = match variance {
Some(TypeVarVariance::Covariant) => " (covariant)",
@@ -961,14 +965,12 @@ def ab(a: str): ...
assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(a: str) -> Unknown
---------------------------------------------
the int overload
---------------------------------------------
```python
(a: int) -> Unknown
(a: str) -> Unknown
```
---
```text
@@ -1025,14 +1027,12 @@ def ab(a: str):
.build();
assert_snapshot!(test.hover(), @r#"
(a: int) -> Unknown
(a: str) -> Unknown
---------------------------------------------
the int overload
---------------------------------------------
```python
(a: int) -> Unknown
(a: str) -> Unknown
```
---
@@ -1094,7 +1094,6 @@ def ab(a: int):
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
---------------------------------------------
the two arg overload
@@ -1104,7 +1103,6 @@ def ab(a: int):
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
```
---
```text
@@ -1161,20 +1159,12 @@ def ab(a: int):
.build();
assert_snapshot!(test.hover(), @r"
(
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
---------------------------------------------
the two arg overload
---------------------------------------------
```python
(
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
```
---
@@ -1236,33 +1226,21 @@ def ab(a: int, *, c: int):
.build();
assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
c: int
) -> Unknown
---------------------------------------------
keywordless overload
---------------------------------------------
```python
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
c: int
) -> Unknown
```
---
```text
@@ -1323,12 +1301,6 @@ def ab(a: int, *, c: int):
.build();
assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
@@ -1339,12 +1311,6 @@ def ab(a: int, *, c: int):
---------------------------------------------
```python
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,16 @@ impl QueryPattern {
symbol_name.contains(&self.original)
}
}
/// Returns true when it is known that this pattern will return `true` for
/// all inputs given to `QueryPattern::is_match_symbol_name`.
///
/// This will never return `true` incorrectly, but it may return `false`
/// incorrectly. That is, it's possible that this query will match all
/// inputs but this still returns `false`.
pub fn will_match_everything(&self) -> bool {
self.re.is_none()
}
}
impl From<&str> for QueryPattern {

View File

@@ -12,11 +12,8 @@ P = ParamSpec("P")
Ts = TypeVarTuple("Ts")
R_co = TypeVar("R_co", covariant=True)
Alias: TypeAlias = int
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
return args
def g() -> TypeGuard[int]: ...

View File

@@ -482,17 +482,14 @@ class TD2(TypedDict):
x: str
def f(self, dt: dict[str, Any], key: str):
# TODO: This should not error once typed dict assignability is implemented.
# error: [invalid-assignment]
x1: TD = dt.get(key, {})
reveal_type(x1) # revealed: TD
reveal_type(x1) # revealed: Any
x2: TD = dt.get(key, {"x": 0})
reveal_type(x2) # revealed: Any
x3: TD | None = dt.get(key, {})
# TODO: This should reveal `Any` once typed dict assignability is implemented.
reveal_type(x3) # revealed: Any | None
reveal_type(x3) # revealed: Any
x4: TD | None = dt.get(key, {"x": 0})
reveal_type(x4) # revealed: Any

View File

@@ -2208,9 +2208,9 @@ reveal_type(False.real) # revealed: Literal[0]
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
```py
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[Buffer], /) -> bytes
reveal_type(b"foo".join)
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
# revealed: bound method Literal[b"foo"].endswith(suffix: Buffer | tuple[Buffer, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
reveal_type(b"foo".endswith)
```

View File

@@ -313,8 +313,7 @@ reveal_type(A() + "foo") # revealed: A
reveal_type("foo" + A()) # revealed: A
reveal_type(A() + b"foo") # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(b"foo" + A()) # revealed: A
reveal_type(A() + ()) # revealed: A
reveal_type(() + A()) # revealed: A

View File

@@ -54,10 +54,8 @@ reveal_type(2**largest_u32) # revealed: int
def variable(x: int):
reveal_type(x**2) # revealed: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(2**x) # revealed: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(x**x) # revealed: int
reveal_type(2**x) # revealed: Any
reveal_type(x**x) # revealed: Any
```
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but

View File

@@ -598,9 +598,9 @@ from typing_extensions import Self
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
reveal_type(int.__new__)
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
reveal_type((42).__new__)
class X:

View File

@@ -10,13 +10,13 @@ import pickle
reveal_type(open("")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(open("", "r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(open("", "rb")) # revealed: @Todo(`builtins.open` return type)
reveal_type(open("", "rb")) # revealed: BufferedReader[_BufferedReaderStream]
with open("foo.pickle", "rb") as f:
x = pickle.load(f) # fine
def _(mode: str):
reveal_type(open("", mode)) # revealed: @Todo(`builtins.open` return type)
reveal_type(open("", mode)) # revealed: IO[Any]
```
## `os.fdopen`
@@ -29,7 +29,7 @@ import os
reveal_type(os.fdopen(0)) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(os.fdopen(0, "r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(os.fdopen(0, "rb")) # revealed: @Todo(`os.fdopen` return type)
reveal_type(os.fdopen(0, "rb")) # revealed: BufferedReader[_BufferedReaderStream]
with os.fdopen(0, "rb") as f:
x = pickle.load(f) # fine
@@ -43,9 +43,9 @@ And similarly for `Path.open()`:
from pathlib import Path
import pickle
reveal_type(Path("").open()) # revealed: @Todo(`Path.open` return type)
reveal_type(Path("").open("r")) # revealed: @Todo(`Path.open` return type)
reveal_type(Path("").open("rb")) # revealed: @Todo(`Path.open` return type)
reveal_type(Path("").open()) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(Path("").open("r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(Path("").open("rb")) # revealed: BufferedReader[_BufferedReaderStream]
with Path("foo.pickle").open("rb") as f:
x = pickle.load(f) # fine
@@ -61,7 +61,7 @@ import pickle
reveal_type(NamedTemporaryFile()) # revealed: _TemporaryFileWrapper[bytes]
reveal_type(NamedTemporaryFile("r")) # revealed: _TemporaryFileWrapper[str]
reveal_type(NamedTemporaryFile("rb")) # revealed: @Todo(`tempfile.NamedTemporaryFile` return type)
reveal_type(NamedTemporaryFile("rb")) # revealed: _TemporaryFileWrapper[bytes]
with NamedTemporaryFile("rb") as f:
x = pickle.load(f) # fine

View File

@@ -265,7 +265,7 @@ import dict_a
import dict_b
def _(b_person: dict_b.Person):
# TODO should be error: [invalid-assignment] "Object of type `dict_b.Person` is not assignable to `dict_a.Person`"
# error: [invalid-assignment] "Object of type `dict_b.Person` is not assignable to `dict_a.Person`"
person_var: dict_a.Person = b_person
```

View File

@@ -127,7 +127,7 @@ x = lambda y: y
reveal_type(x.__code__) # revealed: CodeType
reveal_type(x.__name__) # revealed: str
reveal_type(x.__defaults__) # revealed: tuple[Any, ...] | None
reveal_type(x.__annotations__) # revealed: dict[str, @Todo(Support for `typing.TypeAlias`)]
reveal_type(x.__annotations__) # revealed: dict[str, Any]
reveal_type(x.__dict__) # revealed: dict[str, Any]
reveal_type(x.__doc__) # revealed: str | None
reveal_type(x.__kwdefaults__) # revealed: dict[str, Any] | None

View File

@@ -191,13 +191,13 @@ def _(
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
# TODO should be Unknown | int
reveal_type(type_var_or_int) # revealed: T@_ | int
reveal_type(type_var_or_int) # revealed: typing.TypeVar | int
# TODO should be int | Unknown
reveal_type(int_or_type_var) # revealed: int | T@_
reveal_type(int_or_type_var) # revealed: int | typing.TypeVar
# TODO should be Unknown | None
reveal_type(type_var_or_none) # revealed: T@_ | None
reveal_type(type_var_or_none) # revealed: typing.TypeVar | None
# TODO should be None | Unknown
reveal_type(none_or_type_var) # revealed: None | T@_
reveal_type(none_or_type_var) # revealed: None | typing.TypeVar
```
If a type is unioned with itself in a value expression, the result is just that type. No
@@ -1109,7 +1109,7 @@ from typing import List, Dict
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
InvalidList = List[1]
# error: [invalid-type-form] "`typing.typing.List` requires exactly one argument"
# error: [invalid-type-form] "`typing.List` requires exactly one argument"
ListTooManyArgs = List[int, str]
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
@@ -1118,10 +1118,10 @@ InvalidDict1 = Dict[1, str]
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
InvalidDict2 = Dict[str, 2]
# error: [invalid-type-form] "`typing.typing.Dict` requires exactly two arguments, got 1"
# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 1"
DictTooFewArgs = Dict[str]
# error: [invalid-type-form] "`typing.typing.Dict` requires exactly two arguments, got 3"
# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 3"
DictTooManyArgs = Dict[str, int, float]
def _(

View File

@@ -147,6 +147,57 @@ def _(x: int | str | bytes):
reveal_type(x) # revealed: (int & Unknown) | (str & Unknown) | (bytes & Unknown)
```
## `classinfo` is a `types.UnionType`
Python 3.10 added the ability to use `Union[int, str]` as the second argument to `isinstance()`:
```py
from typing import Union
IntOrStr = Union[int, str]
reveal_type(IntOrStr) # revealed: types.UnionType
def _(x: int | str | bytes | memoryview | range):
if isinstance(x, IntOrStr):
reveal_type(x) # revealed: int | str
elif isinstance(x, Union[bytes, memoryview]):
reveal_type(x) # revealed: bytes | memoryview[int]
else:
reveal_type(x) # revealed: range
def _(x: int | str | None):
if isinstance(x, Union[int, None]):
reveal_type(x) # revealed: int | None
else:
reveal_type(x) # revealed: str
ListStrOrInt = Union[list[str], int]
def _(x: dict[int, str] | ListStrOrInt):
# TODO: this should ideally be an error
if isinstance(x, ListStrOrInt):
# TODO: this should not be narrowed
reveal_type(x) # revealed: list[str] | int
# TODO: this should ideally be an error
if isinstance(x, Union[list[str], int]):
# TODO: this should not be narrowed
reveal_type(x) # revealed: list[str] | int
```
## `Optional` as `classinfo`
```py
from typing import Optional
def _(x: int | str | None):
if isinstance(x, Optional[int]):
reveal_type(x) # revealed: int | None
else:
reveal_type(x) # revealed: str
```
## `classinfo` is a `typing.py` special form
Certain special forms in `typing.py` are aliases to classes elsewhere in the standard library; these
@@ -262,6 +313,23 @@ def _(flag: bool):
reveal_type(x) # revealed: Literal[1, "a"]
```
## Generic aliases are not supported as second argument
The `classinfo` argument cannot be a generic alias:
```py
def _(x: list[str] | list[int] | list[bytes]):
# TODO: Ideally, this would be an error (requires https://github.com/astral-sh/ty/issues/116)
if isinstance(x, list[int]):
# No narrowing here:
reveal_type(x) # revealed: list[str] | list[int] | list[bytes]
# error: [invalid-argument-type] "Invalid second argument to `isinstance`"
if isinstance(x, list[int] | list[str]):
# No narrowing here:
reveal_type(x) # revealed: list[str] | list[int] | list[bytes]
```
## `type[]` types are narrowed as well as class-literal types
```py

View File

@@ -200,6 +200,26 @@ def _(x: type[int | str | bytes]):
reveal_type(x) # revealed: (type[int] & Unknown) | (type[str] & Unknown) | (type[bytes] & Unknown)
```
## `classinfo` is a `types.UnionType`
Python 3.10 added the ability to use `Union[int, str]` as the second argument to `issubclass()`:
```py
from typing import Union
IntOrStr = Union[int, str]
reveal_type(IntOrStr) # revealed: types.UnionType
def f(x: type[int | str | bytes | range]):
if issubclass(x, IntOrStr):
reveal_type(x) # revealed: type[int] | type[str]
elif issubclass(x, Union[bytes, memoryview]):
reveal_type(x) # revealed: type[bytes]
else:
reveal_type(x) # revealed: <class 'range'>
```
## Special cases
### Emit a diagnostic if the first argument is of wrong type

View File

@@ -1,8 +1,147 @@
# PEP 613 type aliases
## No panics
PEP 613 type aliases are simple assignment statements, annotated with `typing.TypeAlias` to mark
them as a type alias. At runtime, they behave the same as implicit type aliases. Our support for
them is currently the same as for implicit type aliases, but we don't reproduce the full
implicit-type-alias test suite here, just some particularly interesting cases.
We do not fully support PEP 613 type aliases yet. For now, just make sure that we don't panic:
## Basic
### as `TypeAlias`
```py
from typing import TypeAlias
IntOrStr: TypeAlias = int | str
def _(x: IntOrStr):
reveal_type(x) # revealed: int | str
```
### as `typing.TypeAlias`
```py
import typing
IntOrStr: typing.TypeAlias = int | str
def _(x: IntOrStr):
reveal_type(x) # revealed: int | str
```
## Can be used as value
Because PEP 613 type aliases are just annotated assignments, they can be used as values, like a
legacy type expression (and unlike a PEP 695 type alias). We might prefer this wasn't allowed, but
people do use it.
```py
from typing import TypeAlias
MyExc: TypeAlias = Exception
try:
raise MyExc("error")
except MyExc as e:
reveal_type(e) # revealed: Exception
```
## Can inherit from an alias
```py
from typing import TypeAlias
from ty_extensions import is_subtype_of, static_assert
MyList: TypeAlias = list["int"]
class Foo(MyList): ...
static_assert(is_subtype_of(Foo, list[int]))
```
## Cannot inherit from a stringified alias
```py
from typing import TypeAlias
MyList: TypeAlias = "list[int]"
# error: [invalid-base] "Invalid class base with type `str`"
class Foo(MyList): ...
```
## Unknown type in PEP 604 union
If we run into an unknown type in a PEP 604 union in the right-hand side of a PEP 613 type alias, we
still understand it as a union type, just with an unknown element.
```py
from typing import TypeAlias
from nonexistent import unknown_type # error: [unresolved-import]
MyAlias: TypeAlias = int | unknown_type | str
def _(x: MyAlias):
reveal_type(x) # revealed: int | Unknown | str
```
## Callable type in union
```py
from typing import TypeAlias, Callable
MyAlias: TypeAlias = int | Callable[[str], int]
def _(x: MyAlias):
reveal_type(x) # revealed: int | ((str, /) -> int)
```
## Subscripted generic alias in union
```py
from typing import TypeAlias, TypeVar
T = TypeVar("T")
Alias1: TypeAlias = list[T] | set[T]
MyAlias: TypeAlias = int | Alias1[str]
def _(x: MyAlias):
# TODO: int | list[str] | set[str]
reveal_type(x) # revealed: int | @Todo(Specialization of union type alias)
```
## Imported
`alias.py`:
```py
from typing import TypeAlias
MyAlias: TypeAlias = int | str
```
`main.py`:
```py
from alias import MyAlias
def _(x: MyAlias):
reveal_type(x) # revealed: int | str
```
## String literal in right-hand side
```py
from typing import TypeAlias
IntOrStr: TypeAlias = "int | str"
def _(x: IntOrStr):
reveal_type(x) # revealed: int | str
```
## Cyclic
```py
from typing import TypeAlias
@@ -18,6 +157,26 @@ def _(rec: RecursiveHomogeneousTuple):
reveal_type(rec) # revealed: tuple[Divergent, ...]
```
## Conditionally imported on Python < 3.10
```toml
[environment]
python-version = "3.9"
```
```py
try:
# error: [unresolved-import]
from typing import TypeAlias
except ImportError:
from typing_extensions import TypeAlias
MyAlias: TypeAlias = int
def _(x: MyAlias):
reveal_type(x) # revealed: int
```
## PEP-613 aliases in stubs are deferred
Although the right-hand side of a PEP-613 alias is a value expression, inference of this value is
@@ -46,7 +205,31 @@ f(stub.B())
class Unrelated: ...
# TODO: we should emit `[invalid-argument-type]` here
# (the alias is a `@Todo` because it's imported from another file)
# error: [invalid-argument-type]
f(Unrelated())
```
## Invalid position
`typing.TypeAlias` must be used as the sole annotation in an annotated assignment. Use in any other
context is an error.
```py
from typing import TypeAlias
# error: [invalid-type-form]
def _(x: TypeAlias):
reveal_type(x) # revealed: Unknown
# error: [invalid-type-form]
y: list[TypeAlias] = []
```
## Right-hand side is required
```py
from typing import TypeAlias
# error: [invalid-type-form]
Empty: TypeAlias
```

View File

@@ -28,7 +28,7 @@ def f() -> None:
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__value__) # revealed: @Todo(Support for `typing.TypeAlias`)
reveal_type(IntOrStr.__value__) # revealed: Any
```
## Invalid assignment

View File

@@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for all elemens of a union
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for all elements of a union
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
@@ -16,26 +16,27 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
2 |
3 | class Person(TypedDict):
4 | name: str
5 |
6 | class Animal(TypedDict):
7 | name: str
8 | legs: int
9 |
10 | def _(being: Person | Animal) -> None:
11 | # error: [invalid-key]
5 | phone_number: str
6 |
7 | class Animal(TypedDict):
8 | name: str
9 | legs: int
10 |
11 | def _(being: Person | Animal) -> None:
12 | # error: [invalid-key]
13 | being["surname"] = "unknown"
13 | # error: [invalid-key]
14 | being["surname"] = "unknown"
```
# Diagnostics
```
error[invalid-key]: Unknown key "surname" for TypedDict `Person`
--> src/mdtest_snippet.py:13:5
--> src/mdtest_snippet.py:14:5
|
11 | # error: [invalid-key]
12 | # error: [invalid-key]
13 | being["surname"] = "unknown"
13 | # error: [invalid-key]
14 | being["surname"] = "unknown"
| ----- ^^^^^^^^^ Did you mean "name"?
| |
| TypedDict `Person` in union type `Person | Animal`
@@ -46,11 +47,11 @@ info: rule `invalid-key` is enabled by default
```
error[invalid-key]: Unknown key "surname" for TypedDict `Animal`
--> src/mdtest_snippet.py:13:5
--> src/mdtest_snippet.py:14:5
|
11 | # error: [invalid-key]
12 | # error: [invalid-key]
13 | being["surname"] = "unknown"
13 | # error: [invalid-key]
14 | being["surname"] = "unknown"
| ----- ^^^^^^^^^ Did you mean "name"?
| |
| TypedDict `Animal` in union type `Person | Animal`

View File

@@ -16,23 +16,24 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
2 |
3 | class Person(TypedDict):
4 | name: str
5 |
6 | class Animal(TypedDict):
7 | name: str
8 | legs: int
9 |
10 | def _(being: Person | Animal) -> None:
11 | being["legs"] = 4 # error: [invalid-key]
5 | phone_number: str
6 |
7 | class Animal(TypedDict):
8 | name: str
9 | legs: int
10 |
11 | def _(being: Person | Animal) -> None:
12 | being["legs"] = 4 # error: [invalid-key]
```
# Diagnostics
```
error[invalid-key]: Unknown key "legs" for TypedDict `Person`
--> src/mdtest_snippet.py:11:5
--> src/mdtest_snippet.py:12:5
|
10 | def _(being: Person | Animal) -> None:
11 | being["legs"] = 4 # error: [invalid-key]
11 | def _(being: Person | Animal) -> None:
12 | being["legs"] = 4 # error: [invalid-key]
| ----- ^^^^^^ Unknown key "legs"
| |
| TypedDict `Person` in union type `Person | Animal`

View File

@@ -28,6 +28,10 @@ error[unresolved-import]: Cannot resolve imported module `.does_not_exist.foo.ba
2 |
3 | stat = add(10, 15)
|
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
```

View File

@@ -28,6 +28,10 @@ error[unresolved-import]: Cannot resolve imported module `.does_not_exist`
2 |
3 | stat = add(10, 15)
|
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
```

View File

@@ -40,6 +40,12 @@ error[unresolved-import]: Cannot resolve imported module `....foo`
2 |
3 | stat = add(10, 15)
|
help: The module can be resolved if the number of leading dots is reduced
help: Did you mean `...foo`?
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
```

View File

@@ -95,6 +95,7 @@ from typing import TypedDict
class Person(TypedDict):
name: str
phone_number: str
class Animal(TypedDict):
name: str
@@ -104,13 +105,14 @@ def _(being: Person | Animal) -> None:
being["legs"] = 4 # error: [invalid-key]
```
## Unknown key for all elemens of a union
## Unknown key for all elements of a union
```py
from typing import TypedDict
class Person(TypedDict):
name: str
phone_number: str
class Animal(TypedDict):
name: str

View File

@@ -122,7 +122,7 @@ properties on instance types:
```py
reveal_type(sys.version_info.micro) # revealed: int
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(Support for `typing.TypeAlias`)
reveal_type(sys.version_info.releaselevel) # revealed: Literal["alpha", "beta", "candidate", "final"]
reveal_type(sys.version_info.serial) # revealed: int
```

View File

@@ -460,6 +460,8 @@ and their types, rather than the class hierarchy:
```py
from typing import TypedDict
from typing_extensions import ReadOnly
from ty_extensions import static_assert, is_assignable_to, is_subtype_of
class Person(TypedDict):
name: str
@@ -468,10 +470,201 @@ class Employee(TypedDict):
name: str
employee_id: int
p1: Person = Employee(name="Alice", employee_id=1)
class Robot(TypedDict):
name: int
# TODO: this should be an error
e1: Employee = Person(name="Eve")
static_assert(is_assignable_to(Employee, Person))
static_assert(not is_assignable_to(Person, Employee))
static_assert(not is_assignable_to(Robot, Person))
static_assert(not is_assignable_to(Person, Robot))
```
In order for one `TypedDict` `B` to be assignable to another `TypedDict` `A`, all required keys in
`A`'s schema must be required in `B`'s schema. If a key is not-required and also mutable in `A`,
then it must be not-required in `B` (because `A` allows the caller to `del` that key). These rules
cover keys that are explicitly marked `NotRequired`, and also all the keys in a `TypedDict` with
`total=False`.
```py
from typing_extensions import NotRequired
class Spy1(TypedDict):
name: NotRequired[str]
class Spy2(TypedDict, total=False):
name: str
# invalid because `Spy1` and `Spy2` might be missing `name`
static_assert(not is_assignable_to(Spy1, Person))
static_assert(not is_assignable_to(Spy2, Person))
# invalid because `Spy1` and `Spy2` are allowed to delete `name`, while `Person` is not
static_assert(not is_assignable_to(Person, Spy1))
static_assert(not is_assignable_to(Person, Spy2))
class Amnesiac1(TypedDict):
name: NotRequired[ReadOnly[str]]
class Amnesiac2(TypedDict, total=False):
name: ReadOnly[str]
# invalid because `Amnesiac1` and `Amnesiac2` might be missing `name`
static_assert(not is_assignable_to(Amnesiac1, Person))
static_assert(not is_assignable_to(Amnesiac2, Person))
# Allowed. Neither `Amnesiac1` nor `Amnesiac2` can delete `name`, because it's read-only.
static_assert(is_assignable_to(Person, Amnesiac1))
static_assert(is_assignable_to(Person, Amnesiac2))
```
If an item in `A` (the destination `TypedDict` type) is read-only, then the corresponding item in
`B` can have any assignable type. But if the item in `A` is mutable, the item type in `B` must be
"consistent", i.e. both assignable-to and assignable-from. (For fully-static types, consistent is
the same as equivalent.) The required and not-required cases are different codepaths, so we need
test all the permutations:
```py
from typing import Any
from typing_extensions import ReadOnly
class RequiredMutableInt(TypedDict):
x: int
class RequiredReadOnlyInt(TypedDict):
x: ReadOnly[int]
class NotRequiredMutableInt(TypedDict):
x: NotRequired[int]
class NotRequiredReadOnlyInt(TypedDict):
x: NotRequired[ReadOnly[int]]
class RequiredMutableBool(TypedDict):
x: bool
class RequiredReadOnlyBool(TypedDict):
x: ReadOnly[bool]
class NotRequiredMutableBool(TypedDict):
x: NotRequired[bool]
class NotRequiredReadOnlyBool(TypedDict):
x: NotRequired[ReadOnly[bool]]
class RequiredMutableAny(TypedDict):
x: Any
class RequiredReadOnlyAny(TypedDict):
x: ReadOnly[Any]
class NotRequiredMutableAny(TypedDict):
x: NotRequired[Any]
class NotRequiredReadOnlyAny(TypedDict):
x: NotRequired[ReadOnly[Any]]
# fmt: off
static_assert( is_assignable_to( RequiredMutableInt, RequiredMutableInt))
static_assert( is_subtype_of( RequiredMutableInt, RequiredMutableInt))
static_assert(not is_assignable_to( RequiredReadOnlyInt, RequiredMutableInt))
static_assert(not is_subtype_of( RequiredReadOnlyInt, RequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredMutableInt, RequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredMutableInt, RequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredReadOnlyInt, RequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyInt, RequiredMutableInt))
static_assert(not is_assignable_to( RequiredMutableBool, RequiredMutableInt))
static_assert(not is_subtype_of( RequiredMutableBool, RequiredMutableInt))
static_assert(not is_assignable_to( RequiredReadOnlyBool, RequiredMutableInt))
static_assert(not is_subtype_of( RequiredReadOnlyBool, RequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredMutableBool, RequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredMutableBool, RequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredReadOnlyBool, RequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyBool, RequiredMutableInt))
static_assert( is_assignable_to( RequiredMutableAny, RequiredMutableInt))
static_assert(not is_subtype_of( RequiredMutableAny, RequiredMutableInt))
static_assert(not is_assignable_to( RequiredReadOnlyAny, RequiredMutableInt))
static_assert(not is_subtype_of( RequiredReadOnlyAny, RequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredMutableAny, RequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredMutableAny, RequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredReadOnlyAny, RequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyAny, RequiredMutableInt))
static_assert( is_assignable_to( RequiredMutableInt, RequiredReadOnlyInt))
static_assert( is_subtype_of( RequiredMutableInt, RequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredReadOnlyInt, RequiredReadOnlyInt))
static_assert( is_subtype_of( RequiredReadOnlyInt, RequiredReadOnlyInt))
static_assert(not is_assignable_to( NotRequiredMutableInt, RequiredReadOnlyInt))
static_assert(not is_subtype_of( NotRequiredMutableInt, RequiredReadOnlyInt))
static_assert(not is_assignable_to( NotRequiredReadOnlyInt, RequiredReadOnlyInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyInt, RequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredMutableBool, RequiredReadOnlyInt))
static_assert( is_subtype_of( RequiredMutableBool, RequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredReadOnlyBool, RequiredReadOnlyInt))
static_assert( is_subtype_of( RequiredReadOnlyBool, RequiredReadOnlyInt))
static_assert(not is_assignable_to( NotRequiredMutableBool, RequiredReadOnlyInt))
static_assert(not is_subtype_of( NotRequiredMutableBool, RequiredReadOnlyInt))
static_assert(not is_assignable_to( NotRequiredReadOnlyBool, RequiredReadOnlyInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyBool, RequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredMutableAny, RequiredReadOnlyInt))
static_assert(not is_subtype_of( RequiredMutableAny, RequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredReadOnlyAny, RequiredReadOnlyInt))
static_assert(not is_subtype_of( RequiredReadOnlyAny, RequiredReadOnlyInt))
static_assert(not is_assignable_to( NotRequiredMutableAny, RequiredReadOnlyInt))
static_assert(not is_subtype_of( NotRequiredMutableAny, RequiredReadOnlyInt))
static_assert(not is_assignable_to( NotRequiredReadOnlyAny, RequiredReadOnlyInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyAny, RequiredReadOnlyInt))
static_assert(not is_assignable_to( RequiredMutableInt, NotRequiredMutableInt))
static_assert(not is_subtype_of( RequiredMutableInt, NotRequiredMutableInt))
static_assert(not is_assignable_to( RequiredReadOnlyInt, NotRequiredMutableInt))
static_assert(not is_subtype_of( RequiredReadOnlyInt, NotRequiredMutableInt))
static_assert( is_assignable_to( NotRequiredMutableInt, NotRequiredMutableInt))
static_assert( is_subtype_of( NotRequiredMutableInt, NotRequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredReadOnlyInt, NotRequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyInt, NotRequiredMutableInt))
static_assert(not is_assignable_to( RequiredMutableBool, NotRequiredMutableInt))
static_assert(not is_subtype_of( RequiredMutableBool, NotRequiredMutableInt))
static_assert(not is_assignable_to( RequiredReadOnlyBool, NotRequiredMutableInt))
static_assert(not is_subtype_of( RequiredReadOnlyBool, NotRequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredMutableBool, NotRequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredMutableBool, NotRequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredReadOnlyBool, NotRequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyBool, NotRequiredMutableInt))
static_assert(not is_assignable_to( RequiredMutableAny, NotRequiredMutableInt))
static_assert(not is_subtype_of( RequiredMutableAny, NotRequiredMutableInt))
static_assert(not is_assignable_to( RequiredReadOnlyAny, NotRequiredMutableInt))
static_assert(not is_subtype_of( RequiredReadOnlyAny, NotRequiredMutableInt))
static_assert( is_assignable_to( NotRequiredMutableAny, NotRequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredMutableAny, NotRequiredMutableInt))
static_assert(not is_assignable_to( NotRequiredReadOnlyAny, NotRequiredMutableInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyAny, NotRequiredMutableInt))
static_assert( is_assignable_to( RequiredMutableInt, NotRequiredReadOnlyInt))
static_assert( is_subtype_of( RequiredMutableInt, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredReadOnlyInt, NotRequiredReadOnlyInt))
static_assert( is_subtype_of( RequiredReadOnlyInt, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( NotRequiredMutableInt, NotRequiredReadOnlyInt))
static_assert( is_subtype_of( NotRequiredMutableInt, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( NotRequiredReadOnlyInt, NotRequiredReadOnlyInt))
static_assert( is_subtype_of( NotRequiredReadOnlyInt, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredMutableBool, NotRequiredReadOnlyInt))
static_assert( is_subtype_of( RequiredMutableBool, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredReadOnlyBool, NotRequiredReadOnlyInt))
static_assert( is_subtype_of( RequiredReadOnlyBool, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( NotRequiredMutableBool, NotRequiredReadOnlyInt))
static_assert( is_subtype_of( NotRequiredMutableBool, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( NotRequiredReadOnlyBool, NotRequiredReadOnlyInt))
static_assert( is_subtype_of( NotRequiredReadOnlyBool, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredMutableAny, NotRequiredReadOnlyInt))
static_assert(not is_subtype_of( RequiredMutableAny, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( RequiredReadOnlyAny, NotRequiredReadOnlyInt))
static_assert(not is_subtype_of( RequiredReadOnlyAny, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( NotRequiredMutableAny, NotRequiredReadOnlyInt))
static_assert(not is_subtype_of( NotRequiredMutableAny, NotRequiredReadOnlyInt))
static_assert( is_assignable_to( NotRequiredReadOnlyAny, NotRequiredReadOnlyInt))
static_assert(not is_subtype_of( NotRequiredReadOnlyAny, NotRequiredReadOnlyInt))
# fmt: on
```
All typed dictionaries can be assigned to `Mapping[str, object]`:
@@ -483,10 +676,23 @@ class Person(TypedDict):
name: str
age: int | None
m: Mapping[str, object] = Person(name="Alice", age=30)
alice = Person(name="Alice", age=30)
# Always assignable.
_: Mapping[str, object] = alice
# Follows from above.
_: Mapping[str, Any] = alice
# Also follows from above, because `update` accepts the `SupportsKeysAndGetItem` protocol.
{}.update(alice)
# Not assignable.
# error: [invalid-assignment] "Object of type `Person` is not assignable to `Mapping[str, int]`"
_: Mapping[str, int] = alice
# `Person` does not have `closed=True` or `extra_items`, so it may have additional keys with values
# of unknown type, therefore it can't be assigned to a `Mapping` with value type smaller than `object`.
# error: [invalid-assignment]
_: Mapping[str, str | int | None] = alice
```
They can *not* be assigned to `dict[str, object]`, as that would allow them to be mutated in unsafe
They *cannot* be assigned to `dict[str, object]`, as that would allow them to be mutated in unsafe
ways:
```py
@@ -500,7 +706,7 @@ class Person(TypedDict):
alice: Person = {"name": "Alice"}
# TODO: this should be an invalid-assignment error
# error: [invalid-argument-type] "Argument to function `dangerous` is incorrect: Expected `dict[str, object]`, found `Person`"
dangerous(alice)
reveal_type(alice["name"]) # revealed: str
@@ -515,6 +721,153 @@ alice: dict[str, str] = {"name": "Alice"}
alice: Person = alice
```
## A subtle interaction between two structural assignability rules prevents unsoundness
> For the purposes of these conditions, an open `TypedDict` is treated as if it had **read-only**
> extra items of type `object`.
That language is at the top of [subtyping section of the `TypedDict` spec][subtyping section]. It
sounds like an obscure technicality, especially since `extra_items` is still TODO, but it has an
important interaction with another rule:
> For each item in [the destination type]...If it is non-required...If it is mutable...If \[the
> source type does not have an item with the same key and also\] has extra items, the extra items
> type **must not be read-only**...
In other words, by default (`closed=False`) a `TypedDict` cannot be assigned to a different
`TypedDict` that has an additional, optional, mutable item. That implicit rule turns out to be the
only thing standing in the way of this unsound example:
```py
from typing_extensions import TypedDict, NotRequired
class C(TypedDict):
x: int
y: str
class B(TypedDict):
x: int
class A(TypedDict):
x: int
y: NotRequired[object] # incompatible with both C and (surprisingly!) B
def b_from_c(c: C) -> B:
return c # allowed
def a_from_b(b: B) -> A:
# error: [invalid-return-type] "Return type does not match returned value: expected `A`, found `B`"
return b
# The [invalid-return-type] error above is the only thing that keeps us from corrupting the type of c['y'].
c: C = {"x": 1, "y": "hello"}
a: A = a_from_b(b_from_c(c))
a["y"] = 42
```
If the additional, optional item in the target is read-only, the requirements are *somewhat*
relaxed. In this case, because the source might contain have undeclared extra items of any type, the
target item must be assignable from `object`:
```py
from typing_extensions import ReadOnly
class A2(TypedDict):
x: int
y: NotRequired[ReadOnly[object]]
def a2_from_b(b: B) -> A2:
return b # allowed
class A3(TypedDict):
x: int
y: NotRequired[ReadOnly[int]] # not assignable from `object`
def a3_from_b(b: B) -> A3:
return b # error: [invalid-return-type]
```
## Structural assignability supports `TypedDict`s that contain other `TypedDict`s
```py
from typing_extensions import TypedDict, ReadOnly, NotRequired
from ty_extensions import static_assert, is_assignable_to, is_subtype_of
class Inner1(TypedDict):
name: str
class Inner2(TypedDict):
name: str
class Outer1(TypedDict):
a: Inner1
b: ReadOnly[Inner1]
c: NotRequired[Inner1]
d: ReadOnly[NotRequired[Inner1]]
class Outer2(TypedDict):
a: Inner2
b: ReadOnly[Inner2]
c: NotRequired[Inner2]
d: ReadOnly[NotRequired[Inner2]]
def _(o1: Outer1, o2: Outer2):
static_assert(is_assignable_to(Outer1, Outer2))
static_assert(is_subtype_of(Outer1, Outer2))
static_assert(is_assignable_to(Outer2, Outer1))
static_assert(is_subtype_of(Outer2, Outer1))
```
This also extends to gradual types:
```py
from typing import Any
class Inner3(TypedDict):
name: Any
class Outer3(TypedDict):
a: Inner3
b: ReadOnly[Inner3]
c: NotRequired[Inner3]
d: ReadOnly[NotRequired[Inner3]]
class Outer4(TypedDict):
a: Any
b: ReadOnly[Any]
c: NotRequired[Any]
d: ReadOnly[NotRequired[Any]]
def _(o1: Outer1, o2: Outer2, o3: Outer3, o4: Outer4):
static_assert(is_assignable_to(Outer3, Outer1))
static_assert(not is_subtype_of(Outer3, Outer1))
static_assert(is_assignable_to(Outer4, Outer1))
static_assert(not is_subtype_of(Outer4, Outer1))
static_assert(is_assignable_to(Outer3, Outer2))
static_assert(not is_subtype_of(Outer3, Outer2))
static_assert(is_assignable_to(Outer4, Outer2))
static_assert(not is_subtype_of(Outer4, Outer2))
static_assert(is_assignable_to(Outer1, Outer3))
static_assert(not is_subtype_of(Outer1, Outer3))
static_assert(is_assignable_to(Outer2, Outer3))
static_assert(not is_subtype_of(Outer2, Outer3))
static_assert(is_assignable_to(Outer3, Outer3))
static_assert(is_subtype_of(Outer3, Outer3))
static_assert(is_assignable_to(Outer4, Outer3))
static_assert(not is_subtype_of(Outer4, Outer3))
static_assert(is_assignable_to(Outer1, Outer4))
static_assert(not is_subtype_of(Outer1, Outer4))
static_assert(is_assignable_to(Outer2, Outer4))
static_assert(not is_subtype_of(Outer2, Outer4))
static_assert(is_assignable_to(Outer3, Outer4))
static_assert(not is_subtype_of(Outer3, Outer4))
static_assert(is_assignable_to(Outer4, Outer4))
static_assert(is_subtype_of(Outer4, Outer4))
```
## Key-based access
### Reading
@@ -835,7 +1188,7 @@ def combine(p: Person, e: Employee):
reveal_type(p | p) # revealed: Person
reveal_type(e | e) # revealed: Employee
# TODO: Should be `Person` once we support subtyping for TypedDicts
# TODO: Should be `Person`; simplifying TypedDicts in Unions is pending better cycle handling
reveal_type(p | e) # revealed: Person | Employee
```
@@ -915,7 +1268,8 @@ emp_invalid2 = Employee(id=3)
### Legacy generics
```py
from typing import Generic, TypeVar, TypedDict
from typing import Generic, TypeVar, TypedDict, Any
from ty_extensions import static_assert, is_assignable_to, is_subtype_of
T = TypeVar("T")
@@ -940,6 +1294,14 @@ items2: Items[str] = {"items": ["a", "b", "c"]}
items3: Items[int] = {"items": homogeneous_list(1, 2, 3)}
items4: Items[str] = {"items": homogeneous_list("a", "b", "c")}
items5: Items[int | str] = {"items": homogeneous_list(1, 2, 3)}
# structural assignability
static_assert(is_assignable_to(Items[int], Items[int]))
static_assert(is_subtype_of(Items[int], Items[int]))
static_assert(not is_assignable_to(Items[str], Items[int]))
static_assert(not is_subtype_of(Items[str], Items[int]))
static_assert(is_assignable_to(Items[Any], Items[int]))
static_assert(not is_subtype_of(Items[Any], Items[int]))
```
### PEP-695 generics
@@ -950,7 +1312,8 @@ python-version = "3.12"
```
```py
from typing import TypedDict
from typing import TypedDict, Any
from ty_extensions import static_assert, is_assignable_to, is_subtype_of
class TaggedData[T](TypedDict):
data: T
@@ -973,6 +1336,14 @@ items2: Items[str] = {"items": ["a", "b", "c"]}
items3: Items[int] = {"items": homogeneous_list(1, 2, 3)}
items4: Items[str] = {"items": homogeneous_list("a", "b", "c")}
items5: Items[int | str] = {"items": homogeneous_list(1, 2, 3)}
# structural assignability
static_assert(is_assignable_to(Items[int], Items[int]))
static_assert(is_subtype_of(Items[int], Items[int]))
static_assert(not is_assignable_to(Items[str], Items[int]))
static_assert(not is_subtype_of(Items[str], Items[int]))
static_assert(is_assignable_to(Items[Any], Items[int]))
static_assert(not is_subtype_of(Items[Any], Items[int]))
```
## Recursive `TypedDict`
@@ -997,6 +1368,20 @@ nested: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": "n3",
nested_invalid: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": 3, "parent": None}}}
```
Structural assignment works for recursive `TypedDict`s too:
```py
class Person(TypedDict):
name: str
parent: Person | None
def _(node: Node, person: Person):
_: Person = node
_: Node = person
_: Node = Person(name="Alice", parent=Node(name="Bob", parent=Person(name="Charlie", parent=None)))
```
## Function/assignment syntax
This is not yet supported. Make sure that we do not emit false positives for this syntax:
@@ -1165,4 +1550,5 @@ reveal_type(actual_td) # revealed: ActualTypedDict
reveal_type(actual_td["name"]) # revealed: str
```
[subtyping section]: https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html

View File

@@ -296,7 +296,7 @@ impl ModuleName {
}
/// Computes the absolute module name from the LHS components of `from LHS import RHS`
pub(crate) fn from_identifier_parts(
pub fn from_identifier_parts(
db: &dyn Db,
importing_file: File,
module: Option<&str>,

View File

@@ -91,11 +91,7 @@ impl<'db> SemanticModel<'db> {
}
/// Returns completions for symbols available in a `from module import <CURSOR>` context.
pub fn from_import_completions(
&self,
import: &ast::StmtImportFrom,
_name: Option<usize>,
) -> Vec<Completion<'db>> {
pub fn from_import_completions(&self, import: &ast::StmtImportFrom) -> Vec<Completion<'db>> {
let module_name = match ModuleName::from_import_statement(self.db, self.file, import) {
Ok(module_name) => module_name,
Err(err) => {
@@ -110,69 +106,8 @@ impl<'db> SemanticModel<'db> {
self.module_completions(&module_name)
}
/// Returns completions only for submodules for the module
/// identified by `name` in `import`.
///
/// For example, `import re, os.<CURSOR>, zlib`.
pub fn import_submodule_completions(
&self,
import: &ast::StmtImport,
name: usize,
) -> Vec<Completion<'db>> {
let module_ident = &import.names[name].name;
let Some((parent_ident, _)) = module_ident.rsplit_once('.') else {
return vec![];
};
let module_name =
match ModuleName::from_identifier_parts(self.db, self.file, Some(parent_ident), 0) {
Ok(module_name) => module_name,
Err(err) => {
tracing::debug!(
"Could not extract module name from `{module:?}`: {err:?}",
module = module_ident,
);
return vec![];
}
};
self.import_submodule_completions_for_name(&module_name)
}
/// Returns completions only for submodules for the module
/// used in a `from module import attribute` statement.
///
/// For example, `from os.<CURSOR>`.
pub fn from_import_submodule_completions(
&self,
import: &ast::StmtImportFrom,
) -> Vec<Completion<'db>> {
let level = import.level;
let Some(module_ident) = import.module.as_deref() else {
return vec![];
};
let Some((parent_ident, _)) = module_ident.rsplit_once('.') else {
return vec![];
};
let module_name = match ModuleName::from_identifier_parts(
self.db,
self.file,
Some(parent_ident),
level,
) {
Ok(module_name) => module_name,
Err(err) => {
tracing::debug!(
"Could not extract module name from `{module:?}` with level {level}: {err:?}",
module = import.module,
level = import.level,
);
return vec![];
}
};
self.import_submodule_completions_for_name(&module_name)
}
/// Returns submodule-only completions for the given module.
fn import_submodule_completions_for_name(
pub fn import_submodule_completions_for_name(
&self,
module_name: &ModuleName,
) -> Vec<Completion<'db>> {

View File

@@ -50,8 +50,8 @@ use crate::types::constraints::{
};
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
pub use crate::types::display::DisplaySettings;
use crate::types::display::TupleSpecialization;
pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails};
use crate::types::enums::{enum_metadata, is_single_member_enum};
use crate::types::function::{
DataclassTransformerFlags, DataclassTransformerParams, FunctionDecorators, FunctionSpans,
@@ -863,6 +863,10 @@ impl<'db> Type<'db> {
.is_some()
}
fn is_typealias_special_form(&self) -> bool {
matches!(self, Type::SpecialForm(SpecialFormType::TypeAlias))
}
/// Return true if this type overrides __eq__ or __ne__ methods
fn overrides_equality(&self, db: &'db dyn Db) -> bool {
let check_dunder = |dunder_name, allowed_return_value| {
@@ -2093,14 +2097,6 @@ impl<'db> Type<'db> {
ConstraintSet::from(false)
}
(Type::TypedDict(_), _) => {
// TODO: Implement assignability and subtyping for TypedDict
ConstraintSet::from(relation.is_assignability())
}
// A non-`TypedDict` cannot subtype a `TypedDict`
(_, Type::TypedDict(_)) => ConstraintSet::from(false),
// Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`.
// If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively.
(left, Type::AlwaysFalsy) => ConstraintSet::from(left.bool(db).is_always_false()),
@@ -2207,6 +2203,38 @@ impl<'db> Type<'db> {
// A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
(Type::ProtocolInstance(_), _) => ConstraintSet::from(false),
(Type::TypedDict(self_typeddict), Type::TypedDict(other_typeddict)) => relation_visitor
.visit((self, target, relation), || {
self_typeddict.has_relation_to_impl(
db,
other_typeddict,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
}),
// TODO: When we support `closed` and/or `extra_items`, we could allow assignments to other
// compatible `Mapping`s. `extra_items` could also allow for some assignments to `dict`, as
// long as `total=False`. (But then again, does anyone want a non-total `TypedDict` where all
// key types are a supertype of the extra items type?)
(Type::TypedDict(_), _) => relation_visitor.visit((self, target, relation), || {
KnownClass::Mapping
.to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::object()])
.has_relation_to_impl(
db,
target,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
}),
// A non-`TypedDict` cannot subtype a `TypedDict`
(_, Type::TypedDict(_)) => ConstraintSet::from(false),
// All `StringLiteral` types are a subtype of `LiteralString`.
(Type::StringLiteral(_), Type::LiteralString) => ConstraintSet::from(true),
@@ -4379,11 +4407,6 @@ impl<'db> Type<'db> {
Type::KnownBoundMethod(KnownBoundMethodType::StrStartswith(literal)),
)
.into(),
Type::NominalInstance(instance)
if instance.has_known_class(db, KnownClass::Path) && name == "open" =>
{
Place::bound(Type::KnownBoundMethod(KnownBoundMethodType::PathOpen)).into()
}
Type::ClassLiteral(class)
if name == "range" && class.is_known(db, KnownClass::ConstraintSet) =>
@@ -6738,17 +6761,10 @@ impl<'db> Type<'db> {
invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Generic],
fallback_type: Type::unknown(),
}),
KnownInstanceType::UnionType(list) => {
let mut builder = UnionBuilder::new(db);
let inferred_as = list.inferred_as(db);
for element in list.elements(db) {
builder = builder.add(if inferred_as.type_expression() {
*element
} else {
element.in_type_expression(db, scope_id, typevar_binding_context)?
});
}
Ok(builder.build())
KnownInstanceType::UnionType(instance) => {
// Cloning here is cheap if the result is a `Type` (which is `Copy`). It's more
// expensive if there are errors.
instance.union_type(db).clone()
}
KnownInstanceType::Literal(ty) => Ok(ty.inner(db)),
KnownInstanceType::Annotated(ty) => Ok(ty.inner(db)),
@@ -6764,6 +6780,7 @@ impl<'db> Type<'db> {
Ok(ty.inner(db).to_meta_type(db))
}
KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)),
KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)),
},
Type::SpecialForm(special_form) => match special_form {
@@ -6818,7 +6835,15 @@ impl<'db> Type<'db> {
Ok(typing_self(db, scope_id, typevar_binding_context, class).unwrap_or(*self))
}
SpecialFormType::TypeAlias => Ok(Type::Dynamic(DynamicType::TodoTypeAlias)),
// We ensure that `typing.TypeAlias` used in the expected position (annotating an
// annotated assignment statement) doesn't reach here. Using it in any other type
// expression is an error.
SpecialFormType::TypeAlias => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec_inline![
InvalidTypeExpression::TypeAlias
],
fallback_type: Type::unknown(),
}),
SpecialFormType::TypedDict => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec_inline![
InvalidTypeExpression::TypedDict
@@ -7296,7 +7321,6 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(_)
| Type::KnownBoundMethod(
KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@@ -7457,7 +7481,6 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(_)
| Type::KnownBoundMethod(
KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@@ -7510,7 +7533,7 @@ impl<'db> Type<'db> {
name = enum_literal.name(db)
),
),
Type::SpecialForm(special_form) => Type::string_literal(db, special_form.repr()),
Type::SpecialForm(special_form) => Type::string_literal(db, &special_form.to_string()),
Type::KnownInstance(known_instance) => Type::StringLiteral(StringLiteralType::new(
db,
known_instance.repr(db).to_compact_string(),
@@ -7532,7 +7555,7 @@ impl<'db> Type<'db> {
Type::string_literal(db, &format!("'{}'", literal.value(db).escape_default()))
}
Type::LiteralString => Type::LiteralString,
Type::SpecialForm(special_form) => Type::string_literal(db, special_form.repr()),
Type::SpecialForm(special_form) => Type::string_literal(db, &special_form.to_string()),
Type::KnownInstance(known_instance) => Type::StringLiteral(StringLiteralType::new(
db,
known_instance.repr(db).to_compact_string(),
@@ -7584,6 +7607,11 @@ impl<'db> Type<'db> {
Self::TypeAlias(alias) => alias.value_type(db).definition(db),
Self::NewTypeInstance(newtype) => Some(TypeDefinition::NewType(newtype.definition(db))),
Self::PropertyInstance(property) => property
.getter(db)
.and_then(|getter|getter.definition(db))
.or_else(||property.setter(db).and_then(|setter|setter.definition(db))),
Self::StringLiteral(_)
| Self::BooleanLiteral(_)
| Self::LiteralString
@@ -7595,7 +7623,6 @@ impl<'db> Type<'db> {
| Self::WrapperDescriptor(_)
| Self::DataclassDecorator(_)
| Self::DataclassTransformer(_)
| Self::PropertyInstance(_)
| Self::BoundSuper(_) => self.to_meta_type(db).definition(db),
Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)),
@@ -7605,19 +7632,22 @@ impl<'db> Type<'db> {
Protocol::Synthesized(_) => None,
},
Type::TypedDict(typed_dict) => {
Self::TypedDict(typed_dict) => {
Some(TypeDefinition::Class(typed_dict.defining_class().definition(db)))
}
Self::Union(_) | Self::Intersection(_) => None,
Self::SpecialForm(special_form) => special_form.definition(db),
Self::Never => Type::SpecialForm(SpecialFormType::Never).definition(db),
Self::Dynamic(DynamicType::Any) => Type::SpecialForm(SpecialFormType::Any).definition(db),
Self::Dynamic(DynamicType::Unknown) => Type::SpecialForm(SpecialFormType::Unknown).definition(db),
Self::AlwaysTruthy => Type::SpecialForm(SpecialFormType::AlwaysTruthy).definition(db),
Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db),
// These types have no definition
Self::Dynamic(_)
| Self::Never
Self::Dynamic(DynamicType::Divergent(_) | DynamicType::Todo(_) | DynamicType::TodoUnpack)
| Self::Callable(_)
| Self::AlwaysTruthy
| Self::AlwaysFalsy
| Self::SpecialForm(_)
| Self::TypeIs(_) => None,
}
}
@@ -8004,9 +8034,9 @@ pub enum KnownInstanceType<'db> {
/// `ty_extensions.Specialization`.
Specialization(Specialization<'db>),
/// A single instance of `types.UnionType`, which stores the left- and
/// right-hand sides of a PEP 604 union.
UnionType(InternedTypes<'db>),
/// A single instance of `types.UnionType`, which stores the elements of
/// a PEP 604 union, or a `typing.Union`.
UnionType(UnionTypeInstance<'db>),
/// A single instance of `typing.Literal`
Literal(InternedType<'db>),
@@ -8020,6 +8050,9 @@ pub enum KnownInstanceType<'db> {
/// An instance of `typing.GenericAlias` representing a `Callable[...]` expression.
Callable(CallableType<'db>),
/// A literal string which is the right-hand side of a PEP 613 `TypeAlias`.
LiteralStringAlias(InternedType<'db>),
/// An identity callable created with `typing.NewType(name, base)`, which behaves like a
/// subtype of `base` in type expressions. See the `struct NewType` payload for an example.
NewType(NewType<'db>),
@@ -8052,14 +8085,15 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
visitor.visit_type(db, default_ty);
}
}
KnownInstanceType::UnionType(list) => {
for element in list.elements(db) {
visitor.visit_type(db, *element);
KnownInstanceType::UnionType(instance) => {
if let Ok(union_type) = instance.union_type(db) {
visitor.visit_type(db, *union_type);
}
}
KnownInstanceType::Literal(ty)
| KnownInstanceType::Annotated(ty)
| KnownInstanceType::TypeGenericAlias(ty) => {
| KnownInstanceType::TypeGenericAlias(ty)
| KnownInstanceType::LiteralStringAlias(ty) => {
visitor.visit_type(db, ty.inner(db));
}
KnownInstanceType::Callable(callable) => {
@@ -8098,11 +8132,14 @@ impl<'db> KnownInstanceType<'db> {
Self::TypeAliasType(type_alias.normalized_impl(db, visitor))
}
Self::Field(field) => Self::Field(field.normalized_impl(db, visitor)),
Self::UnionType(list) => Self::UnionType(list.normalized_impl(db, visitor)),
Self::UnionType(instance) => Self::UnionType(instance.normalized_impl(db, visitor)),
Self::Literal(ty) => Self::Literal(ty.normalized_impl(db, visitor)),
Self::Annotated(ty) => Self::Annotated(ty.normalized_impl(db, visitor)),
Self::TypeGenericAlias(ty) => Self::TypeGenericAlias(ty.normalized_impl(db, visitor)),
Self::Callable(callable) => Self::Callable(callable.normalized_impl(db, visitor)),
Self::LiteralStringAlias(ty) => {
Self::LiteralStringAlias(ty.normalized_impl(db, visitor))
}
Self::NewType(newtype) => Self::NewType(
newtype
.map_base_class_type(db, |class_type| class_type.normalized_impl(db, visitor)),
@@ -8138,6 +8175,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Annotated(_)
| Self::TypeGenericAlias(_)
| Self::Callable(_) => KnownClass::GenericAlias,
Self::LiteralStringAlias(_) => KnownClass::Str,
Self::NewType(_) => KnownClass::NewType,
}
}
@@ -8241,6 +8279,7 @@ impl<'db> KnownInstanceType<'db> {
KnownInstanceType::TypeGenericAlias(_) | KnownInstanceType::Callable(_) => {
f.write_str("GenericAlias")
}
KnownInstanceType::LiteralStringAlias(_) => f.write_str("str"),
KnownInstanceType::NewType(declaration) => {
write!(f, "<NewType pseudo-class '{}'>", declaration.name(self.db))
}
@@ -8282,9 +8321,6 @@ pub enum DynamicType<'db> {
///
/// This variant should be created with the `todo_type!` macro.
Todo(TodoType),
/// A special Todo-variant for type aliases declared using `typing.TypeAlias`.
/// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions.
TodoTypeAlias,
/// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]`
TodoUnpack,
/// A type that is determined to be divergent during type inference for a recursive function.
@@ -8316,13 +8352,6 @@ impl std::fmt::Display for DynamicType<'_> {
f.write_str("@Todo")
}
}
DynamicType::TodoTypeAlias => {
if cfg!(debug_assertions) {
f.write_str("@Todo(Support for `typing.TypeAlias`)")
} else {
f.write_str("@Todo")
}
}
DynamicType::Divergent(_) => f.write_str("Divergent"),
}
}
@@ -8430,7 +8459,7 @@ impl<'db> TypeAndQualifiers<'db> {
/// Error struct providing information on type(s) that were deemed to be invalid
/// in a type expression context, and the type we should therefore fallback to
/// for the problematic type expression.
#[derive(Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)]
pub struct InvalidTypeExpressionError<'db> {
fallback_type: Type<'db>,
invalid_expressions: smallvec::SmallVec<[InvalidTypeExpression<'db>; 1]>,
@@ -8461,7 +8490,7 @@ impl<'db> InvalidTypeExpressionError<'db> {
}
/// Enumeration of various types that are invalid in type-expression contexts
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
enum InvalidTypeExpression<'db> {
/// Some types always require exactly one argument when used in a type expression
RequiresOneArgument(Type<'db>),
@@ -8485,6 +8514,9 @@ enum InvalidTypeExpression<'db> {
Specialization,
/// Same for `typing.TypedDict`
TypedDict,
/// Same for `typing.TypeAlias`, anywhere except for as the sole annotation on an annotated
/// assignment
TypeAlias,
/// Type qualifiers are always invalid in *type expressions*,
/// but these ones are okay with 0 arguments in *annotation expressions*
TypeQualifier(SpecialFormType),
@@ -8546,6 +8578,11 @@ impl<'db> InvalidTypeExpression<'db> {
"The special form `typing.TypedDict` is not allowed in type expressions. \
Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?")
}
InvalidTypeExpression::TypeAlias => {
f.write_str(
"`typing.TypeAlias` is only allowed as the sole annotation on an annotated assignment",
)
}
InvalidTypeExpression::TypeQualifier(qualifier) => write!(
f,
"Type qualifier `{qualifier}` is not allowed in type expressions \
@@ -9399,39 +9436,106 @@ impl InferredAs {
}
}
/// A salsa-interned list of types.
/// Contains information about a `types.UnionType` instance built from a PEP 604
/// union or a legacy `typing.Union[…]` annotation in a value expression context,
/// e.g. `IntOrStr = int | str` or `IntOrStr = Union[int, str]`.
///
/// # Ordering
/// Ordering is based on the context's salsa-assigned id and not on its values.
/// The id may change between runs, or when the context was garbage collected and recreated.
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
pub struct InternedTypes<'db> {
#[returns(deref)]
elements: Box<[Type<'db>]>,
inferred_as: InferredAs,
pub struct UnionTypeInstance<'db> {
/// The types of the elements of this union, as they were inferred in a value
/// expression context. For `int | str`, this would contain `<class 'int'>` and
/// `<class 'str'>`. For `Union[int, str]`, this field is `None`, as we infer
/// the elements as type expressions. Use `value_expression_types` to get the
/// corresponding value expression types.
#[expect(clippy::ref_option)]
#[returns(ref)]
_value_expr_types: Option<Box<[Type<'db>]>>,
/// The type of the full union, which can be used when this `UnionType` instance
/// is used in a type expression context. For `int | str`, this would contain
/// `Ok(int | str)`. If any of the element types could not be converted, this
/// contains the first encountered error.
#[returns(ref)]
union_type: Result<Type<'db>, InvalidTypeExpressionError<'db>>,
}
impl get_size2::GetSize for InternedTypes<'_> {}
impl get_size2::GetSize for UnionTypeInstance<'_> {}
impl<'db> InternedTypes<'db> {
pub(crate) fn from_elements(
impl<'db> UnionTypeInstance<'db> {
pub(crate) fn from_value_expression_types(
db: &'db dyn Db,
elements: impl IntoIterator<Item = Type<'db>>,
inferred_as: InferredAs,
) -> InternedTypes<'db> {
InternedTypes::new(db, elements.into_iter().collect::<Box<[_]>>(), inferred_as)
value_expr_types: impl IntoIterator<Item = Type<'db>>,
scope_id: ScopeId<'db>,
typevar_binding_context: Option<Definition<'db>>,
) -> Type<'db> {
let value_expr_types = value_expr_types.into_iter().collect::<Box<_>>();
let mut builder = UnionBuilder::new(db);
for ty in &value_expr_types {
match ty.in_type_expression(db, scope_id, typevar_binding_context) {
Ok(ty) => builder.add_in_place(ty),
Err(error) => {
return Type::KnownInstance(KnownInstanceType::UnionType(
UnionTypeInstance::new(db, Some(value_expr_types), Err(error)),
));
}
}
}
Type::KnownInstance(KnownInstanceType::UnionType(UnionTypeInstance::new(
db,
Some(value_expr_types),
Ok(builder.build()),
)))
}
/// Get the types of the elements of this union as they would appear in a value
/// expression context. For a PEP 604 union, we return the actual types that were
/// inferred when we encountered the union in a value expression context. For a
/// legacy `typing.Union[…]` annotation, we turn the type-expression types into
/// their corresponding value-expression types, i.e. we turn instances like `int`
/// into class literals like `<class 'int'>`. This operation is potentially lossy.
pub(crate) fn value_expression_types(
self,
db: &'db dyn Db,
) -> Result<impl Iterator<Item = Type<'db>> + 'db, InvalidTypeExpressionError<'db>> {
let to_class_literal = |ty: Type<'db>| {
ty.as_nominal_instance()
.map(|instance| Type::ClassLiteral(instance.class(db).class_literal(db).0))
.unwrap_or_else(Type::unknown)
};
if let Some(value_expr_types) = self._value_expr_types(db) {
Ok(Either::Left(value_expr_types.iter().copied()))
} else {
match self.union_type(db).clone()? {
Type::Union(union) => Ok(Either::Right(Either::Left(
union.elements(db).iter().copied().map(to_class_literal),
))),
ty => Ok(Either::Right(Either::Right(std::iter::once(
to_class_literal(ty),
)))),
}
}
}
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
InternedTypes::new(
db,
self.elements(db)
let value_expr_types = self._value_expr_types(db).as_ref().map(|types| {
types
.iter()
.map(|ty| ty.normalized_impl(db, visitor))
.collect::<Box<[_]>>(),
self.inferred_as(db),
)
.collect::<Box<_>>()
});
let union_type = self
.union_type(db)
.clone()
.map(|ty| ty.normalized_impl(db, visitor));
Self::new(db, value_expr_types, union_type)
}
}
@@ -10986,8 +11090,6 @@ pub enum KnownBoundMethodType<'db> {
/// this allows us to understand statically known branches for common tests such as
/// `if sys.platform.startswith("freebsd")`.
StrStartswith(StringLiteralType<'db>),
/// Method wrapper for `Path.open`,
PathOpen,
// ConstraintSet methods
ConstraintSetRange,
@@ -11022,8 +11124,7 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size
KnownBoundMethodType::StrStartswith(string_literal) => {
visitor.visit_type(db, Type::StringLiteral(string_literal));
}
KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
@@ -11081,8 +11182,7 @@ impl<'db> KnownBoundMethodType<'db> {
ConstraintSet::from(self == other)
}
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen)
| (
(
KnownBoundMethodType::ConstraintSetRange,
KnownBoundMethodType::ConstraintSetRange,
)
@@ -11117,7 +11217,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@@ -11130,7 +11229,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@@ -11173,8 +11271,7 @@ impl<'db> KnownBoundMethodType<'db> {
ConstraintSet::from(self == other)
}
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen)
| (
(
KnownBoundMethodType::ConstraintSetRange,
KnownBoundMethodType::ConstraintSetRange,
)
@@ -11213,7 +11310,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@@ -11226,7 +11322,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@@ -11253,7 +11348,6 @@ impl<'db> KnownBoundMethodType<'db> {
KnownBoundMethodType::PropertyDunderSet(property.normalized_impl(db, visitor))
}
KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@@ -11272,7 +11366,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_) => KnownClass::MethodWrapperType,
KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType,
KnownBoundMethodType::PathOpen => KnownClass::MethodType,
KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@@ -11378,9 +11471,6 @@ impl<'db> KnownBoundMethodType<'db> {
Some(KnownClass::Bool.to_instance(db)),
)))
}
KnownBoundMethodType::PathOpen => {
Either::Right(std::iter::once(Signature::todo("`Path.open` return type")))
}
KnownBoundMethodType::ConstraintSetRange => {
Either::Right(std::iter::once(Signature::new(

View File

@@ -502,6 +502,17 @@ impl<'db> UnionBuilder<'db> {
return;
}
// Comparing `TypedDict`s for redundancy requires iterating over their fields, which is
// problematic if some of those fields point to recursive `Union`s. To avoid cycles,
// compare `TypedDict`s by name/identity instead of using the `has_relation_to`
// machinery.
if let (Type::TypedDict(element_td), Type::TypedDict(ty_td)) = (element_type, ty) {
if element_td == ty_td {
return;
}
continue;
}
if should_simplify_full && !matches!(element_type, Type::TypeAlias(_)) {
if ty.is_redundant_with(self.db, element_type) {
return;

View File

@@ -66,6 +66,39 @@ impl<'a, 'db> CallArguments<'a, 'db> {
.collect()
}
/// Like [`Self::from_arguments`] but fills as much typing info in as possible.
///
/// This currently only exists for the LSP usecase, and shouldn't be used in normal
/// typechecking.
pub(crate) fn from_arguments_typed(
arguments: &'a ast::Arguments,
mut infer_argument_type: impl FnMut(Option<&ast::Expr>, &ast::Expr) -> Type<'db>,
) -> Self {
arguments
.arguments_source_order()
.map(|arg_or_keyword| match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
let ty = infer_argument_type(Some(arg), value);
(Argument::Variadic, Some(ty))
}
_ => {
let ty = infer_argument_type(None, arg);
(Argument::Positional, Some(ty))
}
},
ast::ArgOrKeyword::Keyword(ast::Keyword { arg, value, .. }) => {
let ty = infer_argument_type(None, value);
if let Some(arg) = arg {
(Argument::Keyword(&arg.id), Some(ty))
} else {
(Argument::Keywords, Some(ty))
}
}
})
.collect()
}
/// Create a [`CallArguments`] with no arguments.
pub(crate) fn none() -> Self {
Self::default()

View File

@@ -126,6 +126,16 @@ fn try_metaclass_cycle_initial<'db>(
})
}
fn fields_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
_self: ClassLiteral<'db>,
_specialization: Option<Specialization<'db>>,
_field_policy: CodeGeneratorKind<'db>,
) -> FxIndexMap<Name, Field<'db>> {
FxIndexMap::default()
}
/// A category of classes with code generation capabilities (with synthesized methods).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
pub(crate) enum CodeGeneratorKind<'db> {
@@ -555,8 +565,10 @@ impl<'db> ClassType<'db> {
TypeRelation::Assignability => ConstraintSet::from(!other.is_final(db)),
},
// Protocol and Generic are not represented by a ClassType.
ClassBase::Protocol | ClassBase::Generic => ConstraintSet::from(false),
// Protocol, Generic, and TypedDict are not represented by a ClassType.
ClassBase::Protocol | ClassBase::Generic | ClassBase::TypedDict => {
ConstraintSet::from(false)
}
ClassBase::Class(base) => match (base, other) {
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => {
@@ -579,11 +591,6 @@ impl<'db> ClassType<'db> {
ConstraintSet::from(false)
}
},
ClassBase::TypedDict => {
// TODO: Implement subclassing and assignability for TypedDicts.
ConstraintSet::from(true)
}
}
})
}
@@ -2824,7 +2831,10 @@ impl<'db> ClassLiteral<'db> {
/// Returns a list of all annotated attributes defined in this class, or any of its superclasses.
///
/// See [`ClassLiteral::own_fields`] for more details.
#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)]
#[salsa::tracked(
returns(ref),
cycle_initial=fields_cycle_initial,
heap_size=get_size2::GetSize::get_heap_size)]
pub(crate) fn fields(
self,
db: &'db dyn Db,
@@ -3933,6 +3943,7 @@ pub enum KnownClass {
SupportsIndex,
Iterable,
Iterator,
Mapping,
// typing_extensions
ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features
// Collections
@@ -4047,6 +4058,7 @@ impl KnownClass {
| Self::ABCMeta
| Self::Iterable
| Self::Iterator
| Self::Mapping
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
// and raises a `TypeError` in Python >=3.14
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
@@ -4133,6 +4145,7 @@ impl KnownClass {
| KnownClass::SupportsIndex
| KnownClass::Iterable
| KnownClass::Iterator
| KnownClass::Mapping
| KnownClass::ChainMap
| KnownClass::Counter
| KnownClass::DefaultDict
@@ -4218,6 +4231,7 @@ impl KnownClass {
| KnownClass::SupportsIndex
| KnownClass::Iterable
| KnownClass::Iterator
| KnownClass::Mapping
| KnownClass::ChainMap
| KnownClass::Counter
| KnownClass::DefaultDict
@@ -4303,6 +4317,7 @@ impl KnownClass {
| KnownClass::SupportsIndex
| KnownClass::Iterable
| KnownClass::Iterator
| KnownClass::Mapping
| KnownClass::ChainMap
| KnownClass::Counter
| KnownClass::DefaultDict
@@ -4419,7 +4434,8 @@ impl KnownClass {
| Self::BuiltinFunctionType
| Self::ProtocolMeta
| Self::Template
| KnownClass::Path => false,
| Self::Path
| Self::Mapping => false,
}
}
@@ -4492,6 +4508,7 @@ impl KnownClass {
| KnownClass::SupportsIndex
| KnownClass::Iterable
| KnownClass::Iterator
| KnownClass::Mapping
| KnownClass::ChainMap
| KnownClass::Counter
| KnownClass::DefaultDict
@@ -4582,6 +4599,7 @@ impl KnownClass {
Self::Super => "super",
Self::Iterable => "Iterable",
Self::Iterator => "Iterator",
Self::Mapping => "Mapping",
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
Self::StdlibAlias => "_Alias",
// This is the name the type of `sys.version_info` has in typeshed,
@@ -4828,7 +4846,7 @@ impl KnownClass {
}
/// Return the module in which we should look up the definition for this class
fn canonical_module(self, db: &dyn Db) -> KnownModule {
pub(super) fn canonical_module(self, db: &dyn Db) -> KnownModule {
match self {
Self::Bool
| Self::Object
@@ -4880,6 +4898,7 @@ impl KnownClass {
| Self::StdlibAlias
| Self::Iterable
| Self::Iterator
| Self::Mapping
| Self::ProtocolMeta
| Self::SupportsIndex => KnownModule::Typing,
Self::TypeAliasType
@@ -5010,6 +5029,7 @@ impl KnownClass {
| Self::InitVar
| Self::Iterable
| Self::Iterator
| Self::Mapping
| Self::NamedTupleFallback
| Self::NamedTupleLike
| Self::ConstraintSet
@@ -5100,6 +5120,7 @@ impl KnownClass {
| Self::InitVar
| Self::Iterable
| Self::Iterator
| Self::Mapping
| Self::NamedTupleFallback
| Self::NamedTupleLike
| Self::ConstraintSet
@@ -5163,6 +5184,7 @@ impl KnownClass {
"TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar],
"Iterable" => &[Self::Iterable],
"Iterator" => &[Self::Iterator],
"Mapping" => &[Self::Mapping],
"ParamSpec" => &[Self::ParamSpec],
"ParamSpecArgs" => &[Self::ParamSpecArgs],
"ParamSpecKwargs" => &[Self::ParamSpecKwargs],
@@ -5304,6 +5326,7 @@ impl KnownClass {
| Self::TypeVarTuple
| Self::Iterable
| Self::Iterator
| Self::Mapping
| Self::ProtocolMeta
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions),

View File

@@ -48,9 +48,7 @@ impl<'db> ClassBase<'db> {
ClassBase::Class(class) => class.name(db),
ClassBase::Dynamic(DynamicType::Any) => "Any",
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
ClassBase::Dynamic(
DynamicType::Todo(_) | DynamicType::TodoTypeAlias | DynamicType::TodoUnpack,
) => "@Todo",
ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack) => "@Todo",
ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent",
ClassBase::Protocol => "Protocol",
ClassBase::Generic => "Generic",
@@ -179,6 +177,7 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::Specialization(_)
| KnownInstanceType::UnionType(_)
| KnownInstanceType::Literal(_)
| KnownInstanceType::LiteralStringAlias(_)
// A class inheriting from a newtype would make intuitive sense, but newtype
// wrappers are just identity callables at runtime, so this sort of inheritance
// doesn't work and isn't allowed.

View File

@@ -13,6 +13,7 @@ pub enum TypeDefinition<'db> {
TypeVar(Definition<'db>),
TypeAlias(Definition<'db>),
NewType(Definition<'db>),
SpecialForm(Definition<'db>),
}
impl TypeDefinition<'_> {
@@ -23,6 +24,7 @@ impl TypeDefinition<'_> {
| Self::Function(definition)
| Self::TypeVar(definition)
| Self::TypeAlias(definition)
| Self::SpecialForm(definition)
| Self::NewType(definition) => {
let module = parsed_module(db, definition.file(db)).load(db);
Some(definition.focus_range(db, &module))
@@ -41,6 +43,7 @@ impl TypeDefinition<'_> {
| Self::Function(definition)
| Self::TypeVar(definition)
| Self::TypeAlias(definition)
| Self::SpecialForm(definition)
| Self::NewType(definition) => {
let module = parsed_module(db, definition.file(db)).load(db);
Some(definition.full_range(db, &module))

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,7 @@ use crate::types::{
ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType,
NormalizedVisitor, SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation,
UnionBuilder, binding_type, todo_type, walk_signature,
UnionBuilder, binding_type, walk_signature,
};
use crate::{Db, FxOrderSet, ModuleName, resolve_module};
@@ -1152,70 +1152,6 @@ fn is_instance_truthiness<'db>(
}
}
/// Return true, if the type passed as `mode` would require us to pick a non-trivial overload of
/// `builtins.open` / `os.fdopen` / `Path.open`.
fn is_mode_with_nontrivial_return_type<'db>(db: &'db dyn Db, mode: Type<'db>) -> bool {
// Return true for any mode that doesn't match typeshed's
// `OpenTextMode` type alias (<https://github.com/python/typeshed/blob/6937a9b193bfc2f0696452d58aad96d7627aa29a/stdlib/_typeshed/__init__.pyi#L220>).
mode.as_string_literal().is_none_or(|mode| {
!matches!(
mode.value(db),
"r+" | "+r"
| "rt+"
| "r+t"
| "+rt"
| "tr+"
| "t+r"
| "+tr"
| "w+"
| "+w"
| "wt+"
| "w+t"
| "+wt"
| "tw+"
| "t+w"
| "+tw"
| "a+"
| "+a"
| "at+"
| "a+t"
| "+at"
| "ta+"
| "t+a"
| "+ta"
| "x+"
| "+x"
| "xt+"
| "x+t"
| "+xt"
| "tx+"
| "t+x"
| "+tx"
| "w"
| "wt"
| "tw"
| "a"
| "at"
| "ta"
| "x"
| "xt"
| "tx"
| "r"
| "rt"
| "tr"
| "U"
| "rU"
| "Ur"
| "rtU"
| "rUt"
| "Urt"
| "trU"
| "tUr"
| "Utr"
)
})
}
fn signature_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
@@ -1268,16 +1204,6 @@ pub enum KnownFunction {
DunderImport,
/// `importlib.import_module`, which returns the submodule.
ImportModule,
/// `builtins.open`
Open,
/// `os.fdopen`
Fdopen,
/// `tempfile.NamedTemporaryFile`
#[strum(serialize = "NamedTemporaryFile")]
NamedTemporaryFile,
/// `typing(_extensions).final`
Final,
/// `typing(_extensions).disjoint_base`
@@ -1376,7 +1302,6 @@ impl KnownFunction {
| Self::HasAttr
| Self::Len
| Self::Repr
| Self::Open
| Self::DunderImport => module.is_builtins(),
Self::AssertType
| Self::AssertNever
@@ -1396,12 +1321,6 @@ impl KnownFunction {
Self::AbstractMethod => {
matches!(module, KnownModule::Abc)
}
Self::Fdopen => {
matches!(module, KnownModule::Os)
}
Self::NamedTemporaryFile => {
matches!(module, KnownModule::Tempfile)
}
Self::Dataclass | Self::Field => {
matches!(module, KnownModule::Dataclasses)
}
@@ -1790,14 +1709,21 @@ impl KnownFunction {
// `Any` can be used in `issubclass()` calls but not `isinstance()` calls
Type::SpecialForm(SpecialFormType::Any)
if function == KnownFunction::IsSubclass => {}
Type::KnownInstance(KnownInstanceType::UnionType(union)) => {
for element in union.elements(db) {
find_invalid_elements(
db,
function,
*element,
invalid_elements,
);
Type::KnownInstance(KnownInstanceType::UnionType(instance)) => {
match instance.value_expression_types(db) {
Ok(value_expression_types) => {
for element in value_expression_types {
find_invalid_elements(
db,
function,
element,
invalid_elements,
);
}
}
Err(_) => {
invalid_elements.push(ty);
}
}
}
_ => invalid_elements.push(ty),
@@ -1882,38 +1808,6 @@ impl KnownFunction {
overload.set_return_type(Type::module_literal(db, file, module));
}
KnownFunction::Open => {
// TODO: Temporary special-casing for `builtins.open` to avoid an excessive number of
// false positives in lieu of proper support for PEP-613 type aliases.
if let [_, Some(mode), ..] = parameter_types
&& is_mode_with_nontrivial_return_type(db, *mode)
{
overload.set_return_type(todo_type!("`builtins.open` return type"));
}
}
KnownFunction::Fdopen => {
// TODO: Temporary special-casing for `os.fdopen` to avoid an excessive number of
// false positives in lieu of proper support for PEP-613 type aliases.
if let [_, Some(mode), ..] = parameter_types
&& is_mode_with_nontrivial_return_type(db, *mode)
{
overload.set_return_type(todo_type!("`os.fdopen` return type"));
}
}
KnownFunction::NamedTemporaryFile => {
// TODO: Temporary special-casing for `tempfile.NamedTemporaryFile` to avoid an excessive number of
// false positives in lieu of proper support for PEP-613 type aliases.
if let [Some(mode), ..] = parameter_types
&& is_mode_with_nontrivial_return_type(db, *mode)
{
overload
.set_return_type(todo_type!("`tempfile.NamedTemporaryFile` return type"));
}
}
_ => {}
}
}
@@ -1940,15 +1834,10 @@ pub(crate) mod tests {
| KnownFunction::IsInstance
| KnownFunction::HasAttr
| KnownFunction::IsSubclass
| KnownFunction::Open
| KnownFunction::DunderImport => KnownModule::Builtins,
KnownFunction::AbstractMethod => KnownModule::Abc,
KnownFunction::Fdopen => KnownModule::Os,
KnownFunction::NamedTemporaryFile => KnownModule::Tempfile,
KnownFunction::Dataclass | KnownFunction::Field => KnownModule::Dataclasses,
KnownFunction::GetattrStatic => KnownModule::Inspect,

View File

@@ -15,10 +15,10 @@ use crate::types::generics::Specialization;
use crate::types::signatures::Signature;
use crate::types::{CallDunderError, UnionType};
use crate::types::{
ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type, TypeContext,
ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext,
TypeVarBoundOrConstraints, class::CodeGeneratorKind,
};
use crate::{Db, HasType, NameKind, SemanticModel};
use crate::{Db, DisplaySettings, HasType, NameKind, SemanticModel};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_python_ast::name::Name;
@@ -299,9 +299,10 @@ impl<'db> AllMembers<'db> {
Type::KnownInstance(
KnownInstanceType::TypeVar(_)
| KnownInstanceType::TypeAliasType(_)
| KnownInstanceType::UnionType(_),
| KnownInstanceType::UnionType(_)
| KnownInstanceType::Literal(_)
| KnownInstanceType::Annotated(_),
) => continue,
Type::Dynamic(DynamicType::TodoTypeAlias) => continue,
_ => {}
}
}
@@ -1022,6 +1023,65 @@ pub fn call_signature_details<'db>(
}
}
/// Given a call expression that has overloads, and whose overload is resolved to a
/// single option by its arguments, return the type of the Signature.
///
/// This is only used for simplifying complex call types, so if we ever detect that
/// the given callable type *is* simple, or that our answer *won't* be simple, we
/// bail at out and return None, so that the original type can be used.
///
/// We do this because `Type::Signature` intentionally loses a lot of context, and
/// so it has a "worse" display than say `Type::FunctionLiteral` or `Type::BoundMethod`,
/// which this analysis would naturally wipe away. The contexts this function
/// succeeds in are those where we would print a complicated/ugly type anyway.
pub fn call_type_simplified_by_overloads<'db>(
db: &'db dyn Db,
model: &SemanticModel<'db>,
call_expr: &ast::ExprCall,
) -> Option<String> {
let func_type = call_expr.func.inferred_type(model);
// Use into_callable to handle all the complex type conversions
let callable_type = func_type.try_upcast_to_callable(db)?;
let bindings = callable_type.bindings(db);
// If the callable is trivial this analysis is useless, bail out
if let Some(binding) = bindings.single_element()
&& binding.overloads().len() < 2
{
return None;
}
// Hand the overload resolution system as much type info as we have
let args = CallArguments::from_arguments_typed(&call_expr.arguments, |_, splatted_value| {
splatted_value.inferred_type(model)
});
// Try to resolve overloads with the arguments/types we have
let mut resolved = bindings
.match_parameters(db, &args)
.check_types(db, &args, TypeContext::default(), &[])
// Only use the Ok
.iter()
.flatten()
.flat_map(|binding| {
binding.matching_overloads().map(|(_, overload)| {
overload
.signature
.display_with(db, DisplaySettings::default().multiline())
.to_string()
})
})
.collect::<Vec<_>>();
// If at the end of this we still got multiple signatures (or no signatures), give up
if resolved.len() != 1 {
return None;
}
resolved.pop()
}
/// Returns the definitions of the binary operation along with its callable type.
pub fn definitions_for_bin_op<'db>(
db: &'db dyn Db,

View File

@@ -377,6 +377,11 @@ impl<'db> TypeContext<'db> {
annotation: self.annotation.map(f),
}
}
pub(crate) fn is_typealias(&self) -> bool {
self.annotation
.is_some_and(|ty| ty.is_typealias_special_form())
}
}
/// Returns the statically-known truthiness of a given expression.

View File

@@ -102,13 +102,13 @@ use crate::types::typed_dict::{
use crate::types::visitor::any_over_type;
use crate::types::{
CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, IntersectionType,
KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass,
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType,
TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext,
TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
binding_type, todo_type,
UnionTypeInstance, binding_type, todo_type,
};
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
use crate::unpack::{EvaluationMode, UnpackPosition};
@@ -5407,7 +5407,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let target = assignment.target(self.module());
let value = assignment.value(self.module());
let mut declared = self.infer_annotation_expression(
let mut declared = self.infer_annotation_expression_allow_pep_613(
annotation,
DeferredExpressionState::from(self.defer_annotations()),
);
@@ -5459,6 +5459,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
declared.inner = Type::BooleanLiteral(true);
}
// Check if this is a PEP 613 `TypeAlias`. (This must come below the SpecialForm handling
// immediately below, since that can overwrite the type to be `TypeAlias`.)
let is_pep_613_type_alias = declared.inner_type().is_typealias_special_form();
// Handle various singletons.
if let Some(name_expr) = target.as_name_expr() {
if let Some(special_form) =
@@ -5504,20 +5508,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
// We defer the r.h.s. of PEP-613 `TypeAlias` assignments in stub files.
let declared_type = declared.inner_type();
let previous_deferred_state = self.deferred_state;
if matches!(
declared_type,
Type::SpecialForm(SpecialFormType::TypeAlias)
| Type::Dynamic(DynamicType::TodoTypeAlias)
) && self.in_stub()
{
if is_pep_613_type_alias && self.in_stub() {
self.deferred_state = DeferredExpressionState::Deferred;
}
let inferred_ty = self
.infer_maybe_standalone_expression(value, TypeContext::new(Some(declared_type)));
let inferred_ty = self.infer_maybe_standalone_expression(
value,
TypeContext::new(Some(declared.inner_type())),
);
self.deferred_state = previous_deferred_state;
@@ -5534,17 +5534,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
inferred_ty
};
self.add_declaration_with_binding(
target.into(),
definition,
&DeclaredAndInferredType::MightBeDifferent {
declared_ty: declared,
inferred_ty,
},
);
if is_pep_613_type_alias {
self.add_declaration_with_binding(
target.into(),
definition,
&DeclaredAndInferredType::AreTheSame(TypeAndQualifiers::declared(inferred_ty)),
);
} else {
self.add_declaration_with_binding(
target.into(),
definition,
&DeclaredAndInferredType::MightBeDifferent {
declared_ty: declared,
inferred_ty,
},
);
}
self.store_expression_type(target, inferred_ty);
} else {
if is_pep_613_type_alias {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, annotation) {
builder.into_diagnostic(
"`TypeAlias` must be assigned a value in annotated assignments",
);
}
declared.inner = Type::unknown();
}
if self.in_stub() {
self.add_declaration_with_binding(
target.into(),
@@ -5783,10 +5799,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
return;
};
let mut diagnostic = builder.into_diagnostic(format_args!(
"Cannot resolve imported module `{}{}`",
".".repeat(level as usize),
module.unwrap_or_default()
"Cannot resolve imported module `{}`",
format_import_from_module(level, module)
));
if level == 0 {
if let Some(module_name) = module.and_then(ModuleName::new) {
let program = Program::get(self.db());
@@ -5815,39 +5831,63 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
}
// Add search paths information to the diagnostic
// Use the same search paths function that is used in actual module resolution
let verbose = self.db().verbose();
let search_paths = search_paths(self.db(), ModuleResolveMode::StubsAllowed);
diagnostic.info(format_args!(
"Searched in the following paths during module resolution:"
));
let mut search_paths = search_paths.enumerate();
while let Some((index, path)) = search_paths.next() {
if index > 4 && !verbose {
let more = search_paths.count() + 1;
diagnostic.info(format_args!(
" ... and {more} more paths. Run with `-v` to see all paths."
));
break;
}
diagnostic.info(format_args!(
" {}. {} ({})",
index + 1,
path,
path.describe_kind()
} else {
if let Some(better_level) = (0..level).rev().find(|reduced_level| {
let Ok(module_name) = ModuleName::from_identifier_parts(
self.db(),
self.file(),
module,
*reduced_level,
) else {
return false;
};
resolve_module(self.db(), &module_name).is_some()
}) {
diagnostic
.help("The module can be resolved if the number of leading dots is reduced");
diagnostic.help(format_args!(
"Did you mean `{}`?",
format_import_from_module(better_level, module)
));
diagnostic.set_concise_message(format_args!(
"Cannot resolve imported module `{}` - did you mean `{}`?",
format_import_from_module(level, module),
format_import_from_module(better_level, module)
));
}
diagnostic.info(
"make sure your Python environment is properly configured: \
https://docs.astral.sh/ty/modules/#python-environment",
);
}
// Add search paths information to the diagnostic
// Use the same search paths function that is used in actual module resolution
let verbose = self.db().verbose();
let search_paths = search_paths(self.db(), ModuleResolveMode::StubsAllowed);
diagnostic.info(format_args!(
"Searched in the following paths during module resolution:"
));
let mut search_paths = search_paths.enumerate();
while let Some((index, path)) = search_paths.next() {
if index > 4 && !verbose {
let more = search_paths.count() + 1;
diagnostic.info(format_args!(
" ... and {more} more paths. Run with `-v` to see all paths."
));
break;
}
diagnostic.info(format_args!(
" {}. {} ({})",
index + 1,
path,
path.describe_kind()
));
}
diagnostic.info(
"make sure your Python environment is properly configured: \
https://docs.astral.sh/ty/modules/#python-environment",
);
}
fn infer_import_definition(
@@ -6022,7 +6062,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
Err(ModuleNameResolutionError::UnknownCurrentModule) => {
tracing::debug!(
"Relative module resolution `{}` failed; could not resolve file `{}` to a module",
"Relative module resolution `{}` failed: could not resolve file `{}` to a module \
(try adjusting configured search paths?)",
format_import_from_module(*level, module),
self.file().path(self.db())
);
@@ -6894,7 +6935,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}) => Type::none(self.db()),
ast::Expr::NumberLiteral(literal) => self.infer_number_literal_expression(literal),
ast::Expr::BooleanLiteral(literal) => self.infer_boolean_literal_expression(literal),
ast::Expr::StringLiteral(literal) => self.infer_string_literal_expression(literal),
ast::Expr::StringLiteral(literal) => self.infer_string_literal_expression(literal, tcx),
ast::Expr::BytesLiteral(bytes_literal) => {
self.infer_bytes_literal_expression(bytes_literal)
}
@@ -6916,7 +6957,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::Expr::Name(name) => self.infer_name_expression(name),
ast::Expr::Attribute(attribute) => self.infer_attribute_expression(attribute),
ast::Expr::UnaryOp(unary_op) => self.infer_unary_expression(unary_op),
ast::Expr::BinOp(binary) => self.infer_binary_expression(binary),
ast::Expr::BinOp(binary) => self.infer_binary_expression(binary, tcx),
ast::Expr::BoolOp(bool_op) => self.infer_boolean_expression(bool_op),
ast::Expr::Compare(compare) => self.infer_compare_expression(compare),
ast::Expr::Subscript(subscript) => self.infer_subscript_expression(subscript),
@@ -7011,7 +7052,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::BooleanLiteral(*value)
}
fn infer_string_literal_expression(&mut self, literal: &ast::ExprStringLiteral) -> Type<'db> {
fn infer_string_literal_expression(
&mut self,
literal: &ast::ExprStringLiteral,
tcx: TypeContext<'db>,
) -> Type<'db> {
if tcx.is_typealias() {
let aliased_type = self.infer_string_type_expression(literal);
return Type::KnownInstance(KnownInstanceType::LiteralStringAlias(InternedType::new(
self.db(),
aliased_type,
)));
}
if literal.value.len() <= Self::MAX_STRING_LITERAL_SIZE {
Type::string_literal(self.db(), literal.value.to_str())
} else {
@@ -8043,6 +8095,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// are handled by the default constructor-call logic (we synthesize a `__new__` method for them
// in `ClassType::own_class_member()`).
class.is_known(self.db(), KnownClass::Tuple) && !class.is_generic()
) || CodeGeneratorKind::TypedDict.matches(
self.db(),
class.class_literal(self.db()).0,
class.class_literal(self.db()).1,
);
// temporary special-casing for all subclasses of `enum.Enum`
@@ -9208,7 +9264,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
fn infer_binary_expression(&mut self, binary: &ast::ExprBinOp) -> Type<'db> {
fn infer_binary_expression(
&mut self,
binary: &ast::ExprBinOp,
tcx: TypeContext<'db>,
) -> Type<'db> {
if tcx.is_typealias() {
return self.infer_pep_604_union_type_alias(binary, tcx);
}
let ast::ExprBinOp {
left,
op,
@@ -9246,6 +9310,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
})
}
fn infer_pep_604_union_type_alias(
&mut self,
node: &ast::ExprBinOp,
tcx: TypeContext<'db>,
) -> Type<'db> {
let ast::ExprBinOp {
left,
op,
right,
range: _,
node_index: _,
} = node;
if *op != ast::Operator::BitOr {
// TODO diagnostic?
return Type::unknown();
}
let left_ty = self.infer_expression(left, tcx);
let right_ty = self.infer_expression(right, tcx);
// TODO this is overly aggressive; if the operands' `__or__` does not actually return a
// `UnionType` at runtime, we should ideally not infer one here. But this is unlikely to be
// a problem in practice: it would require someone having an explicitly annotated
// `TypeAlias`, which uses `X | Y` syntax, where the returned type is not actually a union.
// And attempting to enforce this more tightly showed a lot of potential false positives in
// the ecosystem.
if left_ty.is_equivalent_to(self.db(), right_ty) {
left_ty
} else {
UnionTypeInstance::from_value_expression_types(
self.db(),
[left_ty, right_ty],
self.scope(),
self.typevar_binding_context,
)
}
}
fn infer_binary_expression_type(
&mut self,
node: AnyNodeRef<'_>,
@@ -9321,20 +9424,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
(unknown @ Type::Dynamic(DynamicType::Unknown), _, _)
| (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown),
(
todo @ Type::Dynamic(
DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoTypeAlias,
),
_,
_,
)
| (
_,
todo @ Type::Dynamic(
DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoTypeAlias,
),
_,
) => Some(todo),
(todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack), _, _)
| (_, todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack), _) => {
Some(todo)
}
(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never),
@@ -9545,13 +9638,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if left_ty.is_equivalent_to(self.db(), right_ty) {
Some(left_ty)
} else {
Some(Type::KnownInstance(KnownInstanceType::UnionType(
InternedTypes::from_elements(
self.db(),
[left_ty, right_ty],
InferredAs::ValueExpression,
),
)))
Some(UnionTypeInstance::from_value_expression_types(
self.db(),
[left_ty, right_ty],
self.scope(),
self.typevar_binding_context,
))
}
}
(
@@ -9574,13 +9666,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
) if pep_604_unions_allowed()
&& instance.has_known_class(self.db(), KnownClass::NoneType) =>
{
Some(Type::KnownInstance(KnownInstanceType::UnionType(
InternedTypes::from_elements(
self.db(),
[left_ty, right_ty],
InferredAs::ValueExpression,
),
)))
Some(UnionTypeInstance::from_value_expression_types(
self.db(),
[left_ty, right_ty],
self.scope(),
self.typevar_binding_context,
))
}
// We avoid calling `type.__(r)or__`, as typeshed annotates these methods as
@@ -10801,13 +10892,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
return ty;
}
return Type::KnownInstance(KnownInstanceType::UnionType(
InternedTypes::from_elements(
self.db(),
[ty, Type::none(self.db())],
InferredAs::ValueExpression,
),
));
return UnionTypeInstance::from_value_expression_types(
self.db(),
[ty, Type::none(self.db())],
self.scope(),
self.typevar_binding_context,
);
}
Type::SpecialForm(SpecialFormType::Union) => {
let db = self.db();
@@ -10822,7 +10912,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let is_empty = elements.peek().is_none();
let union_type = Type::KnownInstance(KnownInstanceType::UnionType(
InternedTypes::from_elements(db, elements, InferredAs::TypeExpression),
UnionTypeInstance::new(
db,
None,
Ok(UnionType::from_elements(db, elements)),
),
));
if is_empty {
@@ -10871,7 +10965,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"`typing.{}` requires exactly one argument",
special_form.repr()
special_form.name()
));
}
Type::unknown()
@@ -10906,7 +11000,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
{
builder.into_diagnostic(format_args!(
"`typing.{}` requires exactly two arguments, got {}",
special_form.repr(),
special_form.name(),
arguments.len()
));
}
@@ -10930,7 +11024,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"`typing.{}` requires exactly two arguments, got 1",
special_form.repr()
special_form.name()
));
}

View File

@@ -10,6 +10,12 @@ use crate::types::{
KnownClass, SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifiers, todo_type,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum PEP613Policy {
Allowed,
Disallowed,
}
/// Annotation expressions.
impl<'db> TypeInferenceBuilder<'db, '_> {
/// Infer the type of an annotation expression with the given [`DeferredExpressionState`].
@@ -18,21 +24,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
annotation: &ast::Expr,
deferred_state: DeferredExpressionState,
) -> TypeAndQualifiers<'db> {
// `DeferredExpressionState::InStringAnnotation` takes precedence over other deferred states.
// However, if it's not a stringified annotation, we must still ensure that annotation expressions
// are always deferred in stub files.
let state = if deferred_state.in_string_annotation() {
deferred_state
} else if self.in_stub() {
DeferredExpressionState::Deferred
} else {
deferred_state
};
self.infer_annotation_expression_inner(annotation, deferred_state, PEP613Policy::Disallowed)
}
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state);
let annotation_ty = self.infer_annotation_expression_impl(annotation);
self.deferred_state = previous_deferred_state;
annotation_ty
/// Infer the type of an annotation expression with the given [`DeferredExpressionState`],
/// allowing a PEP 613 `typing.TypeAlias` annotation.
pub(super) fn infer_annotation_expression_allow_pep_613(
&mut self,
annotation: &ast::Expr,
deferred_state: DeferredExpressionState,
) -> TypeAndQualifiers<'db> {
self.infer_annotation_expression_inner(annotation, deferred_state, PEP613Policy::Allowed)
}
/// Similar to [`infer_annotation_expression`], but accepts an optional annotation expression
@@ -47,17 +49,42 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
annotation.map(|expr| self.infer_annotation_expression(expr, deferred_state))
}
fn infer_annotation_expression_inner(
&mut self,
annotation: &ast::Expr,
deferred_state: DeferredExpressionState,
pep_613_policy: PEP613Policy,
) -> TypeAndQualifiers<'db> {
// `DeferredExpressionState::InStringAnnotation` takes precedence over other deferred states.
// However, if it's not a stringified annotation, we must still ensure that annotation expressions
// are always deferred in stub files.
let state = if deferred_state.in_string_annotation() {
deferred_state
} else if self.in_stub() {
DeferredExpressionState::Deferred
} else {
deferred_state
};
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state);
let annotation_ty = self.infer_annotation_expression_impl(annotation, pep_613_policy);
self.deferred_state = previous_deferred_state;
annotation_ty
}
/// Implementation of [`infer_annotation_expression`].
///
/// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression
fn infer_annotation_expression_impl(
&mut self,
annotation: &ast::Expr,
pep_613_policy: PEP613Policy,
) -> TypeAndQualifiers<'db> {
fn infer_name_or_attribute<'db>(
ty: Type<'db>,
annotation: &ast::Expr,
builder: &TypeInferenceBuilder<'db, '_>,
pep_613_policy: PEP613Policy,
) -> TypeAndQualifiers<'db> {
match ty {
Type::SpecialForm(SpecialFormType::ClassVar) => TypeAndQualifiers::new(
@@ -85,6 +112,24 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
TypeOrigin::Declared,
TypeQualifiers::READ_ONLY,
),
Type::SpecialForm(SpecialFormType::TypeAlias)
if pep_613_policy == PEP613Policy::Allowed =>
{
TypeAndQualifiers::declared(ty)
}
// Conditional import of `typing.TypeAlias` or `typing_extensions.TypeAlias` on a
// Python version where the former doesn't exist.
Type::Union(union)
if pep_613_policy == PEP613Policy::Allowed
&& union.elements(builder.db()).iter().all(|ty| {
matches!(
ty,
Type::SpecialForm(SpecialFormType::TypeAlias) | Type::Dynamic(_)
)
}) =>
{
TypeAndQualifiers::declared(Type::SpecialForm(SpecialFormType::TypeAlias))
}
Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => {
if let Some(builder) =
builder.context.report_lint(&INVALID_TYPE_FORM, annotation)
@@ -148,6 +193,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_attribute_expression(attribute),
annotation,
self,
pep_613_policy,
),
ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()),
ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared(
@@ -156,9 +202,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
},
ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => {
infer_name_or_attribute(self.infer_name_expression(name), annotation, self)
}
ast::ExprContext::Load => infer_name_or_attribute(
self.infer_name_expression(name),
annotation,
self,
pep_613_policy,
),
ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()),
ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared(
todo_type!("Name expression annotation in Store/Del context"),
@@ -188,8 +237,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_expression(element, TypeContext::default());
}
let inner_annotation_ty =
self.infer_annotation_expression_impl(inner_annotation);
let inner_annotation_ty = self.infer_annotation_expression_impl(
inner_annotation,
PEP613Policy::Disallowed,
);
self.store_expression_type(slice, inner_annotation_ty.inner_type());
inner_annotation_ty
@@ -202,7 +253,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
} else {
report_invalid_arguments_to_annotated(&self.context, subscript);
self.infer_annotation_expression_impl(slice)
self.infer_annotation_expression_impl(slice, PEP613Policy::Disallowed)
}
}
Type::SpecialForm(
@@ -219,8 +270,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
};
let num_arguments = arguments.len();
let type_and_qualifiers = if num_arguments == 1 {
let mut type_and_qualifiers =
self.infer_annotation_expression_impl(slice);
let mut type_and_qualifiers = self
.infer_annotation_expression_impl(slice, PEP613Policy::Disallowed);
match type_qualifier {
SpecialFormType::ClassVar => {
@@ -243,7 +294,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
type_and_qualifiers
} else {
for element in arguments {
self.infer_annotation_expression_impl(element);
self.infer_annotation_expression_impl(
element,
PEP613Policy::Disallowed,
);
}
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
@@ -268,13 +322,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
};
let num_arguments = arguments.len();
let type_and_qualifiers = if num_arguments == 1 {
let mut type_and_qualifiers =
self.infer_annotation_expression_impl(slice);
let mut type_and_qualifiers = self
.infer_annotation_expression_impl(slice, PEP613Policy::Disallowed);
type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR);
type_and_qualifiers
} else {
for element in arguments {
self.infer_annotation_expression_impl(element);
self.infer_annotation_expression_impl(
element,
PEP613Policy::Disallowed,
);
}
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, subscript)

View File

@@ -147,7 +147,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
// anything else is an invalid annotation:
op => {
self.infer_binary_expression(binary);
self.infer_binary_expression(binary, TypeContext::default());
if let Some(mut diag) = self.report_invalid_type_expression(
expression,
format_args!(
@@ -518,7 +518,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
/// Infer the type of a string type expression.
fn infer_string_type_expression(&mut self, string: &ast::ExprStringLiteral) -> Type<'db> {
pub(super) fn infer_string_type_expression(
&mut self,
string: &ast::ExprStringLiteral,
) -> Type<'db> {
match parse_string_annotation(&self.context, string) {
Some(parsed) => {
// String annotations are always evaluated in the deferred context.
@@ -843,6 +846,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_type_expression(slice);
todo_type!("Generic manual PEP-695 type alias")
}
KnownInstanceType::LiteralStringAlias(_) => {
self.infer_type_expression(slice);
todo_type!("Generic stringified PEP-613 type alias")
}
KnownInstanceType::UnionType(_) => {
self.infer_type_expression(slice);
todo_type!("Generic specialization of types.UnionType")
@@ -1318,10 +1325,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_type_expression(arguments_slice);
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
let diag = builder.into_diagnostic(format_args!(
"Special form `{}` expected exactly one type parameter",
special_form.repr()
));
let diag = builder.into_diagnostic(
"Special form `typing.TypeIs` expected exactly one type parameter",
);
diagnostic::add_type_expression_reference_link(diag);
}

View File

@@ -212,10 +212,10 @@ impl ClassInfoConstraintFunction {
)
}),
Type::KnownInstance(KnownInstanceType::UnionType(elements)) => {
Type::KnownInstance(KnownInstanceType::UnionType(instance)) => {
UnionType::try_from_elements(
db,
elements.elements(db).iter().map(|element| {
instance.value_expression_types(db).ok()?.map(|element| {
// A special case is made for `None` at runtime
// (it's implicitly converted to `NoneType` in `int | None`)
// which means that `isinstance(x, int | None)` works even though
@@ -223,7 +223,7 @@ impl ClassInfoConstraintFunction {
if element.is_none(db) {
self.generate_constraint(db, KnownClass::NoneType.to_class_literal(db))
} else {
self.generate_constraint(db, *element)
self.generate_constraint(db, element)
}
}),
)
@@ -874,8 +874,6 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
let callable_ty = inference.expression_type(&*expr_call.func);
// TODO: add support for PEP 604 union types on the right hand side of `isinstance`
// and `issubclass`, for example `isinstance(x, str | (int | float))`.
match callable_ty {
Type::FunctionLiteral(function_type)
if matches!(

View File

@@ -4,6 +4,10 @@
use super::{ClassType, Type, class::KnownClass};
use crate::db::Db;
use crate::module_resolver::{KnownModule, file_to_module};
use crate::resolve_module;
use crate::semantic_index::place::ScopedPlaceId;
use crate::semantic_index::{FileScopeId, place_table, use_def_map};
use crate::types::TypeDefinition;
use ruff_db::files::File;
use std::str::FromStr;
@@ -435,59 +439,132 @@ impl SpecialFormType {
}
}
/// Return the repr of the symbol at runtime
pub(super) const fn repr(self) -> &'static str {
/// Return the name of the symbol at runtime
pub(super) const fn name(self) -> &'static str {
match self {
SpecialFormType::Any => "typing.Any",
SpecialFormType::Annotated => "typing.Annotated",
SpecialFormType::Literal => "typing.Literal",
SpecialFormType::LiteralString => "typing.LiteralString",
SpecialFormType::Optional => "typing.Optional",
SpecialFormType::Union => "typing.Union",
SpecialFormType::NoReturn => "typing.NoReturn",
SpecialFormType::Never => "typing.Never",
SpecialFormType::Tuple => "typing.Tuple",
SpecialFormType::Type => "typing.Type",
SpecialFormType::TypingSelf => "typing.Self",
SpecialFormType::Final => "typing.Final",
SpecialFormType::ClassVar => "typing.ClassVar",
SpecialFormType::Callable => "typing.Callable",
SpecialFormType::Concatenate => "typing.Concatenate",
SpecialFormType::Unpack => "typing.Unpack",
SpecialFormType::Required => "typing.Required",
SpecialFormType::NotRequired => "typing.NotRequired",
SpecialFormType::TypeAlias => "typing.TypeAlias",
SpecialFormType::TypeGuard => "typing.TypeGuard",
SpecialFormType::TypedDict => "typing.TypedDict",
SpecialFormType::TypeIs => "typing.TypeIs",
SpecialFormType::List => "typing.List",
SpecialFormType::Dict => "typing.Dict",
SpecialFormType::DefaultDict => "typing.DefaultDict",
SpecialFormType::Set => "typing.Set",
SpecialFormType::FrozenSet => "typing.FrozenSet",
SpecialFormType::Counter => "typing.Counter",
SpecialFormType::Deque => "typing.Deque",
SpecialFormType::ChainMap => "typing.ChainMap",
SpecialFormType::OrderedDict => "typing.OrderedDict",
SpecialFormType::ReadOnly => "typing.ReadOnly",
SpecialFormType::Unknown => "ty_extensions.Unknown",
SpecialFormType::AlwaysTruthy => "ty_extensions.AlwaysTruthy",
SpecialFormType::AlwaysFalsy => "ty_extensions.AlwaysFalsy",
SpecialFormType::Not => "ty_extensions.Not",
SpecialFormType::Intersection => "ty_extensions.Intersection",
SpecialFormType::TypeOf => "ty_extensions.TypeOf",
SpecialFormType::CallableTypeOf => "ty_extensions.CallableTypeOf",
SpecialFormType::Top => "ty_extensions.Top",
SpecialFormType::Bottom => "ty_extensions.Bottom",
SpecialFormType::Protocol => "typing.Protocol",
SpecialFormType::Generic => "typing.Generic",
SpecialFormType::NamedTuple => "typing.NamedTuple",
SpecialFormType::Any => "Any",
SpecialFormType::Annotated => "Annotated",
SpecialFormType::Literal => "Literal",
SpecialFormType::LiteralString => "LiteralString",
SpecialFormType::Optional => "Optional",
SpecialFormType::Union => "Union",
SpecialFormType::NoReturn => "NoReturn",
SpecialFormType::Never => "Never",
SpecialFormType::Tuple => "Tuple",
SpecialFormType::Type => "Type",
SpecialFormType::TypingSelf => "Self",
SpecialFormType::Final => "Final",
SpecialFormType::ClassVar => "ClassVar",
SpecialFormType::Callable => "Callable",
SpecialFormType::Concatenate => "Concatenate",
SpecialFormType::Unpack => "Unpack",
SpecialFormType::Required => "Required",
SpecialFormType::NotRequired => "NotRequired",
SpecialFormType::TypeAlias => "TypeAlias",
SpecialFormType::TypeGuard => "TypeGuard",
SpecialFormType::TypedDict => "TypedDict",
SpecialFormType::TypeIs => "TypeIs",
SpecialFormType::List => "List",
SpecialFormType::Dict => "Dict",
SpecialFormType::DefaultDict => "DefaultDict",
SpecialFormType::Set => "Set",
SpecialFormType::FrozenSet => "FrozenSet",
SpecialFormType::Counter => "Counter",
SpecialFormType::Deque => "Deque",
SpecialFormType::ChainMap => "ChainMap",
SpecialFormType::OrderedDict => "OrderedDict",
SpecialFormType::ReadOnly => "ReadOnly",
SpecialFormType::Unknown => "Unknown",
SpecialFormType::AlwaysTruthy => "AlwaysTruthy",
SpecialFormType::AlwaysFalsy => "AlwaysFalsy",
SpecialFormType::Not => "Not",
SpecialFormType::Intersection => "Intersection",
SpecialFormType::TypeOf => "TypeOf",
SpecialFormType::CallableTypeOf => "CallableTypeOf",
SpecialFormType::Top => "Top",
SpecialFormType::Bottom => "Bottom",
SpecialFormType::Protocol => "Protocol",
SpecialFormType::Generic => "Generic",
SpecialFormType::NamedTuple => "NamedTuple",
}
}
/// Return the module(s) in which this special form could be defined
fn definition_modules(self) -> &'static [KnownModule] {
match self {
SpecialFormType::Any
| SpecialFormType::Annotated
| SpecialFormType::Literal
| SpecialFormType::LiteralString
| SpecialFormType::Optional
| SpecialFormType::Union
| SpecialFormType::NoReturn
| SpecialFormType::Never
| SpecialFormType::Tuple
| SpecialFormType::Type
| SpecialFormType::TypingSelf
| SpecialFormType::Final
| SpecialFormType::ClassVar
| SpecialFormType::Callable
| SpecialFormType::Concatenate
| SpecialFormType::Unpack
| SpecialFormType::Required
| SpecialFormType::NotRequired
| SpecialFormType::TypeAlias
| SpecialFormType::TypeGuard
| SpecialFormType::TypedDict
| SpecialFormType::TypeIs
| SpecialFormType::ReadOnly
| SpecialFormType::Protocol
| SpecialFormType::Generic
| SpecialFormType::NamedTuple
| SpecialFormType::List
| SpecialFormType::Dict
| SpecialFormType::DefaultDict
| SpecialFormType::Set
| SpecialFormType::FrozenSet
| SpecialFormType::Counter
| SpecialFormType::Deque
| SpecialFormType::ChainMap
| SpecialFormType::OrderedDict => &[KnownModule::Typing, KnownModule::TypingExtensions],
SpecialFormType::Unknown
| SpecialFormType::AlwaysTruthy
| SpecialFormType::AlwaysFalsy
| SpecialFormType::Not
| SpecialFormType::Intersection
| SpecialFormType::TypeOf
| SpecialFormType::CallableTypeOf
| SpecialFormType::Top
| SpecialFormType::Bottom => &[KnownModule::TyExtensions],
}
}
pub(super) fn definition(self, db: &dyn Db) -> Option<TypeDefinition<'_>> {
self.definition_modules()
.iter()
.find_map(|module| {
let file = resolve_module(db, &module.name())?.file(db)?;
let scope = FileScopeId::global().to_scope_id(db, file);
let symbol_id = place_table(db, scope).symbol_id(self.name())?;
use_def_map(db, scope)
.end_of_scope_bindings(ScopedPlaceId::Symbol(symbol_id))
.next()?
.binding
.definition()
})
.map(TypeDefinition::SpecialForm)
}
}
impl std::fmt::Display for SpecialFormType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.repr())
write!(
f,
"{}.{}",
self.definition_modules()[0].as_str(),
self.name()
)
}
}

View File

@@ -269,9 +269,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
(DynamicType::TodoUnpack, _) => Ordering::Less,
(_, DynamicType::TodoUnpack) => Ordering::Greater,
(DynamicType::TodoTypeAlias, _) => Ordering::Less,
(_, DynamicType::TodoTypeAlias) => Ordering::Greater,
(DynamicType::Divergent(left), DynamicType::Divergent(right)) => {
left.scope.cmp(&right.scope)
}

View File

@@ -12,7 +12,9 @@ use super::diagnostic::{
report_missing_typed_dict_key,
};
use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor};
use crate::types::TypeContext;
use crate::types::constraints::ConstraintSet;
use crate::types::generics::InferableTypeVars;
use crate::types::{HasRelationToVisitor, IsDisjointVisitor, TypeContext, TypeRelation};
use crate::{Db, FxIndexMap};
use ordermap::OrderSet;
@@ -76,6 +78,174 @@ impl<'db> TypedDictType<'db> {
),
}
}
// Subtyping between `TypedDict`s follows the algorithm described at:
// https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types
pub(super) fn has_relation_to_impl(
self,
db: &'db dyn Db,
target: TypedDictType<'db>,
inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> {
// First do a quick nominal check that (if it succeeds) means that we can avoid
// materializing the full `TypedDict` schema for either `self` or `target`.
// This should be cheaper in many cases, and also helps us avoid some cycles.
if self
.defining_class
.is_subclass_of(db, target.defining_class)
{
return ConstraintSet::from(true);
}
let self_items = self.items(db);
let target_items = target.items(db);
// Many rules violations short-circuit with "never", but asking whether one field is
// [relation] to/of another can produce more complicated constraints, and we collect those.
let mut constraints = ConstraintSet::from(true);
for (target_item_name, target_item_field) in target_items {
let field_constraints = if target_item_field.is_required() {
// required target fields
let Some(self_item_field) = self_items.get(target_item_name) else {
// Self is missing a required field.
return ConstraintSet::from(false);
};
if !self_item_field.is_required() {
// A required field is not required in self.
return ConstraintSet::from(false);
}
if target_item_field.is_read_only() {
// For `ReadOnly[]` fields in the target, the corresponding fields in
// self need to have the same assignability/subtyping/etc relation
// individually that we're looking for overall between the
// `TypedDict`s.
self_item_field.declared_ty.has_relation_to_impl(
db,
target_item_field.declared_ty,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
} else {
if self_item_field.is_read_only() {
// A read-only field can't be assigned to a mutable target.
return ConstraintSet::from(false);
}
// For mutable fields in the target, the relation needs to apply both
// ways, or else mutating the target could violate the structural
// invariants of self. For fully-static types, this is "equivalence".
// For gradual types, it depends on the relation, but mutual
// assignability is "consistency".
self_item_field
.declared_ty
.has_relation_to_impl(
db,
target_item_field.declared_ty,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
.and(db, || {
target_item_field.declared_ty.has_relation_to_impl(
db,
self_item_field.declared_ty,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}
} else {
// `NotRequired[]` target fields
if target_item_field.is_read_only() {
// As above, for `NotRequired[]` + `ReadOnly[]` fields in the target. It's
// tempting to refactor things and unify some of these calls to
// `has_relation_to_impl`, but this branch will get more complicated when we
// add support for `closed` and `extra_items` (which is why the rules in the
// spec are structured like they are), and following the structure of the spec
// makes it easier to check the logic here.
if let Some(self_item_field) = self_items.get(target_item_name) {
self_item_field.declared_ty.has_relation_to_impl(
db,
target_item_field.declared_ty,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
} else {
// Self is missing this not-required, read-only item. However, since all
// `TypedDict`s by default are allowed to have "extra items" of any type
// (until we support `closed` and explicit `extra_items`), this key could
// actually turn out to have a value. To make sure this is type-safe, the
// not-required field in the target needs to be assignable from `object`.
// TODO: `closed` and `extra_items` support will go here.
Type::object().when_assignable_to(
db,
target_item_field.declared_ty,
inferable,
)
}
} else {
// As above, for `NotRequired[]` mutable fields in the target. Again the logic
// is largely the same for now, but it will get more complicated with `closed`
// and `extra_items`.
if let Some(self_item_field) = self_items.get(target_item_name) {
if self_item_field.is_read_only() {
// A read-only field can't be assigned to a mutable target.
return ConstraintSet::from(false);
}
if self_item_field.is_required() {
// A required field can't be assigned to a not-required, mutable field
// in the target, because `del` is allowed on the target field.
return ConstraintSet::from(false);
}
// As above, for mutable fields in the target, the relation needs
// to apply both ways.
self_item_field
.declared_ty
.has_relation_to_impl(
db,
target_item_field.declared_ty,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
.and(db, || {
target_item_field.declared_ty.has_relation_to_impl(
db,
self_item_field.declared_ty,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
} else {
// Self is missing this not-required, mutable field. This isn't ok if self
// has read-only extra items, which all `TypedDict`s effectively do until
// we support `closed` and explicit `extra_items`. See "A subtle
// interaction between two structural assignability rules prevents
// unsoundness" in `typed_dict.md`.
// TODO: `closed` and `extra_items` support will go here.
ConstraintSet::from(false)
}
}
};
constraints.intersect(db, field_constraints);
if constraints.is_never_satisfied(db) {
return constraints;
}
}
constraints
}
}
pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(

View File

@@ -47,7 +47,20 @@ y = foo(1)
"value": ": "
},
{
"value": "int"
"value": "int",
"location": {
"uri": "file://<typeshed>/stdlib/builtins.pyi",
"range": {
"start": {
"line": 347,
"character": 6
},
"end": {
"line": 347,
"character": 9
}
}
}
}
],
"kind": 1

View File

@@ -1198,6 +1198,7 @@ impl TestContext {
r#"The system cannot find the file specified."#,
"No such file or directory",
);
settings.add_filter(r"file://.*/stdlib/", "file://<typeshed>/stdlib/");
let settings_scope = settings.bind_to_scope();

View File

@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.14.5-alpine
name: ghcr.io/astral-sh/ruff:0.14.6-alpine
before_script:
- cd $CI_PROJECT_DIR
- ruff --version
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.5
rev: v0.14.6
hooks:
# Run the linter.
- id: ruff-check
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.5
rev: v0.14.6
hooks:
# Run the linter.
- id: ruff-check
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.5
rev: v0.14.6
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.5
rev: v0.14.6
hooks:
# Run the linter.
- id: ruff

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.14.5"
version = "0.14.6"
description = "An extremely fast Python linter and code formatter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"

View File

@@ -1,6 +1,6 @@
[project]
name = "scripts"
version = "0.14.5"
version = "0.14.6"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]