Compare commits
18 Commits
dcreager/u
...
0.14.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59c6cb521d | ||
|
|
54dba15088 | ||
|
|
1af318534a | ||
|
|
553e568624 | ||
|
|
cdef3f5ab8 | ||
|
|
6178822427 | ||
|
|
6b7adb0537 | ||
|
|
06941c1987 | ||
|
|
eb7c098d6b | ||
|
|
1b28fc1f14 | ||
|
|
290a5720cb | ||
|
|
c4767f5aa8 | ||
|
|
6e84f4fd7a | ||
|
|
78ce17ce8f | ||
|
|
0761ea42d9 | ||
|
|
416e2267da | ||
|
|
02c102da88 | ||
|
|
29c24bc8a6 |
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -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 \
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -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 \
|
||||
|
||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -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
6
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.14.5"
|
||||
version = "0.14.6"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.5"
|
||||
version = "0.14.6"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.5"
|
||||
version = "0.14.6"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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]: ...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 _(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "scripts"
|
||||
version = "0.14.5"
|
||||
version = "0.14.6"
|
||||
description = ""
|
||||
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user