Compare commits

...

12 Commits

Author SHA1 Message Date
Ibraheem Ahmed
2f4dcbf651 prefer declared type of generic classes 2025-10-31 11:01:16 -04:00
Ibraheem Ahmed
1d6ae8596a [ty] Prefer exact matches when solving constrained type variables (#21165)
## Summary

The solver is currently order-dependent, and will choose a supertype
over the exact type if it appears earlier in the list of constraints. We
could be smarter and try to choose the most precise subtype, but I
imagine this is something the new constraint solver will fix anyways,
and this fixes the issue showing up on
https://github.com/astral-sh/ruff/pull/21070.
2025-10-31 10:58:09 -04:00
Douglas Creager
cf4e82d4b0 [ty] Add and test when constraint sets are satisfied by their typevars (#21129)
This PR adds a new `satisfied_by_all_typevar` method, which implements
one of the final steps of actually using these dang constraint sets.
Constraint sets exist to help us check assignability and subtyping of
types in the presence of typevars. We construct a constraint set
describing the conditions under which assignability holds between the
two types. Then we check whether that constraint set is satisfied for
the valid specializations of the relevant typevars (which is this new
method).

We also add a new `ty_extensions.ConstraintSet` method so that we can
test this method's behavior in mdtests, before hooking it up to the rest
of the specialization inference machinery.
2025-10-31 10:53:37 -04:00
Ibraheem Ahmed
1baf98aab3 [ty] Fix is_disjoint_from with @final classes (#21167)
## Summary

We currently perform a subtyping check instead of the intended subclass
check (and the subtyping check is confusingly named `is_subclass_of`).
This showed up in https://github.com/astral-sh/ruff/pull/21070.
2025-10-31 14:50:54 +00:00
Carl Meyer
3179b05221 [ty] don't assume in diagnostic messages that a TypedDict key error is about subscript access (#21166)
## Summary

Before this PR, we would emit diagnostics like "Invalid key access" for
a TypedDict literal with invalid key, which doesn't make sense since
there's no "access" in that case. This PR just adjusts the wording to be
more general, and adjusts the documentation of the lint rule too.

I noticed this in the playground and thought it would be a quick fix. As
usual, it turned out to be a bit more subtle than I expected, but for
now I chose to punt on the complexity. We may ultimately want to have
different rules for invalid subscript vs invalid TypedDict literal,
because an invalid key in a TypedDict literal is low severity: it's a
typo detector, but not actually a type error. But then there's another
wrinkle there: if the TypedDict is `closed=True`, then it _is_ a type
error. So would we want to separate the open and closed cases into
separate rules, too? I decided to leave this as a question for future.

If we wanted to use separate rules, or use specific wording for each
case instead of the generalized wording I chose here, that would also
involve a bit of extra work to distinguish the cases, since we use a
generic set of functions for reporting these errors.

## Test Plan

Added and updated mdtests.
2025-10-31 10:49:59 -04:00
Aria Desires
172e8d4ae0 [ty] Support implicit imports of submodules in __init__.pyi (#20855)
This is a second take at the implicit imports approach, allowing `from .
import submodule` in an `__init__.pyi` to create the
`mypackage.submodule` attribute everyhere.

This implementation operates inside of the
available_submodule_attributes subsystem instead of as a re-export rule.

The upside of this is we are no longer purely syntactic, and absolute
from imports that happen to target submodules work (an intentional
discussed deviation from pyright which demands a relative from import).
Also we don't re-export functions or classes.

The downside(?) of this is star imports no longer see these attributes
(this may be either good or bad. I believe it's not a huge lift to make
it work with star imports but it's some non-trivial reworking).

I've also intentionally made `import mypackage.submodule` not trigger
this rule although it's trivial to change that.

I've tried to cover as many relevant cases as possible for discussion in
the new test file I've added (there are some random overlaps with
existing tests but trying to add them piecemeal felt confusing and
weird, so I just made a dedicated file for this extension to the rules).

Fixes https://github.com/astral-sh/ty/issues/133

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
2025-10-31 14:29:24 +00:00
Mahmoud Saada
735ec0c1f9 [ty] Fix generic inference for non-dataclass inheriting from generic dataclass (#21159)
## Summary

Fixes https://github.com/astral-sh/ty/issues/1427

This PR fixes a regression introduced in alpha.24 where non-dataclass
children of generic dataclasses lost generic type parameter information
during `__init__` synthesis.

The issue occurred because when looking up inherited members in the MRO,
the child class's `inherited_generic_context` was correctly passed down,
but `own_synthesized_member()` (which synthesizes dataclass `__init__`
methods) didn't accept this parameter. It only used
`self.inherited_generic_context(db)`, which returned the parent's
context instead of the child's.

The fix threads the child's generic context through to the synthesis
logic, allowing proper generic type inference for inherited dataclass
constructors.

## Test Plan

- Added regression test for non-dataclass inheriting from generic
dataclass
- Verified the exact repro case from the issue now works
- All 277 mdtest tests passing
- Clippy clean
- Manually verified with Python runtime, mypy, and pyright - all accept
this code pattern

## Verification

Tested against multiple type checkers:
-  Python runtime: Code works correctly
-  mypy: No issues found
-  pyright: 0 errors, 0 warnings
-  ty alpha.23: Worked (before regression)
-  ty alpha.24: Regression
-  ty with this fix: Works correctly

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: David Peter <mail@david-peter.de>
2025-10-31 13:55:17 +01:00
Ben Beasley
3585c96ea5 Update etcetera to 0.11.0 (#21160) 2025-10-31 12:53:18 +00:00
Micha Reiser
4b026c2a55 Fix missing diagnostics for notebooks (#21156) 2025-10-31 01:16:43 +00:00
Matthew Mckee
4b758b3746 [ty] Fix tests for definition completions (#21153) 2025-10-31 00:43:50 +00:00
Amethyst Reese
8737a2d5f5 Bump v0.14.3 (#21152)
- **Upgrade to rooster==0.1.1**
- **Changelog for v0.14.3**
- **Bump v0.14.3**
2025-10-30 17:06:29 -07:00
Matthew Mckee
3be3a10a2f [ty] Don't provide completions when in class or function definition (#21146) 2025-10-30 23:19:59 +00:00
43 changed files with 2055 additions and 244 deletions

View File

@@ -1,5 +1,60 @@
# Changelog
## 0.14.3
Released on 2025-10-30.
### Preview features
- Respect `--output-format` with `--watch` ([#21097](https://github.com/astral-sh/ruff/pull/21097))
- \[`pydoclint`\] Fix false positive on explicit exception re-raising (`DOC501`, `DOC502`) ([#21011](https://github.com/astral-sh/ruff/pull/21011))
- \[`pyflakes`\] Revert to stable behavior if imports for module lie in alternate branches for `F401` ([#20878](https://github.com/astral-sh/ruff/pull/20878))
- \[`pylint`\] Implement `stop-iteration-return` (`PLR1708`) ([#20733](https://github.com/astral-sh/ruff/pull/20733))
- \[`ruff`\] Add support for additional eager conversion patterns (`RUF065`) ([#20657](https://github.com/astral-sh/ruff/pull/20657))
### Bug fixes
- Fix finding keyword range for clause header after statement ending with semicolon ([#21067](https://github.com/astral-sh/ruff/pull/21067))
- Fix syntax error false positive on nested alternative patterns ([#21104](https://github.com/astral-sh/ruff/pull/21104))
- \[`ISC001`\] Fix panic when string literals are unclosed ([#21034](https://github.com/astral-sh/ruff/pull/21034))
- \[`flake8-django`\] Apply `DJ001` to annotated fields ([#20907](https://github.com/astral-sh/ruff/pull/20907))
- \[`flake8-pyi`\] Fix `PYI034` to not trigger on metaclasses (`PYI034`) ([#20881](https://github.com/astral-sh/ruff/pull/20881))
- \[`flake8-type-checking`\] Fix `TC003` false positive with `future-annotations` ([#21125](https://github.com/astral-sh/ruff/pull/21125))
- \[`pyflakes`\] Fix false positive for `__class__` in lambda expressions within class definitions (`F821`) ([#20564](https://github.com/astral-sh/ruff/pull/20564))
- \[`pyupgrade`\] Fix false positive for `TypeVar` with default on Python \<3.13 (`UP046`,`UP047`) ([#21045](https://github.com/astral-sh/ruff/pull/21045))
### Rule changes
- Add missing docstring sections to the numpy list ([#20931](https://github.com/astral-sh/ruff/pull/20931))
- \[`airflow`\] Extend `airflow.models..Param` check (`AIR311`) ([#21043](https://github.com/astral-sh/ruff/pull/21043))
- \[`airflow`\] Warn that `airflow....DAG.create_dagrun` has been removed (`AIR301`) ([#21093](https://github.com/astral-sh/ruff/pull/21093))
- \[`refurb`\] Preserve digit separators in `Decimal` constructor (`FURB157`) ([#20588](https://github.com/astral-sh/ruff/pull/20588))
### Server
- Avoid sending an unnecessary "clear diagnostics" message for clients supporting pull diagnostics ([#21105](https://github.com/astral-sh/ruff/pull/21105))
### Documentation
- \[`flake8-bandit`\] Fix correct example for `S308` ([#21128](https://github.com/astral-sh/ruff/pull/21128))
### Other changes
- Clearer error message when `line-length` goes beyond threshold ([#21072](https://github.com/astral-sh/ruff/pull/21072))
### Contributors
- [@danparizher](https://github.com/danparizher)
- [@jvacek](https://github.com/jvacek)
- [@ntBre](https://github.com/ntBre)
- [@augustelalande](https://github.com/augustelalande)
- [@prakhar1144](https://github.com/prakhar1144)
- [@TaKO8Ki](https://github.com/TaKO8Ki)
- [@dylwil3](https://github.com/dylwil3)
- [@fatelei](https://github.com/fatelei)
- [@ShaharNaveh](https://github.com/ShaharNaveh)
- [@Lee-W](https://github.com/Lee-W)
## 0.14.2
Released on 2025-10-23.

44
Cargo.lock generated
View File

@@ -243,7 +243,7 @@ dependencies = [
"bitflags 2.9.4",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.13.0",
"log",
"prettyplease",
"proc-macro2",
@@ -633,7 +633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -642,7 +642,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1007,7 +1007,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.61.0",
]
[[package]]
@@ -1093,7 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -1115,13 +1115,12 @@ dependencies = [
[[package]]
name = "etcetera"
version = "0.10.0"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6"
checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -1366,15 +1365,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "home"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "html-escape"
version = "0.2.13"
@@ -1563,7 +1553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"serde",
"serde_core",
]
@@ -1690,7 +1680,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1754,7 +1744,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2835,7 +2825,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.2"
version = "0.14.3"
dependencies = [
"anyhow",
"argfile",
@@ -3092,7 +3082,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.2"
version = "0.14.3"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3447,7 +3437,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.2"
version = "0.14.3"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3545,7 +3535,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -3941,7 +3931,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -5021,7 +5011,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.0",
]
[[package]]

View File

@@ -84,7 +84,7 @@ dashmap = { version = "6.0.1" }
dir-test = { version = "0.4.0" }
dunce = { version = "1.0.5" }
drop_bomb = { version = "0.1.5" }
etcetera = { version = "0.10.0" }
etcetera = { version = "0.11.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }

View File

@@ -147,8 +147,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.2/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.3/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.2
rev: v0.14.3
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

@@ -470,6 +470,11 @@ impl File {
self.source_type(db).is_stub()
}
/// Returns `true` if the file is an `__init__.pyi`
pub fn is_package_stub(self, db: &dyn Db) -> bool {
self.path(db).as_str().ends_with("__init__.pyi")
}
pub fn source_type(self, db: &dyn Db) -> PySourceType {
match self.path(db) {
FilePath::System(path) => path

View File

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

View File

@@ -1,7 +1,4 @@
use lsp_types::Url;
use crate::{
Session,
lint::DiagnosticsMap,
session::{Client, DocumentQuery, DocumentSnapshot},
};
@@ -22,21 +19,10 @@ pub(super) fn generate_diagnostics(snapshot: &DocumentSnapshot) -> DiagnosticsMa
}
pub(super) fn publish_diagnostics_for_document(
session: &Session,
url: &Url,
snapshot: &DocumentSnapshot,
client: &Client,
) -> crate::server::Result<()> {
// Publish diagnostics if the client doesn't support pull diagnostics
if session.resolved_client_capabilities().pull_diagnostics {
return Ok(());
}
let snapshot = session
.take_snapshot(url.clone())
.ok_or_else(|| anyhow::anyhow!("Unable to take snapshot for document with URL {url}"))
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
for (uri, diagnostics) in generate_diagnostics(&snapshot) {
for (uri, diagnostics) in generate_diagnostics(snapshot) {
client
.send_notification::<lsp_types::notification::PublishDiagnostics>(
lsp_types::PublishDiagnosticsParams {
@@ -52,14 +38,9 @@ pub(super) fn publish_diagnostics_for_document(
}
pub(super) fn clear_diagnostics_for_document(
session: &Session,
query: &DocumentQuery,
client: &Client,
) -> crate::server::Result<()> {
if session.resolved_client_capabilities().pull_diagnostics {
return Ok(());
}
client
.send_notification::<lsp_types::notification::PublishDiagnostics>(
lsp_types::PublishDiagnosticsParams {

View File

@@ -31,7 +31,11 @@ impl super::SyncNotificationHandler for DidChange {
.update_text_document(&key, content_changes, new_version)
.with_failure_code(ErrorCode::InternalError)?;
publish_diagnostics_for_document(session, &key.into_url(), client)?;
// Publish diagnostics if the client doesn't support pull diagnostics
if !session.resolved_client_capabilities().pull_diagnostics {
let snapshot = session.take_snapshot(key.into_url()).unwrap();
publish_diagnostics_for_document(&snapshot, client)?;
}
Ok(())
}

View File

@@ -27,7 +27,10 @@ impl super::SyncNotificationHandler for DidChangeNotebook {
.with_failure_code(ErrorCode::InternalError)?;
// publish new diagnostics
publish_diagnostics_for_document(session, &key.into_url(), client)?;
let snapshot = session
.take_snapshot(key.into_url())
.expect("snapshot should be available");
publish_diagnostics_for_document(&snapshot, client)?;
Ok(())
}

View File

@@ -31,13 +31,19 @@ impl super::SyncNotificationHandler for DidChangeWatchedFiles {
} else {
// publish diagnostics for text documents
for url in session.text_document_urls() {
publish_diagnostics_for_document(session, url, client)?;
let snapshot = session
.take_snapshot(url.clone())
.expect("snapshot should be available");
publish_diagnostics_for_document(&snapshot, client)?;
}
}
// always publish diagnostics for notebook files (since they don't use pull diagnostics)
for url in session.notebook_document_urls() {
publish_diagnostics_for_document(session, url, client)?;
let snapshot = session
.take_snapshot(url.clone())
.expect("snapshot should be available");
publish_diagnostics_for_document(&snapshot, client)?;
}
}

View File

@@ -27,7 +27,7 @@ impl super::SyncNotificationHandler for DidClose {
);
return Ok(());
};
clear_diagnostics_for_document(session, snapshot.query(), client)?;
clear_diagnostics_for_document(snapshot.query(), client)?;
session
.close_document(&key)

View File

@@ -1,5 +1,6 @@
use crate::TextDocument;
use crate::server::Result;
use crate::server::api::LSPResult;
use crate::server::api::diagnostics::publish_diagnostics_for_document;
use crate::session::{Client, Session};
use lsp_types as types;
@@ -29,7 +30,16 @@ impl super::SyncNotificationHandler for DidOpen {
session.open_text_document(uri.clone(), document);
publish_diagnostics_for_document(session, &uri, client)?;
// Publish diagnostics if the client doesn't support pull diagnostics
if !session.resolved_client_capabilities().pull_diagnostics {
let snapshot = session
.take_snapshot(uri.clone())
.ok_or_else(|| {
anyhow::anyhow!("Unable to take snapshot for document with URL {uri}")
})
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
publish_diagnostics_for_document(&snapshot, client)?;
}
Ok(())
}

View File

@@ -40,7 +40,10 @@ impl super::SyncNotificationHandler for DidOpenNotebook {
session.open_notebook_document(uri.clone(), notebook);
// publish diagnostics
publish_diagnostics_for_document(session, &uri, client)?;
let snapshot = session
.take_snapshot(uri)
.expect("snapshot should be available");
publish_diagnostics_for_document(&snapshot, client)?;
Ok(())
}

View File

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

110
crates/ty/docs/rules.md generated
View File

@@ -474,7 +474,7 @@ an atypical memory layout.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L598" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L606" target="_blank">View source</a>
</small>
@@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L638" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L646" target="_blank">View source</a>
</small>
@@ -529,7 +529,7 @@ a: int = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1749" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1757" target="_blank">View source</a>
</small>
@@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L660" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L668" target="_blank">View source</a>
</small>
@@ -599,7 +599,7 @@ asyncio.run(main())
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L690" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L698" target="_blank">View source</a>
</small>
@@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L741" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L749" target="_blank">View source</a>
</small>
@@ -650,7 +650,7 @@ with 1:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L762" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L770" target="_blank">View source</a>
</small>
@@ -679,7 +679,7 @@ a: str
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L785" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L793" target="_blank">View source</a>
</small>
@@ -723,7 +723,7 @@ except ZeroDivisionError:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L821" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L829" target="_blank">View source</a>
</small>
@@ -762,11 +762,15 @@ Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0
**What it does**
Checks for subscript accesses with invalid keys.
Checks for subscript accesses with invalid keys and `TypedDict` construction with an
unknown key.
**Why is this bad?**
Using an invalid key will raise a `KeyError` at runtime.
Subscripting with an invalid key will raise a `KeyError` at runtime.
Creating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is
`closed=true` it also violates the expectations of the type.
**Examples**
@@ -779,6 +783,10 @@ class Person(TypedDict):
alice = Person(name="Alice", age=30)
alice["height"] # KeyError: 'height'
bob: Person = { "name": "Bob", "age": 30 } # typo!
carol = Person(name="Carol", age=25) # typo!
```
## `invalid-legacy-type-variable`
@@ -787,7 +795,7 @@ alice["height"] # KeyError: 'height'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L847" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L855" target="_blank">View source</a>
</small>
@@ -822,7 +830,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L896" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L904" target="_blank">View source</a>
</small>
@@ -888,7 +896,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L923" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L931" target="_blank">View source</a>
</small>
@@ -938,7 +946,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1022" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1030" target="_blank">View source</a>
</small>
@@ -998,7 +1006,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1042" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1050" target="_blank">View source</a>
</small>
@@ -1047,7 +1055,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L619" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627" target="_blank">View source</a>
</small>
@@ -1072,7 +1080,7 @@ def func() -> int:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1093" target="_blank">View source</a>
</small>
@@ -1130,7 +1138,7 @@ TODO #14889
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L875" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L883" target="_blank">View source</a>
</small>
@@ -1157,7 +1165,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1124" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1132" target="_blank">View source</a>
</small>
@@ -1187,7 +1195,7 @@ TYPE_CHECKING = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1148" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1156" target="_blank">View source</a>
</small>
@@ -1217,7 +1225,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1200" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1208" target="_blank">View source</a>
</small>
@@ -1251,7 +1259,7 @@ f(10) # Error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1172" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1180" target="_blank">View source</a>
</small>
@@ -1285,7 +1293,7 @@ class C:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1228" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1236" target="_blank">View source</a>
</small>
@@ -1320,7 +1328,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1257" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1265" target="_blank">View source</a>
</small>
@@ -1345,7 +1353,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1850" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1858" target="_blank">View source</a>
</small>
@@ -1378,7 +1386,7 @@ alice["age"] # KeyError
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1276" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1284" target="_blank">View source</a>
</small>
@@ -1407,7 +1415,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1299" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1307" target="_blank">View source</a>
</small>
@@ -1431,7 +1439,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1317" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1325" target="_blank">View source</a>
</small>
@@ -1457,7 +1465,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1368" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1376" target="_blank">View source</a>
</small>
@@ -1484,7 +1492,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1603" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1611" target="_blank">View source</a>
</small>
@@ -1542,7 +1550,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1725" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1733" target="_blank">View source</a>
</small>
@@ -1572,7 +1580,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1459" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1467" target="_blank">View source</a>
</small>
@@ -1601,7 +1609,7 @@ class B(A): ... # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1504" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1512" target="_blank">View source</a>
</small>
@@ -1628,7 +1636,7 @@ f("foo") # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1482" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1490" target="_blank">View source</a>
</small>
@@ -1656,7 +1664,7 @@ def _(x: int):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1525" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1533" target="_blank">View source</a>
</small>
@@ -1702,7 +1710,7 @@ class A:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1582" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1590" target="_blank">View source</a>
</small>
@@ -1729,7 +1737,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1624" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1632" target="_blank">View source</a>
</small>
@@ -1757,7 +1765,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1646" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1654" target="_blank">View source</a>
</small>
@@ -1782,7 +1790,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1665" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1673" target="_blank">View source</a>
</small>
@@ -1807,7 +1815,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1337" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1345" target="_blank">View source</a>
</small>
@@ -1844,7 +1852,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1684" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1692" target="_blank">View source</a>
</small>
@@ -1872,7 +1880,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1706" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1714" target="_blank">View source</a>
</small>
@@ -2026,7 +2034,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1389" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397" target="_blank">View source</a>
</small>
@@ -2086,7 +2094,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1419" target="_blank">View source</a>
</small>
@@ -2118,7 +2126,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1777" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1785" target="_blank">View source</a>
</small>
@@ -2145,7 +2153,7 @@ cast(int, f()) # Redundant
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1572" target="_blank">View source</a>
</small>
@@ -2169,7 +2177,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1798" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1806" target="_blank">View source</a>
</small>
@@ -2227,7 +2235,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L708" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L716" target="_blank">View source</a>
</small>
@@ -2266,7 +2274,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L966" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L974" target="_blank">View source</a>
</small>
@@ -2353,7 +2361,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1437" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1445" target="_blank">View source</a>
</small>

View File

@@ -212,7 +212,10 @@ pub fn completion<'db>(
offset: TextSize,
) -> Vec<Completion<'db>> {
let parsed = parsed_module(db, file).load(db);
if is_in_comment(&parsed, offset) || is_in_string(&parsed, offset) {
let tokens = tokens_start_before(parsed.tokens(), offset);
if is_in_comment(tokens) || is_in_string(tokens) || is_in_definition_place(db, tokens, file) {
return vec![];
}
@@ -829,8 +832,7 @@ fn find_typed_text(
/// Whether the given offset within the parsed module is within
/// a comment or not.
fn is_in_comment(parsed: &ParsedModuleRef, offset: TextSize) -> bool {
let tokens = tokens_start_before(parsed.tokens(), offset);
fn is_in_comment(tokens: &[Token]) -> bool {
tokens.last().is_some_and(|t| t.kind().is_comment())
}
@@ -839,8 +841,7 @@ fn is_in_comment(parsed: &ParsedModuleRef, offset: TextSize) -> bool {
///
/// Note that this will return `false` when positioned within an
/// interpolation block in an f-string or a t-string.
fn is_in_string(parsed: &ParsedModuleRef, offset: TextSize) -> bool {
let tokens = tokens_start_before(parsed.tokens(), offset);
fn is_in_string(tokens: &[Token]) -> bool {
tokens.last().is_some_and(|t| {
matches!(
t.kind(),
@@ -849,6 +850,31 @@ fn is_in_string(parsed: &ParsedModuleRef, offset: TextSize) -> bool {
})
}
/// If the tokens end with `class f` or `def f` we return true.
/// If the tokens end with `class` or `def`, we return false.
/// This is fine because we don't provide completions anyway.
fn is_in_definition_place(db: &dyn Db, tokens: &[Token], file: File) -> bool {
let is_definition_keyword = |token: &Token| {
if matches!(
token.kind(),
TokenKind::Def | TokenKind::Class | TokenKind::Type
) {
true
} else if token.kind() == TokenKind::Name {
let source = source_text(db, file);
&source[token.range()] == "type"
} else {
false
}
};
tokens
.len()
.checked_sub(2)
.and_then(|i| tokens.get(i))
.is_some_and(is_definition_keyword)
}
/// Order completions according to the following rules:
///
/// 1) Names with no underscore prefix
@@ -4058,6 +4084,86 @@ def f[T](x: T):
test.build().contains("__repr__");
}
#[test]
fn no_completions_in_function_def_name() {
let builder = completion_test_builder(
"\
def f<CURSOR>
",
);
assert!(builder.auto_import().build().completions().is_empty());
}
#[test]
fn completions_in_function_def_empty_name() {
let builder = completion_test_builder(
"\
def <CURSOR>
",
);
// This is okay because the ide will not request completions when the cursor is in this position.
assert!(!builder.auto_import().build().completions().is_empty());
}
#[test]
fn no_completions_in_class_def_name() {
let builder = completion_test_builder(
"\
class f<CURSOR>
",
);
assert!(builder.auto_import().build().completions().is_empty());
}
#[test]
fn completions_in_class_def_empty_name() {
let builder = completion_test_builder(
"\
class <CURSOR>
",
);
// This is okay because the ide will not request completions when the cursor is in this position.
assert!(!builder.auto_import().build().completions().is_empty());
}
#[test]
fn no_completions_in_type_def_name() {
let builder = completion_test_builder(
"\
type f<CURSOR> = int
",
);
assert!(builder.auto_import().build().completions().is_empty());
}
#[test]
fn no_completions_in_maybe_type_def_name() {
let builder = completion_test_builder(
"\
type f<CURSOR>
",
);
assert!(builder.auto_import().build().completions().is_empty());
}
#[test]
fn completions_in_type_def_empty_name() {
let builder = completion_test_builder(
"\
type <CURSOR>
",
);
// This is okay because the ide will not request completions when the cursor is in this position.
assert!(!builder.auto_import().build().completions().is_empty());
}
/// A way to create a simple single-file (named `main.py`) completion test
/// builder.
///

View File

@@ -427,14 +427,13 @@ a = f("a")
reveal_type(a) # revealed: list[Literal["a"]]
b: list[int | Literal["a"]] = f("a")
reveal_type(b) # revealed: list[Literal["a"] | int]
reveal_type(b) # revealed: list[int | Literal["a"]]
c: list[int | str] = f("a")
reveal_type(c) # revealed: list[str | int]
reveal_type(c) # revealed: list[int | str]
d: list[int | tuple[int, int]] = f((1, 2))
# TODO: We could avoid reordering the union elements here.
reveal_type(d) # revealed: list[tuple[int, int] | int]
reveal_type(d) # revealed: list[int | tuple[int, int]]
e: list[int] = f(True)
reveal_type(e) # revealed: list[int]
@@ -455,7 +454,54 @@ j: int | str = f2(True)
reveal_type(j) # revealed: Literal[True]
```
Types are not widened unnecessarily:
## Prefer the declared type of generic classes
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Any
def f[T](x: T) -> list[T]:
return [x]
def f2[T](x: T) -> list[T] | None:
return [x]
def f3[T](x: T) -> list[T] | dict[T, T]:
return [x]
a = f(1)
reveal_type(a) # revealed: list[Literal[1]]
b: list[Any] = f(1)
reveal_type(b) # revealed: list[Any]
c: list[Any] = [1]
reveal_type(c) # revealed: list[Any]
d: list[Any] | None = f(1)
reveal_type(d) # revealed: list[Any]
e: list[Any] | None = [1]
reveal_type(e) # revealed: list[Any]
f: list[Any] | None = f2(1)
reveal_type(f) # revealed: list[Any] | None
g: list[Any] | dict[Any, Any] = f3(1)
# TODO: Better constraint solver.
reveal_type(g) # revealed: list[Literal[1]] | dict[Literal[1], Literal[1]]
```
## Prefer the inferred type of non-generic classes
```toml
[environment]
python-version = "3.12"
```
```py
def id[T](x: T) -> T:

View File

@@ -50,8 +50,6 @@ def _(l: list[int] | None = None):
def f[T](x: T, cond: bool) -> T | list[T]:
return x if cond else [x]
# TODO: no error
# error: [invalid-assignment] "Object of type `Literal[1] | list[Literal[1]]` is not assignable to `int | list[int]`"
l5: int | list[int] = f(1, True)
```

View File

@@ -838,6 +838,40 @@ class WrappedIntAndExtraData[T](Wrap[int]):
reveal_type(WrappedIntAndExtraData[bytes].__init__)
```
### Non-dataclass inheriting from generic dataclass
This is a regression test for <https://github.com/astral-sh/ty/issues/1427>.
When a non-dataclass inherits from a generic dataclass, the generic type parameters should still be
properly inferred when calling the inherited `__init__` method.
```py
from dataclasses import dataclass
@dataclass
class ParentDataclass[T]:
value: T
# Non-dataclass inheriting from generic dataclass
class ChildOfParentDataclass[T](ParentDataclass[T]): ...
def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]:
return ChildOfParentDataclass(x)
# TODO: ParentDataclass.__init__ should show generic types, not Unknown
# revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None
reveal_type(ParentDataclass.__init__)
# revealed: (self: ParentDataclass[T@ChildOfParentDataclass], value: T@ChildOfParentDataclass) -> None
reveal_type(ChildOfParentDataclass.__init__)
result_int = uses_dataclass(42)
reveal_type(result_int) # revealed: ChildOfParentDataclass[Literal[42]]
result_str = uses_dataclass("hello")
reveal_type(result_str) # revealed: ChildOfParentDataclass[Literal["hello"]]
```
## Descriptor-typed fields
### Same type in `__get__` and `__set__`

View File

@@ -545,3 +545,28 @@ def f(x: T, y: Not[T]) -> T:
y = x # error: [invalid-assignment]
return x
```
## Prefer exact matches for constrained typevars
```py
from typing import TypeVar
class Base: ...
class Sub(Base): ...
# We solve to `Sub`, regardless of the order of constraints.
T = TypeVar("T", Base, Sub)
T2 = TypeVar("T2", Sub, Base)
def f(x: T) -> list[T]:
return [x]
def f2(x: T2) -> list[T2]:
return [x]
x: list[Sub] = f(Sub())
reveal_type(x) # revealed: list[Sub]
y: list[Sub] = f2(Sub())
reveal_type(y) # revealed: list[Sub]
```

View File

@@ -0,0 +1,824 @@
# Nonstandard Import Conventions
This document covers ty-specific extensions to the
[standard import conventions](https://typing.python.org/en/latest/spec/distributing.html#import-conventions).
It's a common idiom for a package's `__init__.py(i)` to include several imports like
`from . import mysubmodule`, with the intent that the `mypackage.mysubmodule` attribute should work
for anyone who only imports `mypackage`.
In the context of a `.py` we handle this well through our general attempts to faithfully implement
import side-effects. However for `.pyi` files we are expected to apply
[a more strict set of rules](https://typing.python.org/en/latest/spec/distributing.html#import-conventions)
to encourage intentional API design. Although `.pyi` files are explicitly designed to work with
typecheckers, which ostensibly should all enforce these strict rules, every typechecker has its own
defacto "extensions" to them and so a few idioms like `from . import mysubmodule` have found their
way into `.pyi` files too.
Thus for the sake of compatibility, we need to define our own "extensions". Any extensions we define
here have several competing concerns:
- Extensions should ideally be kept narrow to continue to encourage explicit API design
- Extensions should be easy to explain, document, and understand
- Extensions should ideally still be a subset of runtime behaviour (if it works in a stub, it works
at runtime)
- Extensions should ideally not make `.pyi` files more permissive than `.py` files (if it works in a
stub, it works in an impl)
To that end we define the following extension:
> If an `__init__.pyi` for `mypackage` contains a `from...import` targetting a direct submodule of
> `mypackage`, then that submodule should be available as an attribute of `mypackage`.
## Relative `from` Import of Direct Submodule in `__init__`
The `from . import submodule` idiom in an `__init__.pyi` is fairly explicit and we should definitely
support it.
`mypackage/__init__.pyi`:
```pyi
from . import imported
```
`mypackage/imported.pyi`:
```pyi
X: int = 42
```
`mypackage/fails.pyi`:
```pyi
Y: int = 47
```
`main.py`:
```py
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: "has no member `fails`"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
## Relative `from` Import of Direct Submodule in `__init__` (Non-Stub Check)
`mypackage/__init__.py`:
```py
from . import imported
```
`mypackage/imported.py`:
```py
X: int = 42
```
`mypackage/fails.py`:
```py
Y: int = 47
```
`main.py`:
```py
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: "has no member `fails`"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
## Absolute `from` Import of Direct Submodule in `__init__`
If an absolute `from...import` happens to import a submodule, it works just as well as a relative
one.
`mypackage/__init__.pyi`:
```pyi
from mypackage import imported
```
`mypackage/imported.pyi`:
```pyi
X: int = 42
```
`mypackage/fails.pyi`:
```pyi
Y: int = 47
```
`main.py`:
```py
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: "has no member `fails`"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
## Absolute `from` Import of Direct Submodule in `__init__` (Non-Stub Check)
`mypackage/__init__.py`:
```py
from mypackage import imported
```
`mypackage/imported.py`:
```py
X: int = 42
```
`mypackage/fails.py`:
```py
Y: int = 47
```
`main.py`:
```py
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: "has no member `fails`"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
## Import of Direct Submodule in `__init__`
An `import` that happens to import a submodule does not expose the submodule as an attribute. (This
is an arbitrary decision and can be changed easily!)
`mypackage/__init__.pyi`:
```pyi
import mypackage.imported
```
`mypackage/imported.pyi`:
```pyi
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this is probably safe to allow, as it's an unambiguous import of a submodule
# error: "has no member `imported`"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
## Import of Direct Submodule in `__init__` (Non-Stub Check)
`mypackage/__init__.py`:
```py
import mypackage.imported
```
`mypackage/imported.py`:
```py
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this is probably safe to allow, as it's an unambiguous import of a submodule
# error: "has no member `imported`"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
## Relative `from` Import of Nested Submodule in `__init__`
`from .submodule import nested` in an `__init__.pyi` is currently not supported as a way to expose
`mypackage.submodule` or `mypackage.submodule.nested` but it could be.
`mypackage/__init__.pyi`:
```pyi
from .submodule import nested
```
`mypackage/submodule/__init__.pyi`:
```pyi
```
`mypackage/submodule/nested.pyi`:
```pyi
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this would be nice to allow
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
```
## Relative `from` Import of Nested Submodule in `__init__` (Non-Stub Check)
`mypackage/__init__.py`:
```py
from .submodule import nested
```
`mypackage/submodule/__init__.py`:
```py
```
`mypackage/submodule/nested.py`:
```py
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this would be nice to support
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
```
## Absolute `from` Import of Nested Submodule in `__init__`
`from mypackage.submodule import nested` in an `__init__.pyi` is currently not supported as a way to
expose `mypackage.submodule` or `mypackage.submodule.nested` but it could be.
`mypackage/__init__.pyi`:
```pyi
from mypackage.submodule import nested
```
`mypackage/submodule/__init__.pyi`:
```pyi
```
`mypackage/submodule/nested.pyi`:
```pyi
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this would be nice to support
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
```
## Absolute `from` Import of Nested Submodule in `__init__` (Non-Stub Check)
`mypackage/__init__.py`:
```py
from mypackage.submodule import nested
```
`mypackage/submodule/__init__.py`:
```py
```
`mypackage/submodule/nested.py`:
```py
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this would be nice to support
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
```
## Import of Nested Submodule in `__init__`
`import mypackage.submodule.nested` in an `__init__.pyi` is currently not supported as a way to
expose `mypackage.submodule` or `mypackage.submodule.nested` but it could be.
`mypackage/__init__.pyi`:
```pyi
import mypackage.submodule.nested
```
`mypackage/submodule/__init__.pyi`:
```pyi
```
`mypackage/submodule/nested.pyi`:
```pyi
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this would be nice to support, and is probably safe to do as it's unambiguous
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
```
## Import of Nested Submodule in `__init__` (Non-Stub Check)
`mypackage/__init__.py`:
```py
import mypackage.submodule.nested
```
`mypackage/submodule/__init__.py`:
```py
```
`mypackage/submodule/nested.py`:
```py
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this would be nice to support, and is probably safe to do as it's unambiguous
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
```
## Relative `from` Import of Direct Submodule in `__init__`, Mismatched Alias
Renaming the submodule to something else disables the `__init__.pyi` idiom.
`mypackage/__init__.pyi`:
```pyi
from . import imported as imported_m
```
`mypackage/imported.pyi`:
```pyi
X: int = 42
```
`main.py`:
```py
import mypackage
# error: "has no member `imported`"
reveal_type(mypackage.imported.X) # revealed: Unknown
# error: "has no member `imported_m`"
reveal_type(mypackage.imported_m.X) # revealed: Unknown
```
## Relative `from` Import of Direct Submodule in `__init__`, Mismatched Alias (Non-Stub Check)
`mypackage/__init__.py`:
```py
from . import imported as imported_m
```
`mypackage/imported.py`:
```py
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this would be nice to support, as it works at runtime
# error: "has no member `imported`"
reveal_type(mypackage.imported.X) # revealed: Unknown
reveal_type(mypackage.imported_m.X) # revealed: int
```
## Relative `from` Import of Direct Submodule in `__init__`, Matched Alias
The `__init__.pyi` idiom should definitely always work if the submodule is renamed to itself, as
this is the re-export idiom.
`mypackage/__init__.pyi`:
```pyi
from . import imported as imported
```
`mypackage/imported.pyi`:
```pyi
X: int = 42
```
`main.py`:
```py
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
```
## Relative `from` Import of Direct Submodule in `__init__`, Matched Alias (Non-Stub Check)
`mypackage/__init__.py`:
```py
from . import imported as imported
```
`mypackage/imported.py`:
```py
X: int = 42
```
`main.py`:
```py
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
```
## Star Import Unaffected
Even if the `__init__` idiom is in effect, star imports do not pick it up. (This is an arbitrary
decision that mostly fell out of the implementation details and can be changed!)
`mypackage/__init__.pyi`:
```pyi
from . import imported
Z: int = 17
```
`mypackage/imported.pyi`:
```pyi
X: int = 42
```
`main.py`:
```py
from mypackage import *
# TODO: this would be nice to support (available_submodule_attributes isn't visible to `*` imports)
# error: "`imported` used when not defined"
reveal_type(imported.X) # revealed: Unknown
reveal_type(Z) # revealed: int
```
## Star Import Unaffected (Non-Stub Check)
`mypackage/__init__.py`:
```py
from . import imported
Z: int = 17
```
`mypackage/imported.py`:
```py
X: int = 42
```
`main.py`:
```py
from mypackage import *
reveal_type(imported.X) # revealed: int
reveal_type(Z) # revealed: int
```
## `from` Import of Non-Submodule
A from import that terminates in a non-submodule should not expose the intermediate submodules as
attributes. This is an arbitrary decision but on balance probably safe and correct, as otherwise it
would be hard for a stub author to be intentional about the submodules being exposed as attributes.
`mypackage/__init__.pyi`:
```pyi
from .imported import X
```
`mypackage/imported.pyi`:
```pyi
X: int = 42
```
`main.py`:
```py
import mypackage
# error: "has no member `imported`"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
## `from` Import of Non-Submodule (Non-Stub Check)
`mypackage/__init__.py`:
```py
from .imported import X
```
`mypackage/imported.py`:
```py
X: int = 42
```
`main.py`:
```py
import mypackage
# TODO: this would be nice to support, as it works at runtime
# error: "has no member `imported`"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
## `from` Import of Other Package's Submodule
`from mypackage import submodule` from outside the package is not modeled as a side-effect on
`mypackage`, even in the importing file (this could be changed!).
`mypackage/__init__.pyi`:
```pyi
```
`mypackage/imported.pyi`:
```pyi
X: int = 42
```
`main.py`:
```py
import mypackage
from mypackage import imported
# TODO: this would be nice to support, but it's dangerous with available_submodule_attributes
reveal_type(imported.X) # revealed: int
# error: "has no member `imported`"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
## `from` Import of Other Package's Submodule (Non-Stub Check)
`mypackage/__init__.py`:
```py
```
`mypackage/imported.py`:
```py
X: int = 42
```
`main.py`:
```py
import mypackage
from mypackage import imported
# TODO: this would be nice to support, as it works at runtime
reveal_type(imported.X) # revealed: int
# error: "has no member `imported`"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
## `from` Import of Sibling Module
`from . import submodule` from a sibling module is not modeled as a side-effect on `mypackage` or a
re-export from `submodule`.
`mypackage/__init__.pyi`:
```pyi
```
`mypackage/imported.pyi`:
```pyi
from . import fails
X: int = 42
```
`mypackage/fails.pyi`:
```pyi
Y: int = 47
```
`main.py`:
```py
import mypackage
from mypackage import imported
reveal_type(imported.X) # revealed: int
# error: "has no member `fails`"
reveal_type(imported.fails.Y) # revealed: Unknown
# error: "has no member `fails`"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
## `from` Import of Sibling Module (Non-Stub Check)
`mypackage/__init__.py`:
```py
```
`mypackage/imported.py`:
```py
from . import fails
X: int = 42
```
`mypackage/fails.py`:
```py
Y: int = 47
```
`main.py`:
```py
import mypackage
from mypackage import imported
reveal_type(imported.X) # revealed: int
reveal_type(imported.fails.Y) # revealed: int
# error: "has no member `fails`"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
## Fractal Re-export Nameclash Problems
This precise configuration of:
- a subpackage that defines a submodule with its own name
- that in turn defines a function/class with its own name
- and re-exporting that name through every layer using `from` imports and `__all__`
Can easily result in the typechecker getting "confused" and thinking imports of the name from the
top-level package are referring to the subpackage and not the function/class. This issue can be
found with the `lobpcg` function in `scipy.sparse.linalg`.
This kind of failure mode is why the rule is restricted to *direct* submodule imports, as anything
more powerful than that in the current implementation strategy quickly gets the functions and
submodules mixed up.
`mypackage/__init__.pyi`:
```pyi
from .funcmod import funcmod
__all__ = ["funcmod"]
```
`mypackage/funcmod/__init__.pyi`:
```pyi
from .funcmod import funcmod
__all__ = ["funcmod"]
```
`mypackage/funcmod/funcmod.pyi`:
```pyi
__all__ = ["funcmod"]
def funcmod(x: int) -> int: ...
```
`main.py`:
```py
from mypackage import funcmod
x = funcmod(1)
```
## Fractal Re-export Nameclash Problems (Non-Stub Check)
`mypackage/__init__.py`:
```py
from .funcmod import funcmod
__all__ = ["funcmod"]
```
`mypackage/funcmod/__init__.py`:
```py
from .funcmod import funcmod
__all__ = ["funcmod"]
```
`mypackage/funcmod/funcmod.py`:
```py
__all__ = ["funcmod"]
def funcmod(x: int) -> int:
return x
```
`main.py`:
```py
from mypackage import funcmod
x = funcmod(1)
```

View File

@@ -37,20 +37,24 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
23 |
24 | def write_to_non_literal_string_key(person: Person, str_key: str):
25 | person[str_key] = "Alice" # error: [invalid-key]
26 | from typing_extensions import ReadOnly
27 |
28 | class Employee(TypedDict):
29 | id: ReadOnly[int]
30 | name: str
26 |
27 | def create_with_invalid_string_key():
28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
30 | from typing_extensions import ReadOnly
31 |
32 | def write_to_readonly_key(employee: Employee):
33 | employee["id"] = 42 # error: [invalid-assignment]
32 | class Employee(TypedDict):
33 | id: ReadOnly[int]
34 | name: str
35 |
36 | def write_to_readonly_key(employee: Employee):
37 | employee["id"] = 42 # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-key]: Invalid key access on TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:8:5
|
7 | def access_invalid_literal_string_key(person: Person):
@@ -66,7 +70,7 @@ info: rule `invalid-key` is enabled by default
```
```
error[invalid-key]: Invalid key access on TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:13:5
|
12 | def access_invalid_key(person: Person):
@@ -82,7 +86,7 @@ info: rule `invalid-key` is enabled by default
```
```
error[invalid-key]: TypedDict `Person` cannot be indexed with a key of type `str`
error[invalid-key]: Invalid key for TypedDict `Person` of type `str`
--> src/mdtest_snippet.py:16:12
|
15 | def access_with_str_key(person: Person, str_key: str):
@@ -123,7 +127,7 @@ info: rule `invalid-assignment` is enabled by default
```
```
error[invalid-key]: Invalid key access on TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:22:5
|
21 | def write_to_non_existing_key(person: Person):
@@ -145,7 +149,39 @@ error[invalid-key]: Cannot access `Person` with a key of type `str`. Only string
24 | def write_to_non_literal_string_key(person: Person, str_key: str):
25 | person[str_key] = "Alice" # error: [invalid-key]
| ^^^^^^^
26 | from typing_extensions import ReadOnly
26 |
27 | def create_with_invalid_string_key():
|
info: rule `invalid-key` is enabled by default
```
```
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:28:21
|
27 | def create_with_invalid_string_key():
28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
| -----------------------------^^^^^^^^^--------
| | |
| | Unknown key "unknown"
| TypedDict `Person`
29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
30 | from typing_extensions import ReadOnly
|
info: rule `invalid-key` is enabled by default
```
```
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:29:11
|
27 | def create_with_invalid_string_key():
28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
| ------ TypedDict `Person` ^^^^^^^^^^^^^ Unknown key "unknown"
30 | from typing_extensions import ReadOnly
|
info: rule `invalid-key` is enabled by default
@@ -153,21 +189,21 @@ info: rule `invalid-key` is enabled by default
```
error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee`
--> src/mdtest_snippet.py:33:5
--> src/mdtest_snippet.py:37:5
|
32 | def write_to_readonly_key(employee: Employee):
33 | employee["id"] = 42 # error: [invalid-assignment]
36 | def write_to_readonly_key(employee: Employee):
37 | employee["id"] = 42 # error: [invalid-assignment]
| -------- ^^^^ key is marked read-only
| |
| TypedDict `Employee`
|
info: Item declaration
--> src/mdtest_snippet.py:29:5
--> src/mdtest_snippet.py:33:5
|
28 | class Employee(TypedDict):
29 | id: ReadOnly[int]
32 | class Employee(TypedDict):
33 | id: ReadOnly[int]
| ----------------- Read-only item declared here
30 | name: str
34 | name: str
|
info: rule `invalid-assignment` is enabled by default

View File

@@ -87,6 +87,31 @@ static_assert(is_disjoint_from(memoryview, Foo))
static_assert(is_disjoint_from(type[memoryview], type[Foo]))
```
## Specialized `@final` types
```toml
[environment]
python-version = "3.12"
```
```py
from typing import final
from ty_extensions import static_assert, is_disjoint_from
@final
class Foo[T]:
def get(self) -> T:
raise NotImplementedError
class A: ...
class B: ...
static_assert(not is_disjoint_from(Foo[A], Foo[B]))
# TODO: `int` and `str` are disjoint bases, so these should be disjoint.
static_assert(not is_disjoint_from(Foo[int], Foo[str]))
```
## "Disjoint base" builtin types
Most other builtins can be subclassed and can even be used in multiple inheritance. However, builtin

View File

@@ -0,0 +1,220 @@
# Constraint set satisfaction
```toml
[environment]
python-version = "3.12"
```
Constraint sets exist to help us check assignability and subtyping of types in the presence of
typevars. We construct a constraint set describing the conditions under which assignability holds
between the two types. Then we check whether that constraint set is satisfied for the valid
specializations of the relevant typevars. This file tests that final step.
## Inferable vs non-inferable typevars
Typevars can appear in _inferable_ or _non-inferable_ positions.
When a typevar is in an inferable position, the constraint set only needs to be satisfied for _some_
valid specialization. The most common inferable position occurs when invoking a generic function:
all of the function's typevars are inferable, because we want to use the argument types to infer
which specialization is being invoked.
When a typevar is in a non-inferable position, the constraint set must be satisfied for _every_
valid specialization. The most common non-inferable position occurs in the body of a generic
function or class: here we don't know in advance what type the typevar will be specialized to, and
so we have to ensure that the body is valid for all possible specializations.
```py
def f[T](t: T) -> T:
# In the function body, T is non-inferable. All assignability checks involving T must be
# satisfied for _all_ valid specializations of T.
return t
# When invoking the function, T is inferable — we attempt to infer a specialization that is valid
# for the particular arguments that are passed to the function. Assignability checks (in particular,
# that the argument type is assignable to the parameter type) only need to succeed for _at least
# one_ specialization.
f(1)
```
In all of the examples below, for ease of reproducibility, we explicitly list the typevars that are
inferable in each `satisfied_by_all_typevars` call; any typevar not listed is assumed to be
non-inferable.
## Unbounded typevar
If a typevar has no bound or constraints, then it can specialize to any type. In an inferable
position, that means we just need a single type (any type at all!) that satisfies the constraint
set. In a non-inferable position, that means the constraint set must be satisfied for every possible
type.
```py
from typing import final, Never
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
class Sub(Base): ...
@final
class Unrelated: ...
def unbounded[T]():
static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(ConstraintSet.always().satisfied_by_all_typevars())
static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(not ConstraintSet.never().satisfied_by_all_typevars())
# (T = Never) is a valid specialization, which satisfies (T ≤ Unrelated).
static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated).
static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars())
# (T = Base) is a valid specialization, which satisfies (T ≤ Super).
static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Super).
static_assert(not ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars())
# (T = Base) is a valid specialization, which satisfies (T ≤ Base).
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Base).
static_assert(not ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars())
# (T = Sub) is a valid specialization, which satisfies (T ≤ Sub).
static_assert(ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Sub).
static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars())
```
## Typevar with an upper bound
If a typevar has an upper bound, then it must specialize to a type that is a subtype of that bound.
For an inferable typevar, that means we need a single type that satisfies both the constraint set
and the upper bound. For a non-inferable typevar, that means the constraint set must be satisfied
for every type that satisfies the upper bound.
```py
from typing import final, Never
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
class Sub(Base): ...
@final
class Unrelated: ...
def bounded[T: Base]():
static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(ConstraintSet.always().satisfied_by_all_typevars())
static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(not ConstraintSet.never().satisfied_by_all_typevars())
# (T = Base) is a valid specialization, which satisfies (T ≤ Super).
static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T]))
# Every valid specialization satisfies (T ≤ Base). Since (Base ≤ Super), every valid
# specialization also satisfies (T ≤ Super).
static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars())
# (T = Base) is a valid specialization, which satisfies (T ≤ Base).
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T]))
# Every valid specialization satisfies (T ≤ Base).
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars())
# (T = Sub) is a valid specialization, which satisfies (T ≤ Sub).
static_assert(ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Base) is a valid specialization, which does not satisfy (T ≤ Sub).
static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars())
# (T = Never) is a valid specialization, which satisfies (T ≤ Unrelated).
constraints = ConstraintSet.range(Never, T, Unrelated)
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated).
static_assert(not constraints.satisfied_by_all_typevars())
# Never is the only type that satisfies both (T ≤ Base) and (T ≤ Unrelated). So there is no
# valid specialization that satisfies (T ≤ Unrelated ∧ T ≠ Never).
constraints = constraints & ~ConstraintSet.range(Never, T, Never)
static_assert(not constraints.satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(not constraints.satisfied_by_all_typevars())
```
## Constrained typevar
If a typevar has constraints, then it must specialize to one of those specific types. (Not to a
subtype of one of those types!) For an inferable typevar, that means we need the constraint set to
be satisfied by any one of the constraints. For a non-inferable typevar, that means we need the
constraint set to be satisfied by all of those constraints.
```py
from typing import final, Never
from ty_extensions import ConstraintSet, static_assert
class Super: ...
class Base(Super): ...
class Sub(Base): ...
@final
class Unrelated: ...
def constrained[T: (Base, Unrelated)]():
static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(ConstraintSet.always().satisfied_by_all_typevars())
static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(not ConstraintSet.never().satisfied_by_all_typevars())
# (T = Unrelated) is a valid specialization, which satisfies (T ≤ Unrelated).
static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated).
static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars())
# (T = Base) is a valid specialization, which satisfies (T ≤ Super).
static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Super).
static_assert(not ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars())
# (T = Base) is a valid specialization, which satisfies (T ≤ Base).
static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Base).
static_assert(not ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars())
# Neither (T = Base) nor (T = Unrelated) satisfy (T ≤ Sub).
static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars())
# (T = Base) and (T = Unrelated) both satisfy (T ≤ Super T ≤ Unrelated).
constraints = ConstraintSet.range(Never, T, Super) | ConstraintSet.range(Never, T, Unrelated)
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(constraints.satisfied_by_all_typevars())
# (T = Base) and (T = Unrelated) both satisfy (T ≤ Base T ≤ Unrelated).
constraints = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Unrelated)
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(constraints.satisfied_by_all_typevars())
# (T = Unrelated) is a valid specialization, which satisfies (T ≤ Sub T ≤ Unrelated).
constraints = ConstraintSet.range(Never, T, Sub) | ConstraintSet.range(Never, T, Unrelated)
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Base) is a valid specialization, which does not satisfy (T ≤ Sub T ≤ Unrelated).
static_assert(not constraints.satisfied_by_all_typevars())
# (T = Unrelated) is a valid specialization, which satisfies (T = Super T = Unrelated).
constraints = ConstraintSet.range(Super, T, Super) | ConstraintSet.range(Unrelated, T, Unrelated)
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Base) is a valid specialization, which does not satisfy (T = Super T = Unrelated).
static_assert(not constraints.satisfied_by_all_typevars())
# (T = Base) and (T = Unrelated) both satisfy (T = Base T = Unrelated).
constraints = ConstraintSet.range(Base, T, Base) | ConstraintSet.range(Unrelated, T, Unrelated)
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
static_assert(constraints.satisfied_by_all_typevars())
# (T = Unrelated) is a valid specialization, which satisfies (T = Sub T = Unrelated).
constraints = ConstraintSet.range(Sub, T, Sub) | ConstraintSet.range(Unrelated, T, Unrelated)
static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T]))
# (T = Base) is a valid specialization, which does not satisfy (T = Sub T = Unrelated).
static_assert(not constraints.satisfied_by_all_typevars())
```

View File

@@ -29,7 +29,7 @@ alice: Person = {"name": "Alice", "age": 30}
reveal_type(alice["name"]) # revealed: str
reveal_type(alice["age"]) # revealed: int | None
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(alice["non_existing"]) # revealed: Unknown
```
@@ -41,7 +41,7 @@ bob = Person(name="Bob", age=25)
reveal_type(bob["name"]) # revealed: str
reveal_type(bob["age"]) # revealed: int | None
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(bob["non_existing"]) # revealed: Unknown
```
@@ -69,7 +69,7 @@ def name_or_age() -> Literal["name", "age"]:
carol: Person = {NAME: "Carol", AGE: 20}
reveal_type(carol[NAME]) # revealed: str
# error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`"
# error: [invalid-key] "Invalid key for TypedDict `Person` of type `str`"
reveal_type(carol[non_literal()]) # revealed: Unknown
reveal_type(carol[name_or_age()]) # revealed: str | int | None
@@ -81,7 +81,7 @@ def _():
CAPITALIZED_NAME = "Name"
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "Name" - did you mean "name"?"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "Name" - did you mean "name"?"
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
dave: Person = {CAPITALIZED_NAME: "Dave", "age": 20}
@@ -104,9 +104,9 @@ eve2a: Person = {"age": 22}
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
eve2b = Person(age=22)
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
eve3b = Person(name="Eve", age=25, extra=True)
```
@@ -157,10 +157,10 @@ bob["name"] = None
Assignments to non-existing keys are disallowed:
```py
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
alice["extra"] = True
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
bob["extra"] = True
```
@@ -185,10 +185,10 @@ alice: Person = {"inner": {"name": "Alice", "age": 30}}
reveal_type(alice["inner"]["name"]) # revealed: str
reveal_type(alice["inner"]["age"]) # revealed: int | None
# error: [invalid-key] "Invalid key access on TypedDict `Inner`: Unknown key "non_existing""
# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "non_existing""
reveal_type(alice["inner"]["non_existing"]) # revealed: Unknown
# error: [invalid-key] "Invalid key access on TypedDict `Inner`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "extra""
alice: Person = {"inner": {"name": "Alice", "age": 30, "extra": 1}}
```
@@ -267,22 +267,22 @@ a_person = {"name": None, "age": 30}
All of these have an extra field that is not defined in the `TypedDict`:
```py
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
alice4: Person = {"name": "Alice", "age": 30, "extra": True}
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
Person(name="Alice", age=30, extra=True)
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
Person({"name": "Alice", "age": 30, "extra": True})
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
accepts_person({"name": "Alice", "age": 30, "extra": True})
# TODO: this should be an error
house.owner = {"name": "Alice", "age": 30, "extra": True}
a_person: Person
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
a_person = {"name": "Alice", "age": 30, "extra": True}
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
(a_person := {"name": "Alice", "age": 30, "extra": True})
```
@@ -323,7 +323,7 @@ user2 = User({"name": "Bob"})
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `User`: value of type `None`"
user3 = User({"name": None, "age": 25})
# error: [invalid-key] "Invalid key access on TypedDict `User`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `User`: Unknown key "extra""
user4 = User({"name": "Charlie", "age": 30, "extra": True})
```
@@ -360,7 +360,7 @@ invalid = OptionalPerson(name=123)
Extra fields are still not allowed, even with `total=False`:
```py
# error: [invalid-key] "Invalid key access on TypedDict `OptionalPerson`: Unknown key "extra""
# error: [invalid-key] "Invalid key for TypedDict `OptionalPerson`: Unknown key "extra""
invalid_extra = OptionalPerson(name="George", extra=True)
```
@@ -503,10 +503,10 @@ def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age",
reveal_type(person[union_of_keys]) # revealed: int | None | str
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(person["non_existing"]) # revealed: Unknown
# error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`"
# error: [invalid-key] "Invalid key for TypedDict `Person` of type `str`"
reveal_type(person[str_key]) # revealed: Unknown
# No error here:
@@ -530,7 +530,7 @@ def _(person: Person):
person["name"] = "Alice"
person["age"] = 30
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "naem" - did you mean "name"?"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "naem" - did you mean "name"?"
person["naem"] = "Alice"
def _(person: Person):
@@ -646,7 +646,7 @@ def _(p: Person) -> None:
reveal_type(p.setdefault("name", "Alice")) # revealed: str
reveal_type(p.setdefault("extra", "default")) # revealed: str
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?"
reveal_type(p.setdefault("extraz", "value")) # revealed: Unknown
```
@@ -1015,6 +1015,10 @@ def write_to_non_existing_key(person: Person):
def write_to_non_literal_string_key(person: Person, str_key: str):
person[str_key] = "Alice" # error: [invalid-key]
def create_with_invalid_string_key():
alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
```
Assignment to `ReadOnly` keys:

View File

@@ -6,12 +6,12 @@ use ruff_db::parsed::parsed_module;
use ruff_index::{IndexSlice, IndexVec};
use ruff_python_ast::NodeIndex;
use ruff_python_ast::name::Name;
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
use rustc_hash::{FxHashMap, FxHashSet};
use salsa::Update;
use salsa::plumbing::AsId;
use crate::Db;
use crate::module_name::ModuleName;
use crate::node_key::NodeKey;
use crate::semantic_index::ast_ids::AstIds;
@@ -28,6 +28,7 @@ use crate::semantic_index::scope::{
use crate::semantic_index::symbol::ScopedSymbolId;
use crate::semantic_index::use_def::{EnclosingSnapshotKey, ScopedEnclosingSnapshotId, UseDefMap};
use crate::semantic_model::HasTrackedScope;
use crate::{Db, Module, resolve_module};
pub mod ast_ids;
mod builder;
@@ -75,20 +76,73 @@ pub(crate) fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<Plac
/// Returns the set of modules that are imported anywhere in `file`.
///
/// This set only considers `import` statements, not `from...import` statements, because:
///
/// - In `from foo import bar`, we cannot determine whether `foo.bar` is a submodule (and is
/// therefore imported) without looking outside the content of this file. (We could turn this
/// into a _potentially_ imported modules set, but that would change how it's used in our type
/// inference logic.)
///
/// - We cannot resolve relative imports (which aren't allowed in `import` statements) without
/// knowing the name of the current module, and whether it's a package.
/// This set only considers `import` statements, not `from...import` statements.
/// See [`ModuleLiteralType::available_submodule_attributes`] for discussion
/// of why this analysis is intentionally limited.
#[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc<FxHashSet<ModuleName>> {
semantic_index(db, file).imported_modules.clone()
}
/// Returns the set of relative submodules that are explicitly imported anywhere in
/// `importing_module`.
///
/// This set only considers `from...import` statements (but it could also include `import`).
/// It also only returns a non-empty result for `__init__.pyi` files.
/// See [`ModuleLiteralType::available_submodule_attributes`] for discussion
/// of why this analysis is intentionally limited.
///
/// This function specifically implements the rule that if an `__init__.pyi` file
/// contains a `from...import` that imports a direct submodule of the package,
/// that submodule should be available as an attribute of the package.
///
/// While we endeavour to accurately model import side-effects for `.py` files, we intentionally
/// limit them for `.pyi` files to encourage more intentional API design. The standard escape
/// hatches for this are the `import x as x` idiom or listing them in `__all__`, but in practice
/// some other idioms are popular.
///
/// In particular, many packages have their `__init__` include lines like
/// `from . import subpackage`, with the intent that `mypackage.subpackage` should be
/// available for anyone who only does `import mypackage`.
#[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn imported_relative_submodules_of_stub_package<'db>(
db: &'db dyn Db,
importing_module: Module<'db>,
) -> Box<[ModuleName]> {
let Some(file) = importing_module.file(db) else {
return Box::default();
};
if !file.is_package_stub(db) {
return Box::default();
}
semantic_index(db, file)
.maybe_imported_modules
.iter()
.filter_map(|import| {
let mut submodule = ModuleName::from_identifier_parts(
db,
file,
import.from_module.as_deref(),
import.level,
)
.ok()?;
// We only actually care if this is a direct submodule of the package
// so this part should actually be exactly the importing module.
let importing_module_name = importing_module.name(db);
if importing_module_name != &submodule {
return None;
}
submodule.extend(&ModuleName::new(import.submodule.as_str())?);
// Throw out the result if this doesn't resolve to an actual module.
// This is quite expensive, but we've gone through a lot of hoops to
// get here so it won't happen too much.
resolve_module(db, &submodule)?;
// Return only the relative part
submodule.relative_to(importing_module_name)
})
.collect()
}
/// Returns the use-def map for a specific `scope`.
///
/// Using [`use_def_map`] over [`semantic_index`] has the advantage that
@@ -230,6 +284,9 @@ pub(crate) struct SemanticIndex<'db> {
/// The set of modules that are imported anywhere within this file.
imported_modules: Arc<FxHashSet<ModuleName>>,
/// `from...import` statements within this file that might import a submodule.
maybe_imported_modules: FxHashSet<MaybeModuleImport>,
/// Flags about the global scope (code usage impacting inference)
has_future_annotations: bool,
@@ -243,6 +300,16 @@ pub(crate) struct SemanticIndex<'db> {
generator_functions: FxHashSet<FileScopeId>,
}
/// A `from...import` that may be an import of a module
///
/// Later analysis will determine if it is.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)]
pub(crate) struct MaybeModuleImport {
level: u32,
from_module: Option<Name>,
submodule: Name,
}
impl<'db> SemanticIndex<'db> {
/// Returns the place table for a specific scope.
///

View File

@@ -47,7 +47,9 @@ use crate::semantic_index::symbol::{ScopedSymbolId, Symbol};
use crate::semantic_index::use_def::{
EnclosingSnapshotKey, FlowSnapshot, ScopedEnclosingSnapshotId, UseDefMapBuilder,
};
use crate::semantic_index::{ExpressionsScopeMap, SemanticIndex, VisibleAncestorsIter};
use crate::semantic_index::{
ExpressionsScopeMap, MaybeModuleImport, SemanticIndex, VisibleAncestorsIter,
};
use crate::semantic_model::HasTrackedScope;
use crate::unpack::{EvaluationMode, Unpack, UnpackKind, UnpackPosition, UnpackValue};
use crate::{Db, Program};
@@ -111,6 +113,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> {
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
imported_modules: FxHashSet<ModuleName>,
maybe_imported_modules: FxHashSet<MaybeModuleImport>,
/// Hashset of all [`FileScopeId`]s that correspond to [generator functions].
///
/// [generator functions]: https://docs.python.org/3/glossary.html#term-generator
@@ -148,6 +151,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
definitions_by_node: FxHashMap::default(),
expressions_by_node: FxHashMap::default(),
maybe_imported_modules: FxHashSet::default(),
imported_modules: FxHashSet::default(),
generator_functions: FxHashSet::default(),
@@ -1262,6 +1266,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
self.scopes_by_node.shrink_to_fit();
self.generator_functions.shrink_to_fit();
self.enclosing_snapshots.shrink_to_fit();
self.maybe_imported_modules.shrink_to_fit();
SemanticIndex {
place_tables,
@@ -1274,6 +1279,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
scopes_by_node: self.scopes_by_node,
use_def_maps,
imported_modules: Arc::new(self.imported_modules),
maybe_imported_modules: self.maybe_imported_modules,
has_future_annotations: self.has_future_annotations,
enclosing_snapshots: self.enclosing_snapshots,
semantic_syntax_errors: self.semantic_syntax_errors.into_inner(),
@@ -1558,6 +1564,15 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
(&alias.name.id, false)
};
// If there's no alias or a redundant alias, record this as a potential import of a submodule
if alias.asname.is_none() || is_reexported {
self.maybe_imported_modules.insert(MaybeModuleImport {
level: node.level,
from_module: node.module.clone().map(Into::into),
submodule: alias.name.clone().into(),
});
}
// Look for imports `from __future__ import annotations`, ignore `as ...`
// We intentionally don't enforce the rules about location of `__future__`
// imports here, we assume the user's intent was to apply the `__future__`

View File

@@ -39,7 +39,9 @@ use crate::place::{
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::place::ScopedPlaceId;
use crate::semantic_index::scope::ScopeId;
use crate::semantic_index::{imported_modules, place_table, semantic_index};
use crate::semantic_index::{
imported_modules, imported_relative_submodules_of_stub_package, place_table, semantic_index,
};
use crate::suppression::check_suppressions;
use crate::types::bound_super::BoundSuperType;
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
@@ -889,13 +891,24 @@ impl<'db> Type<'db> {
known_class: KnownClass,
) -> Option<Specialization<'db>> {
let class_literal = known_class.try_to_class_literal(db)?;
self.specialization_of(db, Some(class_literal))
self.specialization_of(db, class_literal)
}
// If this type is a class instance, returns its specialization.
pub(crate) fn class_specialization(self, db: &'db dyn Db) -> Option<Specialization<'db>> {
self.specialization_of_optional(db, None)
}
// If the type is a specialized instance of the given class, returns the specialization.
//
// If no class is provided, returns the specialization of any class instance.
pub(crate) fn specialization_of(
self,
db: &'db dyn Db,
expected_class: ClassLiteral<'_>,
) -> Option<Specialization<'db>> {
self.specialization_of_optional(db, Some(expected_class))
}
fn specialization_of_optional(
self,
db: &'db dyn Db,
expected_class: Option<ClassLiteral<'_>>,
@@ -1212,22 +1225,28 @@ impl<'db> Type<'db> {
/// If the type is a union, filters union elements based on the provided predicate.
///
/// Otherwise, returns the type unchanged.
/// Otherwise, considers the type to be the sole inhabitant of a single-valued union,
/// and filters it, returning `Never` if the predicate returns `false`, or the type
/// unchanged if `true`.
pub(crate) fn filter_union(
self,
db: &'db dyn Db,
f: impl FnMut(&Type<'db>) -> bool,
mut f: impl FnMut(&Type<'db>) -> bool,
) -> Type<'db> {
if let Type::Union(union) = self {
union.filter(db, f)
} else {
} else if f(&self) {
self
} else {
Type::Never
}
}
/// If the type is a union, removes union elements that are disjoint from `target`.
///
/// Otherwise, returns the type unchanged.
/// Otherwise, considers the type to be the sole inhabitant of a single-valued union,
/// and filters it, returning `Never` if it is disjoint from `target`, or the type
/// unchanged if `true`.
pub(crate) fn filter_disjoint_elements(
self,
db: &'db dyn Db,
@@ -4159,6 +4178,14 @@ impl<'db> Type<'db> {
))
.into()
}
Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked))
if name == "satisfied_by_all_typevars" =>
{
Place::bound(Type::KnownBoundMethod(
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(tracked),
))
.into()
}
Type::ClassLiteral(class)
if name == "__get__" && class.is_known(db, KnownClass::FunctionType) =>
@@ -6921,6 +6948,7 @@ impl<'db> Type<'db> {
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
@@ -7072,7 +7100,8 @@ impl<'db> Type<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
@@ -10337,6 +10366,7 @@ pub enum KnownBoundMethodType<'db> {
ConstraintSetAlways,
ConstraintSetNever,
ConstraintSetImpliesSubtypeOf(TrackedConstraintSet<'db>),
ConstraintSetSatisfiedByAllTypeVars(TrackedConstraintSet<'db>),
}
pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
@@ -10364,7 +10394,8 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => {}
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {}
}
}
@@ -10432,6 +10463,10 @@ impl<'db> KnownBoundMethodType<'db> {
| (
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
)
| (
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
) => ConstraintSet::from(true),
(
@@ -10444,7 +10479,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_)
@@ -10454,7 +10490,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
) => ConstraintSet::from(false),
}
}
@@ -10507,6 +10544,10 @@ impl<'db> KnownBoundMethodType<'db> {
(
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(left_constraints),
KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(right_constraints),
)
| (
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(left_constraints),
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(right_constraints),
) => left_constraints
.constraints(db)
.iff(db, right_constraints.constraints(db)),
@@ -10521,7 +10562,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_)
@@ -10531,7 +10573,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
) => ConstraintSet::from(false),
}
}
@@ -10555,7 +10598,8 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => self,
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => self,
}
}
@@ -10571,7 +10615,10 @@ impl<'db> KnownBoundMethodType<'db> {
KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => KnownClass::ConstraintSet,
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
KnownClass::ConstraintSet
}
}
}
@@ -10710,6 +10757,19 @@ impl<'db> KnownBoundMethodType<'db> {
Some(KnownClass::ConstraintSet.to_instance(db)),
)))
}
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
Either::Right(std::iter::once(Signature::new(
Parameters::new([Parameter::keyword_only(Name::new_static("inferable"))
.type_form()
.with_annotated_type(UnionType::from_elements(
db,
[Type::homogeneous_tuple(db, Type::any()), Type::none(db)],
))
.with_default_type(Type::none(db))]),
Some(KnownClass::Bool.to_instance(db)),
)))
}
}
}
}
@@ -10830,11 +10890,68 @@ impl<'db> ModuleLiteralType<'db> {
self._importing_file(db)
}
/// Get the submodule attributes we believe to be defined on this module.
///
/// Note that `ModuleLiteralType` is per-importing-file, so this analysis
/// includes "imports the importing file has performed".
///
///
/// # Danger! Powerful Hammer!
///
/// These results immediately make the attribute always defined in the importing file,
/// shadowing any other attribute in the module with the same name, even if the
/// non-submodule-attribute is in fact always the one defined in practice.
///
/// Intuitively this means `available_submodule_attributes` "win all tie-breaks",
/// with the idea that if we're ever confused about complicated code then usually
/// the import is the thing people want in scope.
///
/// However this "always defined, always shadows" rule if applied too aggressively
/// creates VERY confusing conclusions that break perfectly reasonable code.
///
/// For instance, consider a package which has a `myfunc` submodule which defines a
/// `myfunc` function (a common idiom). If the package "re-exports" this function
/// (`from .myfunc import myfunc`), then at runtime in python
/// `from mypackage import myfunc` should import the function and not the submodule.
///
/// However, if we were to consider `from mypackage import myfunc` as introducing
/// the attribute `mypackage.myfunc` in `available_submodule_attributes`, we would
/// fail to ever resolve the function. This is because `available_submodule_attributes`
/// is *so early* and *so powerful* in our analysis that **this conclusion would be
/// used when actually resolving `from mypackage import myfunc`**!
///
/// This currently cannot be fixed by considering the actual symbols defined in `mypackage`,
/// because `available_submodule_attributes` is an *input* to that analysis.
///
/// We should therefore avoid marking something as an `available_submodule_attribute`
/// when the import could be importing a non-submodule (a function, class, or value).
///
///
/// # Rules
///
/// We have two rules for whether a submodule attribute is defined:
///
/// * If the importing file include `import x.y` then `x.y` is defined in the importing file.
/// This is an easy rule to justify because `import` can only ever import a module, and so
/// *should* shadow any non-submodule of the same name.
///
/// * If the module is an `__init__.pyi` for `mypackage`, and it contains a `from...import`
/// that normalizes to `from mypackage import submodule`, then `mypackage.submodule` is
/// defined in all files. This supports the `from . import submodule` idiom. Critically,
/// we do *not* allow `from mypackage.nested import submodule` to affect `mypackage`.
/// The idea here is that `from mypackage import submodule` *from mypackage itself* can
/// only ever reasonably be an import of a submodule. It doesn't make any sense to import
/// a function or class from yourself! (You *can* do it but... why? Don't? Please?)
fn available_submodule_attributes(&self, db: &'db dyn Db) -> impl Iterator<Item = Name> {
self.importing_file(db)
.into_iter()
.flat_map(|file| imported_modules(db, file))
.filter_map(|submodule_name| submodule_name.relative_to(self.module(db).name(db)))
.chain(
imported_relative_submodules_of_stub_package(db, self.module(db))
.iter()
.cloned(),
)
.filter_map(|relative_submodule| relative_submodule.components().next().map(Name::from))
}

View File

@@ -9,6 +9,7 @@ use std::fmt;
use itertools::{Either, Itertools};
use ruff_db::parsed::parsed_module;
use ruff_python_ast::name::Name;
use rustc_hash::FxHashSet;
use smallvec::{SmallVec, smallvec, smallvec_inline};
use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type};
@@ -34,10 +35,11 @@ use crate::types::generics::{
use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters};
use crate::types::tuple::{TupleLength, TupleType};
use crate::types::{
BoundMethodType, ClassLiteral, DataclassFlags, DataclassParams, FieldInstance,
KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, PropertyInstanceType,
SpecialFormType, TrackedConstraintSet, TypeAliasType, TypeContext, UnionBuilder, UnionType,
WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, todo_type,
BoundMethodType, BoundTypeVarIdentity, ClassLiteral, DataclassFlags, DataclassParams,
FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy,
NominalInstanceType, PropertyInstanceType, SpecialFormType, TrackedConstraintSet,
TypeAliasType, TypeContext, UnionBuilder, UnionType, WrapperDescriptorKind, enums, ide_support,
infer_isolated_expression, todo_type,
};
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion};
@@ -1174,6 +1176,42 @@ impl<'db> Bindings<'db> {
));
}
Type::KnownBoundMethod(
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(tracked),
) => {
let extract_inferable = |instance: &NominalInstanceType<'db>| {
if instance.has_known_class(db, KnownClass::NoneType) {
// Caller explicitly passed None, so no typevars are inferable.
return Some(FxHashSet::default());
}
instance
.tuple_spec(db)?
.fixed_elements()
.map(|ty| {
ty.as_typevar()
.map(|bound_typevar| bound_typevar.identity(db))
})
.collect()
};
let inferable = match overload.parameter_types() {
// Caller did not provide argument, so no typevars are inferable.
[None] => FxHashSet::default(),
[Some(Type::NominalInstance(instance))] => {
match extract_inferable(instance) {
Some(inferable) => inferable,
None => continue,
}
}
_ => continue,
};
let result = tracked
.constraints(db)
.satisfied_by_all_typevars(db, InferableTypeVars::One(&inferable));
overload.set_return_type(Type::BooleanLiteral(result));
}
Type::ClassLiteral(class) => match class.known(db) {
Some(KnownClass::Bool) => match overload.parameter_types() {
[Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)),
@@ -2680,9 +2718,25 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
return;
};
let return_with_tcx = self
.signature
.return_ty
.zip(self.call_expression_tcx.annotation);
self.inferable_typevars = generic_context.inferable_typevars(self.db);
let mut builder = SpecializationBuilder::new(self.db, self.inferable_typevars);
// Prefer the declared type of generic classes.
let preferred_type_mappings = return_with_tcx.and_then(|(return_ty, tcx)| {
let preferred_return_ty =
tcx.filter_union(self.db, |ty| ty.class_specialization(self.db).is_some());
let return_ty =
return_ty.filter_union(self.db, |ty| ty.class_specialization(self.db).is_some());
builder.infer(return_ty, preferred_return_ty).ok()?;
Some(builder.type_mappings().clone())
});
let parameters = self.signature.parameters();
for (argument_index, adjusted_argument_index, _, argument_type) in
self.enumerate_argument_types()
@@ -2695,9 +2749,21 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
continue;
};
if let Err(error) = builder.infer(
let filter = |declared_ty: BoundTypeVarIdentity<'_>, inferred_ty: Type<'_>| {
// Avoid widening the inferred type if it is already assignable to the
// preferred declared type.
preferred_type_mappings
.as_ref()
.and_then(|types| types.get(&declared_ty))
.is_none_or(|preferred_ty| {
!inferred_ty.is_assignable_to(self.db, *preferred_ty)
})
};
if let Err(error) = builder.infer_filter(
expected_type,
variadic_argument_type.unwrap_or(argument_type),
filter,
) {
self.errors.push(BindingError::SpecializationError {
error,
@@ -2707,15 +2773,14 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
}
}
// Build the specialization first without inferring the type context.
// Build the specialization first without inferring the complete type context.
let isolated_specialization = builder.build(generic_context, *self.call_expression_tcx);
let isolated_return_ty = self
.return_ty
.apply_specialization(self.db, isolated_specialization);
let mut try_infer_tcx = || {
let return_ty = self.signature.return_ty?;
let call_expression_tcx = self.call_expression_tcx.annotation?;
let (return_ty, call_expression_tcx) = return_with_tcx?;
// A type variable is not a useful type-context for expression inference, and applying it
// to the return type can lead to confusing unions in nested generic calls.
@@ -2724,7 +2789,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
}
// If the return type is already assignable to the annotated type, we can ignore the
// type context and prefer the narrower inferred type.
// rest of the type context and prefer the narrower inferred type.
if isolated_return_ty.is_assignable_to(self.db, call_expression_tcx) {
return None;
}
@@ -2733,7 +2798,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
// annotated assignment, to closer match the order of any unions written in the type annotation.
builder.infer(return_ty, call_expression_tcx).ok()?;
// Otherwise, build the specialization again after inferring the type context.
// Otherwise, build the specialization again after inferring the complete type context.
let specialization = builder.build(generic_context, *self.call_expression_tcx);
let return_ty = return_ty.apply_specialization(self.db, specialization);

View File

@@ -258,7 +258,7 @@ impl<'db> GenericAlias<'db> {
) -> Self {
let tcx = tcx
.annotation
.and_then(|ty| ty.specialization_of(db, Some(self.origin(db))))
.and_then(|ty| ty.specialization_of(db, self.origin(db)))
.map(|specialization| specialization.types(db))
.unwrap_or(&[]);
@@ -637,12 +637,17 @@ impl<'db> ClassType<'db> {
return true;
}
// Optimisation: if either class is `@final`, we only need to do one `is_subclass_of` call.
if self.is_final(db) {
return self.is_subclass_of(db, other);
return self
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| class.class_literal(db).0 == other.class_literal(db).0);
}
if other.is_final(db) {
return other.is_subclass_of(db, self);
return other
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| class.class_literal(db).0 == self.class_literal(db).0);
}
// Two disjoint bases can only coexist in an MRO if one is a subclass of the other.
@@ -2176,7 +2181,8 @@ impl<'db> ClassLiteral<'db> {
});
if member.is_undefined() {
if let Some(synthesized_member) = self.own_synthesized_member(db, specialization, name)
if let Some(synthesized_member) =
self.own_synthesized_member(db, specialization, inherited_generic_context, name)
{
return Member::definitely_declared(synthesized_member);
}
@@ -2192,6 +2198,7 @@ impl<'db> ClassLiteral<'db> {
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
inherited_generic_context: Option<GenericContext<'db>>,
name: &str,
) -> Option<Type<'db>> {
let dataclass_params = self.dataclass_params(db);
@@ -2320,7 +2327,7 @@ impl<'db> ClassLiteral<'db> {
let signature = match name {
"__new__" | "__init__" => Signature::new_generic(
self.inherited_generic_context(db),
inherited_generic_context.or_else(|| self.inherited_generic_context(db)),
Parameters::new(parameters),
return_ty,
),
@@ -2702,7 +2709,7 @@ impl<'db> ClassLiteral<'db> {
name: &str,
policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
if let Some(member) = self.own_synthesized_member(db, specialization, name) {
if let Some(member) = self.own_synthesized_member(db, specialization, None, name) {
Place::bound(member).into()
} else {
KnownClass::TypedDictFallback

View File

@@ -65,7 +65,10 @@ use salsa::plumbing::AsId;
use crate::Db;
use crate::types::generics::InferableTypeVars;
use crate::types::{BoundTypeVarInstance, IntersectionType, Type, TypeRelation, UnionType};
use crate::types::{
BoundTypeVarInstance, IntersectionType, Type, TypeRelation, TypeVarBoundOrConstraints,
UnionType,
};
/// An extension trait for building constraint sets from [`Option`] values.
pub(crate) trait OptionConstraintsExtension<T> {
@@ -256,6 +259,28 @@ impl<'db> ConstraintSet<'db> {
}
}
/// Returns whether this constraint set is satisfied by all of the typevars that it mentions.
///
/// Each typevar has a set of _valid specializations_, which is defined by any upper bound or
/// constraints that the typevar has.
///
/// Each typevar is also either _inferable_ or _non-inferable_. (You provide a list of the
/// `inferable` typevars; all others are considered non-inferable.) For an inferable typevar,
/// then there must be _some_ valid specialization that satisfies the constraint set. For a
/// non-inferable typevar, then _all_ valid specializations must satisfy it.
///
/// Note that we don't have to consider typevars that aren't mentioned in the constraint set,
/// since the constraint set cannot be affected by any typevars that it does not mention. That
/// means that those additional typevars trivially satisfy the constraint set, regardless of
/// whether they are inferable or not.
pub(crate) fn satisfied_by_all_typevars(
self,
db: &'db dyn Db,
inferable: InferableTypeVars<'_, 'db>,
) -> bool {
self.node.satisfied_by_all_typevars(db, inferable)
}
/// Updates this constraint set to hold the union of itself and another constraint set.
pub(crate) fn union(&mut self, db: &'db dyn Db, other: Self) -> Self {
self.node = self.node.or(db, other.node);
@@ -746,6 +771,13 @@ impl<'db> Node<'db> {
.or(db, self.negate(db).and(db, else_node))
}
fn satisfies(self, db: &'db dyn Db, other: Self) -> Self {
let simplified_self = self.simplify(db);
let implication = simplified_self.implies(db, other);
let (simplified, domain) = implication.simplify_and_domain(db);
simplified.and(db, domain)
}
fn when_subtype_of_given(
self,
db: &'db dyn Db,
@@ -767,10 +799,48 @@ impl<'db> Node<'db> {
_ => return lhs.when_subtype_of(db, rhs, inferable).node,
};
let simplified_self = self.simplify(db);
let implication = simplified_self.implies(db, constraint);
let (simplified, domain) = implication.simplify_and_domain(db);
simplified.and(db, domain)
self.satisfies(db, constraint)
}
fn satisfied_by_all_typevars(
self,
db: &'db dyn Db,
inferable: InferableTypeVars<'_, 'db>,
) -> bool {
match self {
Node::AlwaysTrue => return true,
Node::AlwaysFalse => return false,
Node::Interior(_) => {}
}
let mut typevars = FxHashSet::default();
self.for_each_constraint(db, &mut |constraint| {
typevars.insert(constraint.typevar(db));
});
for typevar in typevars {
// Determine which valid specializations of this typevar satisfy the constraint set.
let valid_specializations = typevar.valid_specializations(db).node;
let when_satisfied = valid_specializations
.satisfies(db, self)
.and(db, valid_specializations);
let satisfied = if typevar.is_inferable(db, inferable) {
// If the typevar is inferable, then we only need one valid specialization to
// satisfy the constraint set.
!when_satisfied.is_never_satisfied()
} else {
// If the typevar is non-inferable, then we need _all_ valid specializations to
// satisfy the constraint set.
when_satisfied
.iff(db, valid_specializations)
.is_always_satisfied(db)
};
if !satisfied {
return false;
}
}
true
}
/// Returns a new BDD that returns the same results as `self`, but with some inputs fixed to
@@ -1861,6 +1931,33 @@ impl<'db> SatisfiedClauses<'db> {
}
}
/// Returns a constraint set describing the valid specializations of a typevar.
impl<'db> BoundTypeVarInstance<'db> {
pub(crate) fn valid_specializations(self, db: &'db dyn Db) -> ConstraintSet<'db> {
match self.typevar(db).bound_or_constraints(db) {
None => ConstraintSet::from(true),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => ConstraintSet::constrain_typevar(
db,
self,
Type::Never,
bound,
TypeRelation::Assignability,
),
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
constraints.elements(db).iter().when_any(db, |constraint| {
ConstraintSet::constrain_typevar(
db,
self,
*constraint,
*constraint,
TypeRelation::Assignability,
)
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -572,10 +572,14 @@ declare_lint! {
// Added in #19763.
declare_lint! {
/// ## What it does
/// Checks for subscript accesses with invalid keys.
/// Checks for subscript accesses with invalid keys and `TypedDict` construction with an
/// unknown key.
///
/// ## Why is this bad?
/// Using an invalid key will raise a `KeyError` at runtime.
/// Subscripting with an invalid key will raise a `KeyError` at runtime.
///
/// Creating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is
/// `closed=true` it also violates the expectations of the type.
///
/// ## Examples
/// ```python
@@ -587,9 +591,13 @@ declare_lint! {
///
/// alice = Person(name="Alice", age=30)
/// alice["height"] # KeyError: 'height'
///
/// bob: Person = { "name": "Bob", "age": 30 } # typo!
///
/// carol = Person(name="Carol", age=25) # typo!
/// ```
pub(crate) static INVALID_KEY = {
summary: "detects invalid subscript accesses",
summary: "detects invalid subscript accesses or TypedDict literal keys",
status: LintStatus::stable("0.0.1-alpha.17"),
default_level: Level::Error,
}
@@ -2966,7 +2974,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
let typed_dict_name = typed_dict_ty.display(db);
let mut diagnostic = builder.into_diagnostic(format_args!(
"Invalid key access on TypedDict `{typed_dict_name}`",
"Invalid key for TypedDict `{typed_dict_name}`",
));
diagnostic.annotate(
@@ -2989,7 +2997,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
diagnostic
}
_ => builder.into_diagnostic(format_args!(
"TypedDict `{}` cannot be indexed with a key of type `{}`",
"Invalid key for TypedDict `{}` of type `{}`",
typed_dict_ty.display(db),
key_ty.display(db),
)),

View File

@@ -535,6 +535,9 @@ impl Display for DisplayRepresentation<'_> {
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)) => {
f.write_str("bound method `ConstraintSet.implies_subtype_of`")
}
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(
_,
)) => f.write_str("bound method `ConstraintSet.satisfied_by_all_typevars`"),
Type::WrapperDescriptor(kind) => {
let (method, object) = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"),

View File

@@ -1,4 +1,5 @@
use std::cell::RefCell;
use std::collections::hash_map::Entry;
use std::fmt::Display;
use itertools::Itertools;
@@ -1319,6 +1320,11 @@ impl<'db> SpecializationBuilder<'db> {
}
}
/// Returns the current set of type mappings for this specialization.
pub(crate) fn type_mappings(&self) -> &FxHashMap<BoundTypeVarIdentity<'db>, Type<'db>> {
&self.types
}
pub(crate) fn build(
&mut self,
generic_context: GenericContext<'db>,
@@ -1326,7 +1332,7 @@ impl<'db> SpecializationBuilder<'db> {
) -> Specialization<'db> {
let tcx_specialization = tcx
.annotation
.and_then(|annotation| annotation.specialization_of(self.db, None));
.and_then(|annotation| annotation.class_specialization(self.db));
let types =
(generic_context.variables_inner(self.db).iter()).map(|(identity, variable)| {
@@ -1349,19 +1355,43 @@ impl<'db> SpecializationBuilder<'db> {
generic_context.specialize_partial(self.db, types)
}
fn add_type_mapping(&mut self, bound_typevar: BoundTypeVarInstance<'db>, ty: Type<'db>) {
self.types
.entry(bound_typevar.identity(self.db))
.and_modify(|existing| {
*existing = UnionType::from_elements(self.db, [*existing, ty]);
})
.or_insert(ty);
fn add_type_mapping(
&mut self,
bound_typevar: BoundTypeVarInstance<'db>,
ty: Type<'db>,
filter: impl Fn(BoundTypeVarIdentity<'db>, Type<'db>) -> bool,
) {
let identity = bound_typevar.identity(self.db);
match self.types.entry(identity) {
Entry::Occupied(mut entry) => {
if filter(identity, ty) {
*entry.get_mut() = UnionType::from_elements(self.db, [*entry.get(), ty]);
}
}
Entry::Vacant(entry) => {
entry.insert(ty);
}
}
}
/// Infer type mappings for the specialization based on a given type and its declared type.
pub(crate) fn infer(
&mut self,
formal: Type<'db>,
actual: Type<'db>,
) -> Result<(), SpecializationError<'db>> {
self.infer_filter(formal, actual, |_, _| true)
}
/// Infer type mappings for the specialization based on a given type and its declared type.
///
/// The filter predicate is provided with a type variable and the type being mapped to it. Type
/// mappings to which the predicate returns `false` will be ignored.
pub(crate) fn infer_filter(
&mut self,
formal: Type<'db>,
actual: Type<'db>,
filter: impl Fn(BoundTypeVarIdentity<'db>, Type<'db>) -> bool,
) -> Result<(), SpecializationError<'db>> {
if formal == actual {
return Ok(());
@@ -1442,7 +1472,7 @@ impl<'db> SpecializationBuilder<'db> {
if remaining_actual.is_never() {
return Ok(());
}
self.add_type_mapping(*formal_bound_typevar, remaining_actual);
self.add_type_mapping(*formal_bound_typevar, remaining_actual, filter);
}
(Type::Union(formal), _) => {
// Second, if the formal is a union, and precisely one union element _is_ a typevar (not
@@ -1452,7 +1482,7 @@ impl<'db> SpecializationBuilder<'db> {
let bound_typevars =
(formal.elements(self.db).iter()).filter_map(|ty| ty.as_typevar());
if let Ok(bound_typevar) = bound_typevars.exactly_one() {
self.add_type_mapping(bound_typevar, actual);
self.add_type_mapping(bound_typevar, actual, filter);
}
}
@@ -1480,15 +1510,23 @@ impl<'db> SpecializationBuilder<'db> {
argument: ty,
});
}
self.add_type_mapping(bound_typevar, ty);
self.add_type_mapping(bound_typevar, ty, filter);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
// Prefer an exact match first.
for constraint in constraints.elements(self.db) {
if ty == *constraint {
self.add_type_mapping(bound_typevar, ty, filter);
return Ok(());
}
}
for constraint in constraints.elements(self.db) {
if ty
.when_assignable_to(self.db, *constraint, self.inferable)
.is_always_satisfied(self.db)
{
self.add_type_mapping(bound_typevar, *constraint);
self.add_type_mapping(bound_typevar, *constraint, filter);
return Ok(());
}
}
@@ -1498,7 +1536,7 @@ impl<'db> SpecializationBuilder<'db> {
});
}
_ => {
self.add_type_mapping(bound_typevar, ty);
self.add_type_mapping(bound_typevar, ty, filter);
}
}
}

View File

@@ -6260,7 +6260,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let inferred_elt_ty = self.get_or_infer_expression(elt, elt_tcx);
// Simplify the inference based on the declared type of the element.
// Avoid widening the inferred type if it is already assignable to the preferred
// declared type.
if let Some(elt_tcx) = elt_tcx.annotation {
if inferred_elt_ty.is_assignable_to(self.db(), elt_tcx) {
continue;

View File

@@ -67,6 +67,16 @@ class ConstraintSet:
.. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
"""
def satisfied_by_all_typevars(
self, *, inferable: tuple[Any, ...] | None = None
) -> bool:
"""
Returns whether this constraint set is satisfied by all of the typevars
that it mentions. You must provide a tuple of the typevars that should
be considered `inferable`. All other typevars mentioned in the
constraint set will be considered non-inferable.
"""
def __bool__(self) -> bool: ...
def __eq__(self, other: ConstraintSet) -> bool: ...
def __ne__(self, other: ConstraintSet) -> bool: ...

View File

@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.14.2-alpine
name: ghcr.io/astral-sh/ruff:0.14.3-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.2
rev: v0.14.3
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.2
rev: v0.14.3
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.2
rev: v0.14.3
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ project_root="$(dirname "$script_root")"
echo "Updating metadata with rooster..."
cd "$project_root"
uvx --python 3.12 --isolated -- \
rooster@0.1.0 release "$@"
rooster@0.1.1 release "$@"
echo "Updating lockfile..."
cargo update -p ruff

4
ty.schema.json generated
View File

@@ -584,8 +584,8 @@
]
},
"invalid-key": {
"title": "detects invalid subscript accesses",
"description": "## What it does\nChecks for subscript accesses with invalid keys.\n\n## Why is this bad?\nUsing an invalid key will raise a `KeyError` at runtime.\n\n## Examples\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice = Person(name=\"Alice\", age=30)\nalice[\"height\"] # KeyError: 'height'\n```",
"title": "detects invalid subscript accesses or TypedDict literal keys",
"description": "## What it does\nChecks for subscript accesses with invalid keys and `TypedDict` construction with an\nunknown key.\n\n## Why is this bad?\nSubscripting with an invalid key will raise a `KeyError` at runtime.\n\nCreating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is\n`closed=true` it also violates the expectations of the type.\n\n## Examples\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice = Person(name=\"Alice\", age=30)\nalice[\"height\"] # KeyError: 'height'\n\nbob: Person = { \"name\": \"Bob\", \"age\": 30 } # typo!\n\ncarol = Person(name=\"Carol\", age=25) # typo!\n```",
"default": "error",
"oneOf": [
{