Compare commits
27 Commits
cjm/tvassi
...
micha/fixp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05854142e7 | ||
|
|
33eb008fb6 | ||
|
|
04d4febfc4 | ||
|
|
d566636ca5 | ||
|
|
51cef5a72b | ||
|
|
2cf5cba7ff | ||
|
|
ce0800fccf | ||
|
|
d03a7069ad | ||
|
|
f5096f2050 | ||
|
|
895b6161a6 | ||
|
|
74fe7982ba | ||
|
|
51386b3c7a | ||
|
|
51e2effd2d | ||
|
|
82d31a6014 | ||
|
|
78054824c0 | ||
|
|
c6f4929cdc | ||
|
|
2ec0d7e072 | ||
|
|
ad658f4d68 | ||
|
|
3dedd70a92 | ||
|
|
fab862c8cd | ||
|
|
0d9b6a0975 | ||
|
|
c5e299e796 | ||
|
|
c504001b32 | ||
|
|
04457f99b6 | ||
|
|
a33d0d4bf4 | ||
|
|
443f62e98d | ||
|
|
a2e9a7732a |
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Report an issue with ty
|
||||
url: https://github.com/astral-sh/ty/issues/new/choose
|
||||
about: Please report issues for our type checker ty in the ty repository.
|
||||
- name: Documentation
|
||||
url: https://docs.astral.sh/ruff
|
||||
about: Please consult the documentation before creating an issue.
|
||||
|
||||
@@ -366,6 +366,15 @@ uvx --from ./python/ruff-ecosystem ruff-ecosystem format ruff "./target/debug/ru
|
||||
|
||||
See the [ruff-ecosystem package](https://github.com/astral-sh/ruff/tree/main/python/ruff-ecosystem) for more details.
|
||||
|
||||
## Upgrading Rust
|
||||
|
||||
1. Change the `channel` in `./rust-toolchain.toml` to the new Rust version (`<latest>`)
|
||||
1. Change the `rust-version` in the `./Cargo.toml` to `<latest> - 2` (e.g. 1.84 if the latest is 1.86)
|
||||
1. Run `cargo clippy --fix --allow-dirty --allow-staged` to fix new clippy warnings
|
||||
1. Create and merge the PR
|
||||
1. Bump the Rust version in Ruff's conda forge recipe. See [this PR](https://github.com/conda-forge/ruff-feedstock/pull/266) for an example.
|
||||
1. Enjoy the new Rust version!
|
||||
|
||||
## Benchmarking and Profiling
|
||||
|
||||
We have several ways of benchmarking and profiling Ruff:
|
||||
|
||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -3237,7 +3237,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b2b82bccdbef3e7ce7f302c52f43a0c98ac7177a#b2b82bccdbef3e7ce7f302c52f43a0c98ac7177a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef93d3830ffb8dc621fef1ba7b234ccef9dc3639#ef93d3830ffb8dc621fef1ba7b234ccef9dc3639"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3260,12 +3260,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b2b82bccdbef3e7ce7f302c52f43a0c98ac7177a#b2b82bccdbef3e7ce7f302c52f43a0c98ac7177a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef93d3830ffb8dc621fef1ba7b234ccef9dc3639#ef93d3830ffb8dc621fef1ba7b234ccef9dc3639"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b2b82bccdbef3e7ce7f302c52f43a0c98ac7177a#b2b82bccdbef3e7ce7f302c52f43a0c98ac7177a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef93d3830ffb8dc621fef1ba7b234ccef9dc3639#ef93d3830ffb8dc621fef1ba7b234ccef9dc3639"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3955,6 +3955,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
"clap",
|
||||
"clap_complete_command",
|
||||
"colored 3.0.0",
|
||||
"countme",
|
||||
"crossbeam",
|
||||
|
||||
@@ -124,7 +124,7 @@ rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "b2b82bccdbef3e7ce7f302c52f43a0c98ac7177a" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "ef93d3830ffb8dc621fef1ba7b234ccef9dc3639" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
@@ -232,6 +232,15 @@ impl Diagnostic {
|
||||
pub fn primary_tags(&self) -> Option<&[DiagnosticTag]> {
|
||||
self.primary_annotation().map(|ann| ann.tags.as_slice())
|
||||
}
|
||||
|
||||
/// Returns a key that can be used to sort two diagnostics into the canonical order
|
||||
/// in which they should appear when rendered.
|
||||
pub fn rendering_sort_key<'a>(&'a self, db: &'a dyn Db) -> impl Ord + 'a {
|
||||
RenderingSortKey {
|
||||
db,
|
||||
diagnostic: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
@@ -243,6 +252,64 @@ struct DiagnosticInner {
|
||||
subs: Vec<SubDiagnostic>,
|
||||
}
|
||||
|
||||
struct RenderingSortKey<'a> {
|
||||
db: &'a dyn Db,
|
||||
diagnostic: &'a Diagnostic,
|
||||
}
|
||||
|
||||
impl Ord for RenderingSortKey<'_> {
|
||||
// We sort diagnostics in a way that keeps them in source order
|
||||
// and grouped by file. After that, we fall back to severity
|
||||
// (with fatal messages sorting before info messages) and then
|
||||
// finally the diagnostic ID.
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
if let (Some(span1), Some(span2)) = (
|
||||
self.diagnostic.primary_span(),
|
||||
other.diagnostic.primary_span(),
|
||||
) {
|
||||
let order = span1
|
||||
.file()
|
||||
.path(self.db)
|
||||
.as_str()
|
||||
.cmp(span2.file().path(self.db).as_str());
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
|
||||
if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) {
|
||||
let order = range1.start().cmp(&range2.start());
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reverse so that, e.g., Fatal sorts before Info.
|
||||
let order = self
|
||||
.diagnostic
|
||||
.severity()
|
||||
.cmp(&other.diagnostic.severity())
|
||||
.reverse();
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
self.diagnostic.id().cmp(&other.diagnostic.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for RenderingSortKey<'_> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RenderingSortKey<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RenderingSortKey<'_> {}
|
||||
|
||||
/// A collection of information subservient to a diagnostic.
|
||||
///
|
||||
/// A sub-diagnostic is always rendered after the parent diagnostic it is
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn max_parallelism() -> NonZeroUsize {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::files::Files;
|
||||
use crate::system::TestSystem;
|
||||
@@ -69,6 +69,8 @@ mod tests {
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
use crate::Db;
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
/// Database that can be used for testing.
|
||||
///
|
||||
/// Uses an in memory filesystem and it stubs out the vendored files by default.
|
||||
@@ -79,36 +81,37 @@ mod tests {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
events: Events,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
let events = Events::default();
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
tracing::trace!("event: {:?}", event);
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: VendoredFileSystem::default(),
|
||||
events: std::sync::Arc::default(),
|
||||
events,
|
||||
files: Files::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Empties the internal store of salsa events that have been emitted,
|
||||
/// and returns them as a `Vec` (equivalent to [`std::mem::take`]).
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are pending database snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events)
|
||||
.expect("expected no pending salsa database snapshots.");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
std::mem::take(inner.get_mut().unwrap())
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
/// Clears the emitted salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are pending database snapshots.
|
||||
pub(crate) fn clear_salsa_events(&mut self) {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
@@ -148,12 +151,5 @@ mod tests {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {:?}", event);
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
impl salsa::Database for TestDb {}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,4 @@ impl Db for ModuleDb {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for ModuleDb {
|
||||
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
|
||||
}
|
||||
impl salsa::Database for ModuleDb {}
|
||||
|
||||
@@ -84,3 +84,9 @@ str(
|
||||
'''Lorem
|
||||
ipsum''' # Comment
|
||||
).foo
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17606
|
||||
bool(True)and None
|
||||
int(1)and None
|
||||
float(1.)and None
|
||||
bool(True)and()
|
||||
|
||||
@@ -15,6 +15,34 @@ use ruff_python_ast::PythonVersion;
|
||||
/// Dunder names are not meant to be called explicitly and, in most cases, can
|
||||
/// be replaced with builtins or operators.
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This fix is always unsafe. When replacing dunder method calls with operators
|
||||
/// or builtins, the behavior can change in the following ways:
|
||||
///
|
||||
/// 1. Types may implement only a subset of related dunder methods. Calling a
|
||||
/// missing dunder method directly returns `NotImplemented`, but using the
|
||||
/// equivalent operator raises a `TypeError`.
|
||||
/// ```python
|
||||
/// class C: pass
|
||||
/// c = C()
|
||||
/// c.__gt__(1) # before fix: NotImplemented
|
||||
/// c > 1 # after fix: raises TypeError
|
||||
/// ```
|
||||
/// 2. Instance-assigned dunder methods are ignored by operators and builtins.
|
||||
/// ```python
|
||||
/// class C: pass
|
||||
/// c = C()
|
||||
/// c.__bool__ = lambda: False
|
||||
/// c.__bool__() # before fix: False
|
||||
/// bool(c) # after fix: True
|
||||
/// ```
|
||||
///
|
||||
/// 3. Even with built-in types, behavior can differ.
|
||||
/// ```python
|
||||
/// (1).__gt__(1.0) # before fix: NotImplemented
|
||||
/// 1 > 1.0 # after fix: False
|
||||
/// ```
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// three = (3.0).__str__()
|
||||
|
||||
@@ -161,13 +161,14 @@ pub(crate) fn native_literals(
|
||||
keywords,
|
||||
range: _,
|
||||
},
|
||||
range: _,
|
||||
range: call_range,
|
||||
} = call;
|
||||
|
||||
if !keywords.is_empty() || args.len() > 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let tokens = checker.tokens();
|
||||
let semantic = checker.semantic();
|
||||
|
||||
let Some(builtin) = semantic.resolve_builtin_symbol(func) else {
|
||||
@@ -244,7 +245,20 @@ pub(crate) fn native_literals(
|
||||
|
||||
let arg_code = checker.locator().slice(arg);
|
||||
|
||||
let content = match (parent_expr, literal_type, has_unary_op) {
|
||||
let mut needs_space = false;
|
||||
// Look for the `Rpar` token of the call expression and check if there is a keyword token right
|
||||
// next to it without any space separating them. Without this check, the fix for this
|
||||
// rule would create a syntax error.
|
||||
// Ex) `bool(True)and None` no space between `)` and the keyword `and`.
|
||||
//
|
||||
// Subtract 1 from the end of the range to include `Rpar` token in the slice.
|
||||
if let [paren_token, next_token, ..] = tokens.after(call_range.sub_end(1.into()).end())
|
||||
{
|
||||
needs_space = next_token.kind().is_keyword()
|
||||
&& paren_token.range().end() == next_token.range().start();
|
||||
}
|
||||
|
||||
let mut content = match (parent_expr, literal_type, has_unary_op) {
|
||||
// Expressions including newlines must be parenthesised to be valid syntax
|
||||
(_, _, true) if find_newline(arg_code).is_some() => format!("({arg_code})"),
|
||||
|
||||
@@ -265,6 +279,10 @@ pub(crate) fn native_literals(
|
||||
_ => arg_code.to_string(),
|
||||
};
|
||||
|
||||
if needs_space {
|
||||
content.push(' ');
|
||||
}
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(call.range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -602,6 +602,8 @@ UP018.py:83:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
|
||||
85 | | ipsum''' # Comment
|
||||
86 | | ).foo
|
||||
| |_^ UP018
|
||||
87 |
|
||||
88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
|
|
||||
= help: Replace with string literal
|
||||
|
||||
@@ -615,3 +617,80 @@ UP018.py:83:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
|
||||
86 |-).foo
|
||||
83 |+'''Lorem
|
||||
84 |+ ipsum'''.foo
|
||||
87 85 |
|
||||
88 86 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 87 | bool(True)and None
|
||||
|
||||
UP018.py:89:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal)
|
||||
|
|
||||
88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 | bool(True)and None
|
||||
| ^^^^^^^^^^ UP018
|
||||
90 | int(1)and None
|
||||
91 | float(1.)and None
|
||||
|
|
||||
= help: Replace with boolean literal
|
||||
|
||||
ℹ Safe fix
|
||||
86 86 | ).foo
|
||||
87 87 |
|
||||
88 88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 |-bool(True)and None
|
||||
89 |+True and None
|
||||
90 90 | int(1)and None
|
||||
91 91 | float(1.)and None
|
||||
92 92 | bool(True)and()
|
||||
|
||||
UP018.py:90:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
|
|
||||
88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 | bool(True)and None
|
||||
90 | int(1)and None
|
||||
| ^^^^^^ UP018
|
||||
91 | float(1.)and None
|
||||
92 | bool(True)and()
|
||||
|
|
||||
= help: Replace with integer literal
|
||||
|
||||
ℹ Safe fix
|
||||
87 87 |
|
||||
88 88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 89 | bool(True)and None
|
||||
90 |-int(1)and None
|
||||
90 |+1 and None
|
||||
91 91 | float(1.)and None
|
||||
92 92 | bool(True)and()
|
||||
|
||||
UP018.py:91:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
|
|
||||
89 | bool(True)and None
|
||||
90 | int(1)and None
|
||||
91 | float(1.)and None
|
||||
| ^^^^^^^^^ UP018
|
||||
92 | bool(True)and()
|
||||
|
|
||||
= help: Replace with float literal
|
||||
|
||||
ℹ Safe fix
|
||||
88 88 | # https://github.com/astral-sh/ruff/issues/17606
|
||||
89 89 | bool(True)and None
|
||||
90 90 | int(1)and None
|
||||
91 |-float(1.)and None
|
||||
91 |+1. and None
|
||||
92 92 | bool(True)and()
|
||||
|
||||
UP018.py:92:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal)
|
||||
|
|
||||
90 | int(1)and None
|
||||
91 | float(1.)and None
|
||||
92 | bool(True)and()
|
||||
| ^^^^^^^^^^ UP018
|
||||
|
|
||||
= help: Replace with boolean literal
|
||||
|
||||
ℹ Safe fix
|
||||
89 89 | bool(True)and None
|
||||
90 90 | int(1)and None
|
||||
91 91 | float(1.)and None
|
||||
92 |-bool(True)and()
|
||||
92 |+True and()
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
sum(x for x in range(10), 5)
|
||||
total(1, 2, x for x in range(5), 6)
|
||||
sum(x for x in range(10),)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
zip((x for x in range(10)), (y for y in range(10)))
|
||||
sum(x for x in range(10))
|
||||
sum((x for x in range(10)),)
|
||||
|
||||
@@ -661,117 +661,120 @@ impl<'src> Parser<'src> {
|
||||
let mut seen_keyword_argument = false; // foo = 1
|
||||
let mut seen_keyword_unpacking = false; // **foo
|
||||
|
||||
self.parse_comma_separated_list(RecoveryContextKind::Arguments, |parser| {
|
||||
let argument_start = parser.node_start();
|
||||
if parser.eat(TokenKind::DoubleStar) {
|
||||
let value = parser.parse_conditional_expression_or_higher();
|
||||
|
||||
keywords.push(ast::Keyword {
|
||||
arg: None,
|
||||
value: value.expr,
|
||||
range: parser.node_range(argument_start),
|
||||
});
|
||||
|
||||
seen_keyword_unpacking = true;
|
||||
} else {
|
||||
let start = parser.node_start();
|
||||
let mut parsed_expr = parser
|
||||
.parse_named_expression_or_higher(ExpressionContext::starred_conditional());
|
||||
|
||||
match parser.current_token_kind() {
|
||||
TokenKind::Async | TokenKind::For => {
|
||||
if parsed_expr.is_unparenthesized_starred_expr() {
|
||||
parser.add_error(
|
||||
ParseErrorType::IterableUnpackingInComprehension,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
|
||||
parsed_expr = Expr::Generator(parser.parse_generator_expression(
|
||||
parsed_expr.expr,
|
||||
start,
|
||||
Parenthesized::No,
|
||||
))
|
||||
.into();
|
||||
}
|
||||
_ => {
|
||||
if seen_keyword_unpacking && parsed_expr.is_unparenthesized_starred_expr() {
|
||||
parser.add_error(
|
||||
ParseErrorType::InvalidArgumentUnpackingOrder,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let arg_range = parser.node_range(start);
|
||||
if parser.eat(TokenKind::Equal) {
|
||||
seen_keyword_argument = true;
|
||||
let arg = if let ParsedExpr {
|
||||
expr: Expr::Name(ident_expr),
|
||||
is_parenthesized,
|
||||
} = parsed_expr
|
||||
{
|
||||
// test_ok parenthesized_kwarg_py37
|
||||
// # parse_options: {"target-version": "3.7"}
|
||||
// f((a)=1)
|
||||
|
||||
// test_err parenthesized_kwarg_py38
|
||||
// # parse_options: {"target-version": "3.8"}
|
||||
// f((a)=1)
|
||||
// f((a) = 1)
|
||||
// f( ( a ) = 1)
|
||||
|
||||
if is_parenthesized {
|
||||
parser.add_unsupported_syntax_error(
|
||||
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName,
|
||||
arg_range,
|
||||
);
|
||||
}
|
||||
|
||||
ast::Identifier {
|
||||
id: ident_expr.id,
|
||||
range: ident_expr.range,
|
||||
}
|
||||
} else {
|
||||
// TODO(dhruvmanila): Parser shouldn't drop the `parsed_expr` if it's
|
||||
// not a name expression. We could add the expression into `args` but
|
||||
// that means the error is a missing comma instead.
|
||||
parser.add_error(
|
||||
ParseErrorType::OtherError("Expected a parameter name".to_string()),
|
||||
&parsed_expr,
|
||||
);
|
||||
ast::Identifier {
|
||||
id: Name::empty(),
|
||||
range: parsed_expr.range(),
|
||||
}
|
||||
};
|
||||
|
||||
let has_trailing_comma =
|
||||
self.parse_comma_separated_list(RecoveryContextKind::Arguments, |parser| {
|
||||
let argument_start = parser.node_start();
|
||||
if parser.eat(TokenKind::DoubleStar) {
|
||||
let value = parser.parse_conditional_expression_or_higher();
|
||||
|
||||
keywords.push(ast::Keyword {
|
||||
arg: Some(arg),
|
||||
arg: None,
|
||||
value: value.expr,
|
||||
range: parser.node_range(argument_start),
|
||||
});
|
||||
|
||||
seen_keyword_unpacking = true;
|
||||
} else {
|
||||
if !parsed_expr.is_unparenthesized_starred_expr() {
|
||||
if seen_keyword_unpacking {
|
||||
parser.add_error(
|
||||
ParseErrorType::PositionalAfterKeywordUnpacking,
|
||||
&parsed_expr,
|
||||
);
|
||||
} else if seen_keyword_argument {
|
||||
parser.add_error(
|
||||
ParseErrorType::PositionalAfterKeywordArgument,
|
||||
&parsed_expr,
|
||||
);
|
||||
let start = parser.node_start();
|
||||
let mut parsed_expr = parser
|
||||
.parse_named_expression_or_higher(ExpressionContext::starred_conditional());
|
||||
|
||||
match parser.current_token_kind() {
|
||||
TokenKind::Async | TokenKind::For => {
|
||||
if parsed_expr.is_unparenthesized_starred_expr() {
|
||||
parser.add_error(
|
||||
ParseErrorType::IterableUnpackingInComprehension,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
|
||||
parsed_expr = Expr::Generator(parser.parse_generator_expression(
|
||||
parsed_expr.expr,
|
||||
start,
|
||||
Parenthesized::No,
|
||||
))
|
||||
.into();
|
||||
}
|
||||
_ => {
|
||||
if seen_keyword_unpacking
|
||||
&& parsed_expr.is_unparenthesized_starred_expr()
|
||||
{
|
||||
parser.add_error(
|
||||
ParseErrorType::InvalidArgumentUnpackingOrder,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
args.push(parsed_expr.expr);
|
||||
|
||||
let arg_range = parser.node_range(start);
|
||||
if parser.eat(TokenKind::Equal) {
|
||||
seen_keyword_argument = true;
|
||||
let arg = if let ParsedExpr {
|
||||
expr: Expr::Name(ident_expr),
|
||||
is_parenthesized,
|
||||
} = parsed_expr
|
||||
{
|
||||
// test_ok parenthesized_kwarg_py37
|
||||
// # parse_options: {"target-version": "3.7"}
|
||||
// f((a)=1)
|
||||
|
||||
// test_err parenthesized_kwarg_py38
|
||||
// # parse_options: {"target-version": "3.8"}
|
||||
// f((a)=1)
|
||||
// f((a) = 1)
|
||||
// f( ( a ) = 1)
|
||||
|
||||
if is_parenthesized {
|
||||
parser.add_unsupported_syntax_error(
|
||||
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName,
|
||||
arg_range,
|
||||
);
|
||||
}
|
||||
|
||||
ast::Identifier {
|
||||
id: ident_expr.id,
|
||||
range: ident_expr.range,
|
||||
}
|
||||
} else {
|
||||
// TODO(dhruvmanila): Parser shouldn't drop the `parsed_expr` if it's
|
||||
// not a name expression. We could add the expression into `args` but
|
||||
// that means the error is a missing comma instead.
|
||||
parser.add_error(
|
||||
ParseErrorType::OtherError("Expected a parameter name".to_string()),
|
||||
&parsed_expr,
|
||||
);
|
||||
ast::Identifier {
|
||||
id: Name::empty(),
|
||||
range: parsed_expr.range(),
|
||||
}
|
||||
};
|
||||
|
||||
let value = parser.parse_conditional_expression_or_higher();
|
||||
|
||||
keywords.push(ast::Keyword {
|
||||
arg: Some(arg),
|
||||
value: value.expr,
|
||||
range: parser.node_range(argument_start),
|
||||
});
|
||||
} else {
|
||||
if !parsed_expr.is_unparenthesized_starred_expr() {
|
||||
if seen_keyword_unpacking {
|
||||
parser.add_error(
|
||||
ParseErrorType::PositionalAfterKeywordUnpacking,
|
||||
&parsed_expr,
|
||||
);
|
||||
} else if seen_keyword_argument {
|
||||
parser.add_error(
|
||||
ParseErrorType::PositionalAfterKeywordArgument,
|
||||
&parsed_expr,
|
||||
);
|
||||
}
|
||||
}
|
||||
args.push(parsed_expr.expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.expect(TokenKind::Rpar);
|
||||
|
||||
@@ -781,7 +784,7 @@ impl<'src> Parser<'src> {
|
||||
keywords: keywords.into_boxed_slice(),
|
||||
};
|
||||
|
||||
self.validate_arguments(&arguments);
|
||||
self.validate_arguments(&arguments, has_trailing_comma);
|
||||
|
||||
arguments
|
||||
}
|
||||
@@ -2521,9 +2524,9 @@ impl<'src> Parser<'src> {
|
||||
|
||||
/// Performs the following validations on the function call arguments:
|
||||
/// 1. There aren't any duplicate keyword argument
|
||||
/// 2. If there are more than one argument (positional or keyword), all generator expressions
|
||||
/// present should be parenthesized.
|
||||
fn validate_arguments(&mut self, arguments: &ast::Arguments) {
|
||||
/// 2. If there are more than one argument (positional or keyword) or a single argument with a
|
||||
/// trailing comma, all generator expressions present should be parenthesized.
|
||||
fn validate_arguments(&mut self, arguments: &ast::Arguments, has_trailing_comma: bool) {
|
||||
let mut all_arg_names =
|
||||
FxHashSet::with_capacity_and_hasher(arguments.keywords.len(), FxBuildHasher);
|
||||
|
||||
@@ -2541,7 +2544,7 @@ impl<'src> Parser<'src> {
|
||||
}
|
||||
}
|
||||
|
||||
if arguments.len() > 1 {
|
||||
if has_trailing_comma || arguments.len() > 1 {
|
||||
for arg in &*arguments.args {
|
||||
if let Some(ast::ExprGenerator {
|
||||
range,
|
||||
@@ -2550,11 +2553,14 @@ impl<'src> Parser<'src> {
|
||||
}) = arg.as_generator_expr()
|
||||
{
|
||||
// test_ok args_unparenthesized_generator
|
||||
// zip((x for x in range(10)), (y for y in range(10)))
|
||||
// sum(x for x in range(10))
|
||||
// sum((x for x in range(10)),)
|
||||
|
||||
// test_err args_unparenthesized_generator
|
||||
// sum(x for x in range(10), 5)
|
||||
// total(1, 2, x for x in range(5), 6)
|
||||
// sum(x for x in range(10),)
|
||||
self.add_error(ParseErrorType::UnparenthesizedGeneratorExpression, range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,17 +539,19 @@ impl<'src> Parser<'src> {
|
||||
}
|
||||
|
||||
/// Parses a comma separated list of elements where each element is parsed
|
||||
/// sing the given `parse_element` function.
|
||||
/// using the given `parse_element` function.
|
||||
///
|
||||
/// The difference between this function and `parse_comma_separated_list_into_vec`
|
||||
/// is that this function does not return the parsed elements. Instead, it is the
|
||||
/// caller's responsibility to handle the parsed elements. This is the reason
|
||||
/// that the `parse_element` parameter is bound to [`FnMut`] instead of [`Fn`].
|
||||
///
|
||||
/// Returns `true` if there is a trailing comma present.
|
||||
fn parse_comma_separated_list(
|
||||
&mut self,
|
||||
recovery_context_kind: RecoveryContextKind,
|
||||
mut parse_element: impl FnMut(&mut Parser<'src>),
|
||||
) {
|
||||
) -> bool {
|
||||
let mut progress = ParserProgress::default();
|
||||
|
||||
let saved_context = self.recovery_context;
|
||||
@@ -659,6 +661,8 @@ impl<'src> Parser<'src> {
|
||||
}
|
||||
|
||||
self.recovery_context = saved_context;
|
||||
|
||||
trailing_comma_range.is_some()
|
||||
}
|
||||
|
||||
#[cold]
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/inline/err/args_unparenthesized_generator.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## AST
|
||||
|
||||
```
|
||||
Module(
|
||||
ModModule {
|
||||
range: 0..65,
|
||||
range: 0..92,
|
||||
body: [
|
||||
Expr(
|
||||
StmtExpr {
|
||||
@@ -194,6 +193,82 @@ Module(
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 65..91,
|
||||
value: Call(
|
||||
ExprCall {
|
||||
range: 65..91,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 65..68,
|
||||
id: Name("sum"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 68..91,
|
||||
args: [
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 69..89,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 69..70,
|
||||
id: Name("x"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 71..89,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 75..76,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 80..89,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 80..85,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 85..89,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 86..88,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
parenthesized: false,
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -204,6 +279,7 @@ Module(
|
||||
1 | sum(x for x in range(10), 5)
|
||||
| ^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here
|
||||
2 | total(1, 2, x for x in range(5), 6)
|
||||
3 | sum(x for x in range(10),)
|
||||
|
|
||||
|
||||
|
||||
@@ -211,4 +287,13 @@ Module(
|
||||
1 | sum(x for x in range(10), 5)
|
||||
2 | total(1, 2, x for x in range(5), 6)
|
||||
| ^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here
|
||||
3 | sum(x for x in range(10),)
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
1 | sum(x for x in range(10), 5)
|
||||
2 | total(1, 2, x for x in range(5), 6)
|
||||
3 | sum(x for x in range(10),)
|
||||
| ^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here
|
||||
|
|
||||
|
||||
@@ -1,67 +1,195 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/inline/ok/args_unparenthesized_generator.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## AST
|
||||
|
||||
```
|
||||
Module(
|
||||
ModModule {
|
||||
range: 0..26,
|
||||
range: 0..107,
|
||||
body: [
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..25,
|
||||
range: 0..51,
|
||||
value: Call(
|
||||
ExprCall {
|
||||
range: 0..25,
|
||||
range: 0..51,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 0..3,
|
||||
id: Name("sum"),
|
||||
id: Name("zip"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 3..25,
|
||||
range: 3..51,
|
||||
args: [
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 4..24,
|
||||
range: 4..26,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 4..5,
|
||||
range: 5..6,
|
||||
id: Name("x"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 6..24,
|
||||
range: 7..25,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 10..11,
|
||||
range: 11..12,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 15..24,
|
||||
range: 16..25,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 15..20,
|
||||
range: 16..21,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 20..24,
|
||||
range: 21..25,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 21..23,
|
||||
range: 22..24,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
parenthesized: true,
|
||||
},
|
||||
),
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 28..50,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 29..30,
|
||||
id: Name("y"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 31..49,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 35..36,
|
||||
id: Name("y"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 40..49,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 40..45,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 45..49,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 46..48,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
parenthesized: true,
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 52..77,
|
||||
value: Call(
|
||||
ExprCall {
|
||||
range: 52..77,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 52..55,
|
||||
id: Name("sum"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 55..77,
|
||||
args: [
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 56..76,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 56..57,
|
||||
id: Name("x"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 58..76,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 62..63,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 67..76,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 67..72,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 72..76,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 73..75,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
@@ -86,6 +214,82 @@ Module(
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 78..106,
|
||||
value: Call(
|
||||
ExprCall {
|
||||
range: 78..106,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 78..81,
|
||||
id: Name("sum"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 81..106,
|
||||
args: [
|
||||
Generator(
|
||||
ExprGenerator {
|
||||
range: 82..104,
|
||||
elt: Name(
|
||||
ExprName {
|
||||
range: 83..84,
|
||||
id: Name("x"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 85..103,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 89..90,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 94..103,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 94..99,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 99..103,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 100..102,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
parenthesized: true,
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ ty_server = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
argfile = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help", "string"] }
|
||||
clap_complete_command = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
countme = { workspace = true, features = ["enable"] }
|
||||
crossbeam = { workspace = true }
|
||||
|
||||
@@ -24,6 +24,10 @@ pub(crate) enum Command {
|
||||
|
||||
/// Display ty's version
|
||||
Version,
|
||||
|
||||
/// Generate shell completion
|
||||
#[clap(hide = true)]
|
||||
GenerateShellCompletion { shell: clap_complete_command::Shell },
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::sync::Mutex;
|
||||
use crate::args::{Args, CheckCommand, Command, TerminalColor};
|
||||
use crate::logging::setup_tracing;
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
@@ -68,6 +68,10 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
Command::Server => run_server().map(|()| ExitStatus::Success),
|
||||
Command::Check(check_args) => run_check(check_args),
|
||||
Command::Version => version().map(|()| ExitStatus::Success),
|
||||
Command::GenerateShellCompletion { shell } => {
|
||||
shell.generate(&mut Args::command(), &mut stdout());
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use ruff_text_size::TextSize;
|
||||
|
||||
use crate::Db;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Completion {
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub trait Db: SemanticDb + Upcast<dyn SemanticDb> + Upcast<dyn SourceDb> {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::Db;
|
||||
use ruff_db::files::{File, Files};
|
||||
@@ -16,6 +16,8 @@ pub(crate) mod tests {
|
||||
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use ty_python_semantic::{default_lint_registry, Db as SemanticDb, Program};
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
@@ -23,31 +25,35 @@ pub(crate) mod tests {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
events: Events,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
let events = Events::default();
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: ty_vendored::file_system().clone(),
|
||||
events: Arc::default(),
|
||||
events,
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
let events = inner.get_mut().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
@@ -127,12 +133,5 @@ pub(crate) mod tests {
|
||||
impl Db for TestDb {}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
impl salsa::Database for TestDb {}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,19 @@ impl ProjectDatabase {
|
||||
{
|
||||
let mut db = Self {
|
||||
project: None,
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(if tracing::enabled!(tracing::Level::TRACE) {
|
||||
Some(Box::new({
|
||||
move |event: Event| {
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation) {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::trace!("Salsa event: {event:?}");
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
files: Files::default(),
|
||||
system: Arc::new(system),
|
||||
};
|
||||
@@ -156,20 +168,7 @@ impl SourceDb for ProjectDatabase {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for ProjectDatabase {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> Event) {
|
||||
if !tracing::enabled!(tracing::Level::TRACE) {
|
||||
return;
|
||||
}
|
||||
|
||||
let event = event();
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation) {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::trace!("Salsa event: {event:?}");
|
||||
}
|
||||
}
|
||||
impl salsa::Database for ProjectDatabase {}
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for ProjectDatabase {
|
||||
@@ -206,9 +205,7 @@ mod format {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use salsa::Event;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
@@ -221,11 +218,13 @@ pub(crate) mod tests {
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
use crate::{Project, ProjectMetadata};
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
events: Arc<std::sync::Mutex<Vec<Event>>>,
|
||||
events: Events,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
@@ -234,12 +233,19 @@ pub(crate) mod tests {
|
||||
|
||||
impl TestDb {
|
||||
pub(crate) fn new(project: ProjectMetadata) -> Self {
|
||||
let events = Events::default();
|
||||
let mut db = Self {
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: ty_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
events: Arc::default(),
|
||||
events,
|
||||
project: None,
|
||||
};
|
||||
|
||||
@@ -251,13 +257,9 @@ pub(crate) mod tests {
|
||||
|
||||
impl TestDb {
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
let events = inner.get_mut().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
}
|
||||
@@ -332,10 +334,5 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> Event) {
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event());
|
||||
}
|
||||
}
|
||||
impl salsa::Database for TestDb {}
|
||||
}
|
||||
|
||||
@@ -219,34 +219,10 @@ impl Project {
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
.unwrap();
|
||||
// We sort diagnostics in a way that keeps them in source order
|
||||
// and grouped by file. After that, we fall back to severity
|
||||
// (with fatal messages sorting before info messages) and then
|
||||
// finally the diagnostic ID.
|
||||
file_diagnostics.sort_by(|d1, d2| {
|
||||
if let (Some(span1), Some(span2)) = (d1.primary_span(), d2.primary_span()) {
|
||||
let order = span1
|
||||
.file()
|
||||
.path(db)
|
||||
.as_str()
|
||||
.cmp(span2.file().path(db).as_str());
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
|
||||
if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) {
|
||||
let order = range1.start().cmp(&range2.start());
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reverse so that, e.g., Fatal sorts before Info.
|
||||
let order = d1.severity().cmp(&d2.severity()).reverse();
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
d1.id().cmp(&d2.id())
|
||||
file_diagnostics.sort_by(|left, right| {
|
||||
left.rendering_sort_key(db)
|
||||
.cmp(&right.rendering_sort_key(db))
|
||||
});
|
||||
diagnostics.extend(file_diagnostics);
|
||||
diagnostics
|
||||
|
||||
@@ -76,7 +76,7 @@ from typing_extensions import Annotated
|
||||
class C(Annotated[int, "foo"]): ...
|
||||
|
||||
# TODO: Should be `tuple[Literal[C], Literal[int], Literal[object]]`
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], @Todo(Inference of subscript on special form), Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, @Todo(Inference of subscript on special form), <class 'object'>]
|
||||
```
|
||||
|
||||
### Not parameterized
|
||||
@@ -88,5 +88,5 @@ from typing_extensions import Annotated
|
||||
# error: [invalid-base]
|
||||
class C(Annotated): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ from typing import Any
|
||||
|
||||
class SubclassOfAny(Any): ...
|
||||
|
||||
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[Literal[SubclassOfAny], Any, Literal[object]]
|
||||
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[<class 'SubclassOfAny'>, Any, <class 'object'>]
|
||||
|
||||
x: SubclassOfAny = 1 # error: [invalid-assignment]
|
||||
y: int = SubclassOfAny()
|
||||
|
||||
190
crates/ty_python_semantic/resources/mdtest/annotations/self.md
Normal file
190
crates/ty_python_semantic/resources/mdtest/annotations/self.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Self
|
||||
|
||||
`Self` is treated as if it were a `TypeVar` bound to the class it's being used on.
|
||||
|
||||
`typing.Self` is only available in Python 3.11 and later.
|
||||
|
||||
## Methods
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class Shape:
|
||||
def set_scale(self: Self, scale: float) -> Self:
|
||||
reveal_type(self) # revealed: Self
|
||||
return self
|
||||
|
||||
def nested_type(self) -> list[Self]:
|
||||
return [self]
|
||||
|
||||
def nested_func(self: Self) -> Self:
|
||||
def inner() -> Self:
|
||||
reveal_type(self) # revealed: Self
|
||||
return self
|
||||
return inner()
|
||||
|
||||
def implicit_self(self) -> Self:
|
||||
# TODO: first argument in a method should be considered as "typing.Self"
|
||||
reveal_type(self) # revealed: Unknown
|
||||
return self
|
||||
|
||||
reveal_type(Shape().nested_type()) # revealed: @Todo(specialized non-generic class)
|
||||
reveal_type(Shape().nested_func()) # revealed: Shape
|
||||
|
||||
class Circle(Shape):
|
||||
def set_scale(self: Self, scale: float) -> Self:
|
||||
reveal_type(self) # revealed: Self
|
||||
return self
|
||||
|
||||
class Outer:
|
||||
class Inner:
|
||||
def foo(self: Self) -> Self:
|
||||
reveal_type(self) # revealed: Self
|
||||
return self
|
||||
```
|
||||
|
||||
## Class Methods
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self, TypeVar
|
||||
|
||||
class Shape:
|
||||
def foo(self: Self) -> Self:
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def bar(cls: type[Self]) -> Self:
|
||||
# TODO: type[Shape]
|
||||
reveal_type(cls) # revealed: @Todo(unsupported type[X] special form)
|
||||
return cls()
|
||||
|
||||
class Circle(Shape): ...
|
||||
|
||||
reveal_type(Shape().foo()) # revealed: Shape
|
||||
# TODO: Shape
|
||||
reveal_type(Shape.bar()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class LinkedList:
|
||||
value: int
|
||||
next_node: Self
|
||||
|
||||
def next(self: Self) -> Self:
|
||||
reveal_type(self.value) # revealed: int
|
||||
return self.next_node
|
||||
|
||||
reveal_type(LinkedList().next()) # revealed: LinkedList
|
||||
```
|
||||
|
||||
## Generic Classes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self, Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Container(Generic[T]):
|
||||
value: T
|
||||
def set_value(self: Self, value: T) -> Self:
|
||||
return self
|
||||
|
||||
int_container: Container[int] = Container[int]()
|
||||
reveal_type(int_container) # revealed: Container[int]
|
||||
reveal_type(int_container.set_value(1)) # revealed: Container[int]
|
||||
```
|
||||
|
||||
## Protocols
|
||||
|
||||
TODO: <https://typing.python.org/en/latest/spec/generics.html#use-in-protocols>
|
||||
|
||||
## Annotations
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class Shape:
|
||||
def union(self: Self, other: Self | None):
|
||||
reveal_type(other) # revealed: Self | None
|
||||
return self
|
||||
```
|
||||
|
||||
## Invalid Usage
|
||||
|
||||
`Self` cannot be used in the signature of a function or variable.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self, Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def x(s: Self): ...
|
||||
|
||||
# error: [invalid-type-form]
|
||||
b: Self
|
||||
|
||||
# TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self"
|
||||
class Foo:
|
||||
# TODO: rejected Self because self has a different type
|
||||
def has_existing_self_annotation(self: T) -> Self:
|
||||
return self # error: [invalid-return-type]
|
||||
|
||||
def return_concrete_type(self) -> Self:
|
||||
# TODO: tell user to use "Foo" instead of "Self"
|
||||
# error: [invalid-return-type]
|
||||
return Foo()
|
||||
|
||||
@staticmethod
|
||||
# TODO: reject because of staticmethod
|
||||
def make() -> Self:
|
||||
# error: [invalid-return-type]
|
||||
return Foo()
|
||||
|
||||
class Bar(Generic[T]):
|
||||
foo: T
|
||||
def bar(self) -> T:
|
||||
return self.foo
|
||||
|
||||
# error: [invalid-type-form]
|
||||
class Baz(Bar[Self]): ...
|
||||
|
||||
class MyMetaclass(type):
|
||||
# TODO: rejected
|
||||
def __new__(cls) -> Self:
|
||||
return super().__new__(cls)
|
||||
```
|
||||
@@ -85,25 +85,25 @@ import typing
|
||||
class ListSubclass(typing.List): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'ListSubclass'>, <class 'list'>, <class 'MutableSequence'>, <class 'Sequence'>, <class 'Reversible'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(ListSubclass.__mro__)
|
||||
|
||||
class DictSubclass(typing.Dict): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[DictSubclass], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[Unknown, Unknown]], Literal[Mapping[Unknown, Unknown]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
# revealed: tuple[<class 'DictSubclass'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], <class 'object'>]
|
||||
reveal_type(DictSubclass.__mro__)
|
||||
|
||||
class SetSubclass(typing.Set): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'SetSubclass'>, <class 'set'>, <class 'MutableSet'>, <class 'AbstractSet'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(SetSubclass.__mro__)
|
||||
|
||||
class FrozenSetSubclass(typing.FrozenSet): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[FrozenSetSubclass], Literal[frozenset], Unknown, Literal[object]]
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset'>, <class 'AbstractSet'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(FrozenSetSubclass.__mro__)
|
||||
|
||||
####################
|
||||
@@ -113,30 +113,30 @@ reveal_type(FrozenSetSubclass.__mro__)
|
||||
class ChainMapSubclass(typing.ChainMap): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap[Unknown, Unknown]], Literal[MutableMapping[Unknown, Unknown]], Literal[Mapping[Unknown, Unknown]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
# revealed: tuple[<class 'ChainMapSubclass'>, <class 'ChainMap[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], <class 'object'>]
|
||||
reveal_type(ChainMapSubclass.__mro__)
|
||||
|
||||
class CounterSubclass(typing.Counter): ...
|
||||
|
||||
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[CounterSubclass], Literal[Counter[Unknown]], Literal[dict[Unknown, int]], Literal[MutableMapping[Unknown, int]], Literal[Mapping[Unknown, int]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], Literal[object]]
|
||||
# revealed: tuple[<class 'CounterSubclass'>, <class 'Counter[Unknown]'>, <class 'dict[Unknown, int]'>, <class 'MutableMapping[Unknown, int]'>, <class 'Mapping[Unknown, int]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], <class 'object'>]
|
||||
reveal_type(CounterSubclass.__mro__)
|
||||
|
||||
class DefaultDictSubclass(typing.DefaultDict): ...
|
||||
|
||||
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict[Unknown, Unknown]], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[Unknown, Unknown]], Literal[Mapping[Unknown, Unknown]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
# revealed: tuple[<class 'DefaultDictSubclass'>, <class 'defaultdict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], <class 'object'>]
|
||||
reveal_type(DefaultDictSubclass.__mro__)
|
||||
|
||||
class DequeSubclass(typing.Deque): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'DequeSubclass'>, <class 'deque'>, <class 'MutableSequence'>, <class 'Sequence'>, <class 'Reversible'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(DequeSubclass.__mro__)
|
||||
|
||||
class OrderedDictSubclass(typing.OrderedDict): ...
|
||||
|
||||
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict[Unknown, Unknown]], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[Unknown, Unknown]], Literal[Mapping[Unknown, Unknown]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
# revealed: tuple[<class 'OrderedDictSubclass'>, <class 'OrderedDict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], <class 'object'>]
|
||||
reveal_type(OrderedDictSubclass.__mro__)
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
|
||||
|
||||
class Foo:
|
||||
def method(self, x: Self):
|
||||
reveal_type(x) # revealed: @Todo(Support for `typing.Self`)
|
||||
reveal_type(x) # revealed: Self
|
||||
```
|
||||
|
||||
## Type expressions
|
||||
@@ -73,7 +73,7 @@ class E(Concatenate): ... # error: [invalid-base]
|
||||
class F(Callable): ...
|
||||
class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`"
|
||||
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
|
||||
reveal_type(F.__mro__) # revealed: tuple[<class 'F'>, @Todo(Support for Callable as a base class), <class 'object'>]
|
||||
```
|
||||
|
||||
## Subscriptability
|
||||
|
||||
@@ -54,11 +54,11 @@ c_instance.declared_and_bound = False
|
||||
c_instance.declared_and_bound = "incompatible"
|
||||
|
||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `<class 'C'>` itself."
|
||||
reveal_type(C.inferred_from_value) # revealed: Unknown
|
||||
|
||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `Literal[C]`"
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `<class 'C'>`"
|
||||
C.inferred_from_value = "overwritten on class"
|
||||
|
||||
# This assignment is fine:
|
||||
@@ -90,11 +90,11 @@ reveal_type(c_instance.declared_and_bound) # revealed: str | None
|
||||
|
||||
# Note that both mypy and pyright show no error in this case! So we may reconsider this in
|
||||
# the future, if it turns out to produce too many false positives. We currently emit:
|
||||
# error: [unresolved-attribute] "Attribute `declared_and_bound` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
# error: [unresolved-attribute] "Attribute `declared_and_bound` can only be accessed on instances, not on the class object `<class 'C'>` itself."
|
||||
reveal_type(C.declared_and_bound) # revealed: Unknown
|
||||
|
||||
# Same as above. Mypy and pyright do not show an error here.
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `declared_and_bound` from the class object `Literal[C]`"
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `declared_and_bound` from the class object `<class 'C'>`"
|
||||
C.declared_and_bound = "overwritten on class"
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `declared_and_bound` of type `str | None`"
|
||||
@@ -115,10 +115,10 @@ c_instance = C()
|
||||
reveal_type(c_instance.only_declared) # revealed: str
|
||||
|
||||
# Mypy and pyright do not show an error here. We treat this as a pure instance variable.
|
||||
# error: [unresolved-attribute] "Attribute `only_declared` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
# error: [unresolved-attribute] "Attribute `only_declared` can only be accessed on instances, not on the class object `<class 'C'>` itself."
|
||||
reveal_type(C.only_declared) # revealed: Unknown
|
||||
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `only_declared` from the class object `Literal[C]`"
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `only_declared` from the class object `<class 'C'>`"
|
||||
C.only_declared = "overwritten on class"
|
||||
```
|
||||
|
||||
@@ -191,10 +191,10 @@ reveal_type(c_instance.declared_only) # revealed: bytes
|
||||
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
||||
|
||||
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `<class 'C'>` itself."
|
||||
reveal_type(C.inferred_from_value) # revealed: Unknown
|
||||
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `Literal[C]`"
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `<class 'C'>`"
|
||||
C.inferred_from_value = "overwritten on class"
|
||||
```
|
||||
|
||||
@@ -718,7 +718,7 @@ reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
|
||||
# TODO: should be no error when descriptor protocol is supported
|
||||
# and the assignment is properly attributed to the class method.
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `pure_class_variable` from the class object `Literal[C]`"
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `pure_class_variable` from the class object `<class 'C'>`"
|
||||
C.pure_class_variable = "overwritten on class"
|
||||
|
||||
# TODO: should be `Unknown | Literal["value set in class method"]` or
|
||||
@@ -925,7 +925,7 @@ def _(flag: bool):
|
||||
reveal_type(C1.y) # revealed: int | str
|
||||
|
||||
C1.y = 100
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `Literal[C1, C1]`"
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'C1'> | <class 'C1'>`"
|
||||
C1.y = "problematic"
|
||||
|
||||
class C2:
|
||||
@@ -1002,10 +1002,10 @@ def _(flag1: bool, flag2: bool):
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `Literal[C1, C2, C3]`"
|
||||
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>`"
|
||||
C.x = 100
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
|
||||
@@ -1034,7 +1034,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
@@ -1165,12 +1165,12 @@ def _(flag: bool):
|
||||
class C2: ...
|
||||
C = C1 if flag else C2
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
|
||||
# error: [unresolved-attribute] "Type `<class 'C1'> | <class 'C2'>` has no attribute `x`"
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
|
||||
# TODO: This should ideally be a `unresolved-attribute` error. We need better union
|
||||
# handling in `validate_attribute_assignment` for this.
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `x` on type `Literal[C1, C2]`"
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `x` on type `<class 'C1'> | <class 'C2'>`"
|
||||
C.x = 1
|
||||
```
|
||||
|
||||
@@ -1206,7 +1206,7 @@ class C(D, F): ...
|
||||
class B(E, D): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'A'>, <class 'B'>, <class 'E'>, <class 'C'>, <class 'D'>, <class 'F'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(A.__mro__)
|
||||
|
||||
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
|
||||
@@ -1399,7 +1399,7 @@ class A:
|
||||
class B(Any): ...
|
||||
class C(B, A): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Any, Literal[A], Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'B'>, Any, <class 'A'>, <class 'object'>]
|
||||
reveal_type(C.x) # revealed: Literal[1] & Any
|
||||
```
|
||||
|
||||
@@ -1556,31 +1556,31 @@ The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is alway
|
||||
```py
|
||||
import typing_extensions
|
||||
|
||||
reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType]
|
||||
reveal_type(type(typing_extensions)) # revealed: Literal[ModuleType]
|
||||
reveal_type(typing_extensions.__class__) # revealed: <class 'ModuleType'>
|
||||
reveal_type(type(typing_extensions)) # revealed: <class 'ModuleType'>
|
||||
|
||||
a = 42
|
||||
reveal_type(a.__class__) # revealed: Literal[int]
|
||||
reveal_type(type(a)) # revealed: Literal[int]
|
||||
reveal_type(a.__class__) # revealed: <class 'int'>
|
||||
reveal_type(type(a)) # revealed: <class 'int'>
|
||||
|
||||
b = "42"
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
reveal_type(b.__class__) # revealed: <class 'str'>
|
||||
|
||||
c = b"42"
|
||||
reveal_type(c.__class__) # revealed: Literal[bytes]
|
||||
reveal_type(c.__class__) # revealed: <class 'bytes'>
|
||||
|
||||
d = True
|
||||
reveal_type(d.__class__) # revealed: Literal[bool]
|
||||
reveal_type(d.__class__) # revealed: <class 'bool'>
|
||||
|
||||
e = (42, 42)
|
||||
reveal_type(e.__class__) # revealed: Literal[tuple]
|
||||
reveal_type(e.__class__) # revealed: <class 'tuple'>
|
||||
|
||||
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
|
||||
reveal_type(a.__class__) # revealed: type[int]
|
||||
reveal_type(type(a)) # revealed: type[int]
|
||||
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
reveal_type(type(b)) # revealed: Literal[str]
|
||||
reveal_type(b.__class__) # revealed: <class 'str'>
|
||||
reveal_type(type(b)) # revealed: <class 'str'>
|
||||
|
||||
reveal_type(c.__class__) # revealed: type[int] | type[str]
|
||||
reveal_type(type(c)) # revealed: type[int] | type[str]
|
||||
@@ -1591,11 +1591,11 @@ def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
|
||||
# All we know is that the metaclass must be a (non-strict) subclass of `type`.
|
||||
reveal_type(d.__class__) # revealed: type[type]
|
||||
|
||||
reveal_type(f.__class__) # revealed: Literal[FunctionType]
|
||||
reveal_type(f.__class__) # revealed: <class 'FunctionType'>
|
||||
|
||||
class Foo: ...
|
||||
|
||||
reveal_type(Foo.__class__) # revealed: Literal[type]
|
||||
reveal_type(Foo.__class__) # revealed: <class 'type'>
|
||||
```
|
||||
|
||||
## Module attributes
|
||||
|
||||
@@ -22,6 +22,6 @@ reveal_type(A | B) # revealed: UnionType
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||
# error: "Operator `|` is unsupported between objects of type `<class 'A'>` and `<class 'B'>`"
|
||||
reveal_type(A | B) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -307,11 +307,11 @@ class Yes:
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`"
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `<class 'Yes'>` and `<class 'Yes'>`"
|
||||
reveal_type(Yes + Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`"
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `<class 'Sub'>` and `<class 'Sub'>`"
|
||||
reveal_type(Sub + Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`"
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `<class 'No'>` and `<class 'No'>`"
|
||||
reveal_type(No + No) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
||||
@@ -388,13 +388,13 @@ class A(metaclass=Meta): ...
|
||||
class B(metaclass=Meta): ...
|
||||
|
||||
reveal_type(A + B) # revealed: int
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `<class 'A'>` and `<class 'B'>`"
|
||||
reveal_type(A - B) # revealed: Unknown
|
||||
|
||||
reveal_type(A < B) # revealed: bool
|
||||
reveal_type(A > B) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `Literal[A]` and `Literal[B]`"
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `<class 'A'>` and `<class 'B'>`"
|
||||
reveal_type(A <= B) # revealed: Unknown
|
||||
|
||||
reveal_type(A[0]) # revealed: str
|
||||
|
||||
@@ -312,8 +312,8 @@ class C:
|
||||
```py
|
||||
from mod import MyInt, C
|
||||
|
||||
reveal_type(MyInt) # revealed: Literal[int]
|
||||
reveal_type(C.MyStr) # revealed: Literal[str]
|
||||
reveal_type(MyInt) # revealed: <class 'int'>
|
||||
reveal_type(C.MyStr) # revealed: <class 'str'>
|
||||
```
|
||||
|
||||
### Undeclared and possibly unbound
|
||||
@@ -336,8 +336,8 @@ if flag():
|
||||
# error: [possibly-unbound-import]
|
||||
from mod import MyInt, C
|
||||
|
||||
reveal_type(MyInt) # revealed: Literal[int]
|
||||
reveal_type(C.MyStr) # revealed: Literal[str]
|
||||
reveal_type(MyInt) # revealed: <class 'int'>
|
||||
reveal_type(C.MyStr) # revealed: <class 'str'>
|
||||
```
|
||||
|
||||
### Undeclared and unbound
|
||||
|
||||
@@ -20,7 +20,7 @@ tested more extensively in `crates/ty_python_semantic/resources/mdtest/attribute
|
||||
tests for the `__class__` attribute.)
|
||||
|
||||
```py
|
||||
reveal_type(type(1)) # revealed: Literal[int]
|
||||
reveal_type(type(1)) # revealed: <class 'int'>
|
||||
```
|
||||
|
||||
But a three-argument call to type creates a dynamic instance of the `type` class:
|
||||
|
||||
@@ -75,7 +75,8 @@ constructor from it.
|
||||
from typing_extensions import Self
|
||||
|
||||
class Base:
|
||||
def __new__(cls, x: int) -> Self: ...
|
||||
def __new__(cls, x: int) -> Self:
|
||||
return cls()
|
||||
|
||||
class Foo(Base): ...
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ class Meta(type):
|
||||
class C(metaclass=Meta):
|
||||
pass
|
||||
|
||||
reveal_type(C.f) # revealed: bound method Literal[C].f(arg: int) -> str
|
||||
reveal_type(C.f) # revealed: bound method <class 'C'>.f(arg: int) -> str
|
||||
reveal_type(C.f(1)) # revealed: str
|
||||
```
|
||||
|
||||
@@ -322,7 +322,7 @@ class C:
|
||||
def f(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(C.f) # revealed: bound method Literal[C].f(x: int) -> str
|
||||
reveal_type(C.f) # revealed: bound method <class 'C'>.f(x: int) -> str
|
||||
reveal_type(C().f) # revealed: bound method type[C].f(x: int) -> str
|
||||
```
|
||||
|
||||
@@ -350,7 +350,7 @@ class D:
|
||||
# This function is wrongly annotated, it should be `type[D]` instead of `D`
|
||||
pass
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `D`, found `Literal[D]`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `D`, found `<class 'D'>`"
|
||||
D.f()
|
||||
```
|
||||
|
||||
@@ -360,7 +360,7 @@ When a class method is accessed on a derived class, it is bound to that derived
|
||||
class Derived(C):
|
||||
pass
|
||||
|
||||
reveal_type(Derived.f) # revealed: bound method Literal[Derived].f(x: int) -> str
|
||||
reveal_type(Derived.f) # revealed: bound method <class 'Derived'>.f(x: int) -> str
|
||||
reveal_type(Derived().f) # revealed: bound method type[Derived].f(x: int) -> str
|
||||
|
||||
reveal_type(Derived.f(1)) # revealed: str
|
||||
@@ -386,15 +386,15 @@ reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get_
|
||||
But we correctly model how the `classmethod` descriptor works:
|
||||
|
||||
```py
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: bound method Literal[C].f() -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method Literal[C].f() -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: bound method <class 'C'>.f() -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method <class 'C'>.f() -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: bound method type[C].f() -> Unknown
|
||||
```
|
||||
|
||||
The `owner` argument takes precedence over the `instance` argument:
|
||||
|
||||
```py
|
||||
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound method Literal[C].f() -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound method <class 'C'>.f() -> Unknown
|
||||
```
|
||||
|
||||
### Classmethods mixed with other decorators
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Super
|
||||
|
||||
Python defines the terms *bound super object* and *unbound super object*.
|
||||
Python defines the terms _bound super object_ and _unbound super object_.
|
||||
|
||||
An **unbound super object** is created when `super` is called with only one argument. (e.g.
|
||||
`super(A)`). This object may later be bound using the `super.__get__` method. However, this form is
|
||||
@@ -30,24 +30,24 @@ class C(B):
|
||||
def c(self): ...
|
||||
cc: int = 3
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>]
|
||||
|
||||
super(C, C()).a
|
||||
super(C, C()).b
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[C], C>` has no attribute `c`"
|
||||
# error: [unresolved-attribute] "Type `<super: <class 'C'>, C>` has no attribute `c`"
|
||||
super(C, C()).c
|
||||
|
||||
super(B, C()).a
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `b`"
|
||||
# error: [unresolved-attribute] "Type `<super: <class 'B'>, C>` has no attribute `b`"
|
||||
super(B, C()).b
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `c`"
|
||||
# error: [unresolved-attribute] "Type `<super: <class 'B'>, C>` has no attribute `c`"
|
||||
super(B, C()).c
|
||||
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `a`"
|
||||
# error: [unresolved-attribute] "Type `<super: <class 'A'>, C>` has no attribute `a`"
|
||||
super(A, C()).a
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `b`"
|
||||
# error: [unresolved-attribute] "Type `<super: <class 'A'>, C>` has no attribute `b`"
|
||||
super(A, C()).b
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `c`"
|
||||
# error: [unresolved-attribute] "Type `<super: <class 'A'>, C>` has no attribute `c`"
|
||||
super(A, C()).c
|
||||
|
||||
reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
|
||||
@@ -72,14 +72,14 @@ class A:
|
||||
|
||||
class B(A):
|
||||
def __init__(self, a: int):
|
||||
# TODO: Once `Self` is supported, this should be `<super: Literal[B], B>`
|
||||
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, B>`
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
super().__init__(a)
|
||||
|
||||
@classmethod
|
||||
def f(cls):
|
||||
# TODO: Once `Self` is supported, this should be `<super: Literal[B], Literal[B]>`
|
||||
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
super().f()
|
||||
|
||||
super(B, B(42)).__init__(42)
|
||||
@@ -88,7 +88,7 @@ super(B, B).f()
|
||||
|
||||
### Unbound Super Object
|
||||
|
||||
Calling `super(cls)` without a second argument returns an *unbound super object*. This is treated as
|
||||
Calling `super(cls)` without a second argument returns an _unbound super object_. This is treated as
|
||||
a plain `super` instance and does not support name lookup via the MRO.
|
||||
|
||||
```py
|
||||
@@ -115,7 +115,7 @@ class A:
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(super(B, B()).a) # revealed: int
|
||||
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `<super: Literal[B], B>`"
|
||||
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `<super: <class 'B'>, B>`"
|
||||
super(B, B()).a = 3
|
||||
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`"
|
||||
super(B).a = 5
|
||||
@@ -134,7 +134,7 @@ def f(x):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
reveal_type(super(x, x)) # revealed: <super: Unknown, Unknown>
|
||||
reveal_type(super(A, x)) # revealed: <super: Literal[A], Unknown>
|
||||
reveal_type(super(A, x)) # revealed: <super: <class 'A'>, Unknown>
|
||||
reveal_type(super(x, A())) # revealed: <super: Unknown, A>
|
||||
|
||||
reveal_type(super(x, x).a) # revealed: Unknown
|
||||
@@ -149,29 +149,29 @@ from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: Literal[A], Unknown>
|
||||
reveal_type(super()) # revealed: <super: <class 'A'>, Unknown>
|
||||
|
||||
class B:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
|
||||
class C(A.B):
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: Literal[C], Unknown>
|
||||
reveal_type(super()) # revealed: <super: <class 'C'>, Unknown>
|
||||
|
||||
def inner(t: C):
|
||||
reveal_type(super()) # revealed: <super: Literal[B], C>
|
||||
lambda x: reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, C>
|
||||
lambda x: reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
```
|
||||
|
||||
## Built-ins and Literals
|
||||
|
||||
```py
|
||||
reveal_type(super(bool, True)) # revealed: <super: Literal[bool], bool>
|
||||
reveal_type(super(bool, bool())) # revealed: <super: Literal[bool], bool>
|
||||
reveal_type(super(int, bool())) # revealed: <super: Literal[int], bool>
|
||||
reveal_type(super(int, 3)) # revealed: <super: Literal[int], int>
|
||||
reveal_type(super(str, "")) # revealed: <super: Literal[str], str>
|
||||
reveal_type(super(bool, True)) # revealed: <super: <class 'bool'>, bool>
|
||||
reveal_type(super(bool, bool())) # revealed: <super: <class 'bool'>, bool>
|
||||
reveal_type(super(int, bool())) # revealed: <super: <class 'int'>, bool>
|
||||
reveal_type(super(int, 3)) # revealed: <super: <class 'int'>, int>
|
||||
reveal_type(super(str, "")) # revealed: <super: <class 'str'>, str>
|
||||
```
|
||||
|
||||
## Descriptor Behavior with Super
|
||||
@@ -195,7 +195,7 @@ reveal_type(super(B, B()).a2) # revealed: bound method type[B].a2() -> Unknown
|
||||
# A.__dict__["a1"].__get__(None, B)
|
||||
reveal_type(super(B, B).a1) # revealed: def a1(self) -> Unknown
|
||||
# A.__dict__["a2"].__get__(None, B)
|
||||
reveal_type(super(B, B).a2) # revealed: bound method Literal[B].a2() -> Unknown
|
||||
reveal_type(super(B, B).a2) # revealed: bound method <class 'B'>.a2() -> Unknown
|
||||
```
|
||||
|
||||
## Union of Supers
|
||||
@@ -213,22 +213,22 @@ class C(A, B): ...
|
||||
class D(B, A): ...
|
||||
|
||||
def f(x: C | D):
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
|
||||
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[B], Literal[A], Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'A'>, <class 'B'>, <class 'object'>]
|
||||
reveal_type(D.__mro__) # revealed: tuple[<class 'D'>, <class 'B'>, <class 'A'>, <class 'object'>]
|
||||
|
||||
s = super(A, x)
|
||||
reveal_type(s) # revealed: <super: Literal[A], C> | <super: Literal[A], D>
|
||||
reveal_type(s) # revealed: <super: <class 'A'>, C> | <super: <class 'A'>, D>
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `b` on type `<super: Literal[A], C> | <super: Literal[A], D>` is possibly unbound"
|
||||
# error: [possibly-unbound-attribute] "Attribute `b` on type `<super: <class 'A'>, C> | <super: <class 'A'>, D>` is possibly unbound"
|
||||
s.b
|
||||
|
||||
def f(flag: bool):
|
||||
x = str() if flag else str("hello")
|
||||
reveal_type(x) # revealed: Literal["", "hello"]
|
||||
reveal_type(super(str, x)) # revealed: <super: Literal[str], str>
|
||||
reveal_type(super(str, x)) # revealed: <super: <class 'str'>, str>
|
||||
|
||||
def f(x: int | str):
|
||||
# error: [invalid-super-argument] "`str` is not an instance or subclass of `Literal[int]` in `super(Literal[int], str)` call"
|
||||
# error: [invalid-super-argument] "`str` is not an instance or subclass of `<class 'int'>` in `super(<class 'int'>, str)` call"
|
||||
super(int, x)
|
||||
```
|
||||
|
||||
@@ -254,12 +254,12 @@ def f(flag: bool):
|
||||
class D(C): ...
|
||||
s = super(D, D())
|
||||
|
||||
reveal_type(s) # revealed: <super: Literal[B], B> | <super: Literal[D], D>
|
||||
reveal_type(s) # revealed: <super: <class 'B'>, B> | <super: <class 'D'>, D>
|
||||
|
||||
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
|
||||
reveal_type(s.y) # revealed: int | str
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `a` on type `<super: Literal[B], B> | <super: Literal[D], D>` is possibly unbound"
|
||||
# error: [possibly-unbound-attribute] "Attribute `a` on type `<super: <class 'B'>, B> | <super: <class 'D'>, D>` is possibly unbound"
|
||||
reveal_type(s.a) # revealed: str
|
||||
```
|
||||
|
||||
@@ -339,30 +339,30 @@ def f(x: int):
|
||||
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
|
||||
super(IntAlias, 0)
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[""])` call"
|
||||
# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `<class 'int'>` in `super(<class 'int'>, Literal[""])` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(int, str()))
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[str]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[str])` call"
|
||||
# error: [invalid-super-argument] "`<class 'str'>` is not an instance or subclass of `<class 'int'>` in `super(<class 'int'>, <class 'str'>)` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(int, str))
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
# error: [invalid-super-argument] "`A` is not an instance or subclass of `Literal[B]` in `super(Literal[B], A)` call"
|
||||
# error: [invalid-super-argument] "`A` is not an instance or subclass of `<class 'B'>` in `super(<class 'B'>, A)` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, A()))
|
||||
|
||||
# error: [invalid-super-argument] "`object` is not an instance or subclass of `Literal[B]` in `super(Literal[B], object)` call"
|
||||
# error: [invalid-super-argument] "`object` is not an instance or subclass of `<class 'B'>` in `super(<class 'B'>, object)` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, object()))
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[A]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[A])` call"
|
||||
# error: [invalid-super-argument] "`<class 'A'>` is not an instance or subclass of `<class 'B'>` in `super(<class 'B'>, <class 'A'>)` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, A))
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[object]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[object])` call"
|
||||
# error: [invalid-super-argument] "`<class 'object'>` is not an instance or subclass of `<class 'B'>` in `super(<class 'B'>, <class 'object'>)` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, object))
|
||||
|
||||
@@ -386,7 +386,7 @@ class B(A):
|
||||
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
|
||||
super().a
|
||||
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[B], B>` has no attribute `a`"
|
||||
# error: [unresolved-attribute] "Type `<super: <class 'B'>, B>` has no attribute `a`"
|
||||
super(B, B(42)).a
|
||||
```
|
||||
|
||||
@@ -405,6 +405,6 @@ class B(A): ...
|
||||
|
||||
reveal_type(A()[0]) # revealed: int
|
||||
reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int
|
||||
# error: [non-subscriptable] "Cannot subscript object of type `<super: Literal[B], B>` with no `__getitem__` method"
|
||||
# error: [non-subscriptable] "Cannot subscript object of type `<super: <class 'B'>, B>` with no `__getitem__` method"
|
||||
super(B, B())[0]
|
||||
```
|
||||
|
||||
@@ -281,13 +281,11 @@ class D1:
|
||||
class D2:
|
||||
x: str
|
||||
|
||||
# TODO: these should not be errors
|
||||
D1("a") # error: [too-many-positional-arguments]
|
||||
D2("a") # error: [too-many-positional-arguments]
|
||||
D1("a")
|
||||
D2("a")
|
||||
|
||||
# TODO: these should be invalid-argument-type errors
|
||||
D1(1.2) # error: [too-many-positional-arguments]
|
||||
D2(1.2) # error: [too-many-positional-arguments]
|
||||
D1(1.2) # error: [invalid-argument-type]
|
||||
D2(1.2) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
|
||||
|
||||
@@ -181,7 +181,7 @@ class D:
|
||||
class_literal: TypeOf[SomeClass]
|
||||
class_subtype_of: type[SomeClass]
|
||||
|
||||
# revealed: (function_literal: def some_function() -> None, class_literal: Literal[SomeClass], class_subtype_of: type[SomeClass]) -> None
|
||||
# revealed: (function_literal: def some_function() -> None, class_literal: <class 'SomeClass'>, class_subtype_of: type[SomeClass]) -> None
|
||||
reveal_type(D.__init__)
|
||||
```
|
||||
|
||||
@@ -489,7 +489,7 @@ class DataWithDescription[T]:
|
||||
data: T
|
||||
description: str
|
||||
|
||||
reveal_type(DataWithDescription[int]) # revealed: Literal[DataWithDescription[int]]
|
||||
reveal_type(DataWithDescription[int]) # revealed: <class 'DataWithDescription[int]'>
|
||||
|
||||
d_int = DataWithDescription[int](1, "description") # OK
|
||||
reveal_type(d_int.data) # revealed: int
|
||||
@@ -675,7 +675,7 @@ from dataclasses import dataclass
|
||||
class C:
|
||||
x: int
|
||||
|
||||
# error: [unresolved-attribute] "Attribute `x` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
# error: [unresolved-attribute] "Attribute `x` can only be accessed on instances, not on the class object `<class 'C'>` itself."
|
||||
C.x
|
||||
```
|
||||
|
||||
@@ -715,8 +715,8 @@ class Person:
|
||||
name: str
|
||||
age: int | None = None
|
||||
|
||||
reveal_type(type(Person)) # revealed: Literal[type]
|
||||
reveal_type(Person.__mro__) # revealed: tuple[Literal[Person], Literal[object]]
|
||||
reveal_type(type(Person)) # revealed: <class 'type'>
|
||||
reveal_type(Person.__mro__) # revealed: tuple[<class 'Person'>, <class 'object'>]
|
||||
```
|
||||
|
||||
The generated methods have the following signatures:
|
||||
|
||||
@@ -269,7 +269,7 @@ on the metaclass:
|
||||
```py
|
||||
C1.meta_data_descriptor = 1
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type `Literal[C1]` with custom `__set__` method"
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type `<class 'C1'>` with custom `__set__` method"
|
||||
C1.meta_data_descriptor = "invalid"
|
||||
```
|
||||
|
||||
@@ -371,7 +371,7 @@ def _(flag: bool):
|
||||
# TODO: We currently emit two diagnostics here, corresponding to the two states of `flag`. The diagnostics are not
|
||||
# wrong, but they could be subsumed under a higher-level diagnostic.
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor1` on type `Literal[C5]` with custom `__set__` method"
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor1` on type `<class 'C5'>` with custom `__set__` method"
|
||||
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `meta_data_descriptor1` of type `Literal["value on class"]`"
|
||||
C5.meta_data_descriptor1 = None
|
||||
|
||||
@@ -724,13 +724,13 @@ def _(flag: bool):
|
||||
non_data: NonDataDescriptor = NonDataDescriptor()
|
||||
data: DataDescriptor = DataDescriptor()
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `Literal[PossiblyUnbound]` is possibly unbound"
|
||||
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `<class 'PossiblyUnbound'>` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound.non_data) # revealed: int
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound().non_data) # revealed: int
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `data` on type `Literal[PossiblyUnbound]` is possibly unbound"
|
||||
# error: [possibly-unbound-attribute] "Attribute `data` on type `<class 'PossiblyUnbound'>` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound.data) # revealed: int
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound"
|
||||
|
||||
@@ -600,12 +600,12 @@ except:
|
||||
reveal_type(x) # revealed: E
|
||||
|
||||
x = Bar
|
||||
reveal_type(x) # revealed: Literal[Bar]
|
||||
reveal_type(x) # revealed: <class 'Bar'>
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | Literal[foo] | Literal[Bar]`
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | Literal[Bar]
|
||||
# TODO: should be `Literal[1] | <class 'foo'> | <class 'Bar'>`
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | <class 'Bar'>
|
||||
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | Literal[Bar]
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | <class 'Bar'>
|
||||
```
|
||||
|
||||
[1]: https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d
|
||||
|
||||
@@ -26,9 +26,9 @@ def _(flag: bool):
|
||||
|
||||
reveal_type(A.union_declared) # revealed: int | str
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `<class 'A'>` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: str
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
# error: [unresolved-attribute] "Type `<class 'A'>` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -345,7 +345,7 @@ def f(cond: bool) -> str:
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
A function with a `yield` statement anywhere in its body is a
|
||||
A function with a `yield` or `yield from` expression anywhere in its body is a
|
||||
[generator function](https://docs.python.org/3/glossary.html#term-generator). A generator function
|
||||
implicitly returns an instance of `types.GeneratorType` even if it does not contain any `return`
|
||||
statements.
|
||||
@@ -366,6 +366,9 @@ def h() -> typing.Iterator:
|
||||
def i() -> typing.Iterable:
|
||||
yield 42
|
||||
|
||||
def i2() -> typing.Generator:
|
||||
yield from i()
|
||||
|
||||
def j() -> str: # error: [invalid-return-type]
|
||||
yield 42
|
||||
```
|
||||
|
||||
@@ -29,7 +29,7 @@ class RepeatedTypevar(Generic[T, T]): ...
|
||||
You can only specialize `typing.Generic` with typevars (TODO: or param specs or typevar tuples).
|
||||
|
||||
```py
|
||||
# error: [invalid-argument-type] "`Literal[int]` is not a valid argument to `typing.Generic`"
|
||||
# error: [invalid-argument-type] "`<class 'int'>` is not a valid argument to `typing.Generic`"
|
||||
class GenericOfType(Generic[int]): ...
|
||||
```
|
||||
|
||||
@@ -436,7 +436,7 @@ T = TypeVar("T")
|
||||
class Base(Generic[T]): ...
|
||||
class Sub(Base[Sub]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
reveal_type(Sub) # revealed: <class 'Sub'>
|
||||
```
|
||||
|
||||
#### With string forward references
|
||||
@@ -451,7 +451,7 @@ T = TypeVar("T")
|
||||
class Base(Generic[T]): ...
|
||||
class Sub(Base["Sub"]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
reveal_type(Sub) # revealed: <class 'Sub'>
|
||||
```
|
||||
|
||||
#### Without string forward references
|
||||
|
||||
@@ -19,7 +19,7 @@ in newer Python releases.
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
reveal_type(type(T)) # revealed: Literal[TypeVar]
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
@@ -123,4 +123,32 @@ from typing import TypeVar
|
||||
T = TypeVar("T", int)
|
||||
```
|
||||
|
||||
### Cannot be both covariant and contravariant
|
||||
|
||||
> To facilitate the declaration of container types where covariant or contravariant type checking is
|
||||
> acceptable, type variables accept keyword arguments `covariant=True` or `contravariant=True`. At
|
||||
> most one of these may be passed.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
# error: [invalid-legacy-type-variable]
|
||||
T = TypeVar("T", covariant=True, contravariant=True)
|
||||
```
|
||||
|
||||
### Variance parameters must be unambiguous
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
def cond() -> bool:
|
||||
return True
|
||||
|
||||
# error: [invalid-legacy-type-variable]
|
||||
T = TypeVar("T", covariant=cond())
|
||||
|
||||
# error: [invalid-legacy-type-variable]
|
||||
U = TypeVar("U", contravariant=cond())
|
||||
```
|
||||
|
||||
[generics]: https://typing.python.org/en/latest/spec/generics.html
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
# Variance: Legacy syntax
|
||||
|
||||
Type variables have a property called _variance_ that affects the subtyping and assignability
|
||||
relations. Much more detail can be found in the [spec]. To summarize, each typevar is either
|
||||
**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not
|
||||
currently mentioned in the typing spec, but is a fourth case that we must consider.)
|
||||
|
||||
For all of the examples below, we will consider typevars `T` and `U`, two generic classes using
|
||||
those typevars `C[T]` and `D[U]`, and two types `A` and `B`.
|
||||
|
||||
(Note that dynamic types like `Any` never participate in subtyping, so `C[Any]` is neither a subtype
|
||||
nor supertype of any other specialization of `C`, regardless of `T`'s variance. It is, however,
|
||||
assignable to any specialization of `C`, regardless of variance, via materialization.)
|
||||
|
||||
## Covariance
|
||||
|
||||
With a covariant typevar, subtyping and assignability are in "alignment": if `A <: B` and `C <: D`,
|
||||
then `C[A] <: C[B]` and `C[A] <: D[B]`.
|
||||
|
||||
Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of
|
||||
`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would
|
||||
get from the sequence is a valid `int`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
T = TypeVar("T", covariant=True)
|
||||
U = TypeVar("U", covariant=True)
|
||||
|
||||
class C(Generic[T]):
|
||||
def receive(self) -> T:
|
||||
raise ValueError
|
||||
|
||||
class D(C[U]):
|
||||
pass
|
||||
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
static_assert(is_assignable_to(D[B], C[A]))
|
||||
static_assert(not is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(is_subtype_of(D[B], C[A]))
|
||||
static_assert(not is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
static_assert(not is_subtype_of(D[Any], C[A]))
|
||||
static_assert(not is_subtype_of(D[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
|
||||
```
|
||||
|
||||
## Contravariance
|
||||
|
||||
With a contravariant typevar, subtyping and assignability are in "opposition": if `A <: B` and
|
||||
`C <: D`, then `C[B] <: C[A]` and `D[B] <: C[A]`.
|
||||
|
||||
Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives
|
||||
`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool`
|
||||
that you pass into the consumer is a valid `int`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
T = TypeVar("T", contravariant=True)
|
||||
U = TypeVar("U", contravariant=True)
|
||||
|
||||
class C(Generic[T]):
|
||||
def send(self, value: T): ...
|
||||
|
||||
class D(C[U]):
|
||||
pass
|
||||
|
||||
static_assert(not is_assignable_to(C[B], C[A]))
|
||||
static_assert(is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_assignable_to(D[B], C[A]))
|
||||
static_assert(is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(D[B], C[A]))
|
||||
static_assert(is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
static_assert(not is_subtype_of(D[Any], C[A]))
|
||||
static_assert(not is_subtype_of(D[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
|
||||
```
|
||||
|
||||
## Invariance
|
||||
|
||||
With an invariant typevar, only equivalent specializations of the generic class are subtypes of or
|
||||
assignable to each other.
|
||||
|
||||
This often occurs for types that are both producers _and_ consumers, like a mutable `list`.
|
||||
Iterating over the elements in a list would work with a covariant typevar, just like with the
|
||||
"producer" type above. Appending elements to a list would work with a contravariant typevar, just
|
||||
like with the "consumer" type above. However, a typevar cannot be both covariant and contravariant
|
||||
at the same time!
|
||||
|
||||
If you expect a mutable list of `int`s, it's not safe for someone to provide you with a mutable list
|
||||
of `bool`s, since you might try to add an element to the list: if you try to add an `int`, the list
|
||||
would no longer only contain elements that are subtypes of `bool`.
|
||||
|
||||
Conversely, if you expect a mutable list of `bool`s, it's not safe for someone to provide you with a
|
||||
mutable list of `int`s, since you might try to extract elements from the list: you expect every
|
||||
element that you extract to be a subtype of `bool`, but the list can contain any `int`.
|
||||
|
||||
In the end, if you expect a mutable list, you must always be given a list of exactly that type,
|
||||
since we can't know in advance which of the allowed methods you'll want to use.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
class C(Generic[T]):
|
||||
def send(self, value: T): ...
|
||||
def receive(self) -> T:
|
||||
raise ValueError
|
||||
|
||||
class D(C[U]):
|
||||
pass
|
||||
|
||||
static_assert(not is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_assignable_to(D[B], C[A]))
|
||||
static_assert(not is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(D[B], C[A]))
|
||||
static_assert(not is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
static_assert(not is_subtype_of(D[Any], C[A]))
|
||||
static_assert(not is_subtype_of(D[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
|
||||
```
|
||||
|
||||
## Bivariance
|
||||
|
||||
With a bivariant typevar, _all_ specializations of the generic class are assignable to (and in fact,
|
||||
gradually equivalent to) each other, and all fully static specializations are subtypes of (and
|
||||
equivalent to) each other.
|
||||
|
||||
It is not possible to construct a legacy typevar that is explicitly bivariant.
|
||||
|
||||
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
|
||||
@@ -362,7 +362,7 @@ Here, `Sub` is not a generic class, since it fills its superclass's type paramet
|
||||
class Base[T]: ...
|
||||
class Sub(Base[Sub]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
reveal_type(Sub) # revealed: <class 'Sub'>
|
||||
```
|
||||
|
||||
#### With string forward references
|
||||
@@ -373,7 +373,7 @@ A similar case can work in a non-stub file, if forward references are stringifie
|
||||
class Base[T]: ...
|
||||
class Sub(Base["Sub"]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
reveal_type(Sub) # revealed: <class 'Sub'>
|
||||
```
|
||||
|
||||
#### Without string forward references
|
||||
|
||||
@@ -16,7 +16,7 @@ instances of `typing.TypeVar`, just like legacy type variables.
|
||||
|
||||
```py
|
||||
def f[T]():
|
||||
reveal_type(type(T)) # revealed: Literal[TypeVar]
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
@@ -320,6 +320,106 @@ def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, An
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
A bound or constrained typevar is a subtype of itself in a union:
|
||||
|
||||
```py
|
||||
def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
|
||||
static_assert(is_assignable_to(T, T | None))
|
||||
static_assert(is_assignable_to(U, U | None))
|
||||
|
||||
static_assert(is_subtype_of(T, T | None))
|
||||
static_assert(is_subtype_of(U, U | None))
|
||||
```
|
||||
|
||||
And an intersection of a typevar with another type is always a subtype of the TypeVar:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection, Not, is_disjoint_from
|
||||
|
||||
class A: ...
|
||||
|
||||
def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
|
||||
static_assert(is_assignable_to(Intersection[T, Unrelated], T))
|
||||
static_assert(is_subtype_of(Intersection[T, Unrelated], T))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[U, A], U))
|
||||
static_assert(is_subtype_of(Intersection[U, A], U))
|
||||
|
||||
static_assert(is_disjoint_from(Not[T], T))
|
||||
static_assert(is_disjoint_from(T, Not[T]))
|
||||
static_assert(is_disjoint_from(Not[U], U))
|
||||
static_assert(is_disjoint_from(U, Not[U]))
|
||||
```
|
||||
|
||||
## Equivalence
|
||||
|
||||
A fully static `TypeVar` is always equivalent to itself, but never to another `TypeVar`, since there
|
||||
is no guarantee that they will be specialized to the same type. (This is true even if both typevars
|
||||
are bounded by the same final class, since you can specialize the typevars to `Never` in addition to
|
||||
that final class.)
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
from ty_extensions import is_equivalent_to, static_assert, is_gradual_equivalent_to
|
||||
|
||||
@final
|
||||
class FinalClass: ...
|
||||
|
||||
@final
|
||||
class SecondFinalClass: ...
|
||||
|
||||
def f[A, B, C: FinalClass, D: FinalClass, E: (FinalClass, SecondFinalClass), F: (FinalClass, SecondFinalClass)]():
|
||||
static_assert(is_equivalent_to(A, A))
|
||||
static_assert(is_equivalent_to(B, B))
|
||||
static_assert(is_equivalent_to(C, C))
|
||||
static_assert(is_equivalent_to(D, D))
|
||||
static_assert(is_equivalent_to(E, E))
|
||||
static_assert(is_equivalent_to(F, F))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(A, A))
|
||||
static_assert(is_gradual_equivalent_to(B, B))
|
||||
static_assert(is_gradual_equivalent_to(C, C))
|
||||
static_assert(is_gradual_equivalent_to(D, D))
|
||||
static_assert(is_gradual_equivalent_to(E, E))
|
||||
static_assert(is_gradual_equivalent_to(F, F))
|
||||
|
||||
static_assert(not is_equivalent_to(A, B))
|
||||
static_assert(not is_equivalent_to(C, D))
|
||||
static_assert(not is_equivalent_to(E, F))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(A, B))
|
||||
static_assert(not is_gradual_equivalent_to(C, D))
|
||||
static_assert(not is_gradual_equivalent_to(E, F))
|
||||
```
|
||||
|
||||
TypeVars which have non-fully-static bounds or constraints do not participate in equivalence
|
||||
relations, but do participate in gradual equivalence relations.
|
||||
|
||||
```py
|
||||
from typing import final, Any
|
||||
from ty_extensions import is_equivalent_to, static_assert, is_gradual_equivalent_to
|
||||
|
||||
# fmt: off
|
||||
|
||||
def f[
|
||||
A: tuple[Any],
|
||||
B: tuple[Any],
|
||||
C: (tuple[Any], tuple[Any, Any]),
|
||||
D: (tuple[Any], tuple[Any, Any])
|
||||
]():
|
||||
static_assert(not is_equivalent_to(A, A))
|
||||
static_assert(not is_equivalent_to(B, B))
|
||||
static_assert(not is_equivalent_to(C, C))
|
||||
static_assert(not is_equivalent_to(D, D))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(A, A))
|
||||
static_assert(is_gradual_equivalent_to(B, B))
|
||||
static_assert(is_gradual_equivalent_to(C, C))
|
||||
static_assert(is_gradual_equivalent_to(D, D))
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
## Singletons and single-valued types
|
||||
|
||||
(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the
|
||||
|
||||
@@ -10,12 +10,17 @@ relations. Much more detail can be found in the [spec]. To summarize, each typev
|
||||
**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not
|
||||
currently mentioned in the typing spec, but is a fourth case that we must consider.)
|
||||
|
||||
For all of the examples below, we will consider a typevar `T`, a generic class using that typevar
|
||||
`C[T]`, and two types `A` and `B`.
|
||||
For all of the examples below, we will consider typevars `T` and `U`, two generic classes using
|
||||
those typevars `C[T]` and `D[U]`, and two types `A` and `B`.
|
||||
|
||||
(Note that dynamic types like `Any` never participate in subtyping, so `C[Any]` is neither a subtype
|
||||
nor supertype of any other specialization of `C`, regardless of `T`'s variance. It is, however,
|
||||
assignable to any specialization of `C`, regardless of variance, via materialization.)
|
||||
|
||||
## Covariance
|
||||
|
||||
With a covariant typevar, subtyping is in "alignment": if `A <: B`, then `C[A] <: C[B]`.
|
||||
With a covariant typevar, subtyping and assignability are in "alignment": if `A <: B` and `C <: D`,
|
||||
then `C[A] <: C[B]` and `C[A] <: D[B]`.
|
||||
|
||||
Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of
|
||||
`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would
|
||||
@@ -32,6 +37,9 @@ class C[T]:
|
||||
def receive(self) -> T:
|
||||
raise ValueError
|
||||
|
||||
class D[U](C[U]):
|
||||
pass
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
@@ -41,6 +49,15 @@ static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[B], C[A]))
|
||||
static_assert(not is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
@@ -50,6 +67,15 @@ static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[B], C[A]))
|
||||
static_assert(not is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
static_assert(not is_subtype_of(D[Any], C[A]))
|
||||
static_assert(not is_subtype_of(D[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||
@@ -59,6 +85,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
@@ -69,11 +104,23 @@ static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
|
||||
```
|
||||
|
||||
## Contravariance
|
||||
|
||||
With a contravariant typevar, subtyping is in "opposition": if `A <: B`, then `C[B] <: C[A]`.
|
||||
With a contravariant typevar, subtyping and assignability are in "opposition": if `A <: B` and
|
||||
`C <: D`, then `C[B] <: C[A]` and `D[B] <: C[A]`.
|
||||
|
||||
Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives
|
||||
`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool`
|
||||
@@ -89,6 +136,9 @@ class B(A): ...
|
||||
class C[T]:
|
||||
def send(self, value: T): ...
|
||||
|
||||
class D[U](C[U]):
|
||||
pass
|
||||
|
||||
static_assert(not is_assignable_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
@@ -98,6 +148,15 @@ static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_assignable_to(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
@@ -107,6 +166,15 @@ static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
static_assert(not is_subtype_of(D[Any], C[A]))
|
||||
static_assert(not is_subtype_of(D[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||
@@ -116,6 +184,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
@@ -126,11 +203,23 @@ static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
|
||||
```
|
||||
|
||||
## Invariance
|
||||
|
||||
With an invariant typevar, _no_ specializations of the generic class are subtypes of each other.
|
||||
With an invariant typevar, only equivalent specializations of the generic class are subtypes of or
|
||||
assignable to each other.
|
||||
|
||||
This often occurs for types that are both producers _and_ consumers, like a mutable `list`.
|
||||
Iterating over the elements in a list would work with a covariant typevar, just like with the
|
||||
@@ -161,6 +250,9 @@ class C[T]:
|
||||
def receive(self) -> T:
|
||||
raise ValueError
|
||||
|
||||
class D[U](C[U]):
|
||||
pass
|
||||
|
||||
static_assert(not is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
@@ -168,6 +260,13 @@ static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_assignable_to(D[B], C[A]))
|
||||
static_assert(not is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
@@ -175,6 +274,13 @@ static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(D[B], C[A]))
|
||||
static_assert(not is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
static_assert(not is_subtype_of(D[Any], C[A]))
|
||||
static_assert(not is_subtype_of(D[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||
@@ -184,6 +290,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
@@ -194,11 +309,23 @@ static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
|
||||
```
|
||||
|
||||
## Bivariance
|
||||
|
||||
With a bivariant typevar, _all_ specializations of the generic class are subtypes of (and in fact,
|
||||
With a bivariant typevar, _all_ specializations of the generic class are assignable to (and in fact,
|
||||
gradually equivalent to) each other, and all fully static specializations are subtypes of (and
|
||||
equivalent to) each other.
|
||||
|
||||
This is a bit of pathological case, which really only happens when the class doesn't use the typevar
|
||||
@@ -215,6 +342,9 @@ class B(A): ...
|
||||
class C[T]:
|
||||
pass
|
||||
|
||||
class D[U](C[U]):
|
||||
pass
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
@@ -226,6 +356,17 @@ static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
@@ -237,6 +378,17 @@ static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
static_assert(not is_subtype_of(D[Any], C[A]))
|
||||
static_assert(not is_subtype_of(D[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
# TODO: no error
|
||||
@@ -250,6 +402,15 @@ static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
@@ -272,6 +433,17 @@ static_assert(is_gradual_equivalent_to(C[Any], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(D[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(D[Any], C[B]))
|
||||
```
|
||||
|
||||
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
from b import C as D
|
||||
|
||||
E = D
|
||||
reveal_type(E) # revealed: Literal[C]
|
||||
reveal_type(E) # revealed: <class 'C'>
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
@@ -21,7 +21,7 @@ class C: ...
|
||||
import b
|
||||
|
||||
D = b.C
|
||||
reveal_type(D) # revealed: Literal[C]
|
||||
reveal_type(D) # revealed: <class 'C'>
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
@@ -35,7 +35,7 @@ class C: ...
|
||||
```py
|
||||
import a.b
|
||||
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
reveal_type(a.b.C) # revealed: <class 'C'>
|
||||
```
|
||||
|
||||
`a/__init__.py`:
|
||||
@@ -54,7 +54,7 @@ class C: ...
|
||||
```py
|
||||
import a.b.c
|
||||
|
||||
reveal_type(a.b.c.C) # revealed: Literal[C]
|
||||
reveal_type(a.b.c.C) # revealed: <class 'C'>
|
||||
```
|
||||
|
||||
`a/__init__.py`:
|
||||
@@ -78,7 +78,7 @@ class C: ...
|
||||
```py
|
||||
import a.b as b
|
||||
|
||||
reveal_type(b.C) # revealed: Literal[C]
|
||||
reveal_type(b.C) # revealed: <class 'C'>
|
||||
```
|
||||
|
||||
`a/__init__.py`:
|
||||
@@ -97,7 +97,7 @@ class C: ...
|
||||
```py
|
||||
import a.b.c as c
|
||||
|
||||
reveal_type(c.C) # revealed: Literal[C]
|
||||
reveal_type(c.C) # revealed: <class 'C'>
|
||||
```
|
||||
|
||||
`a/__init__.py`:
|
||||
|
||||
@@ -16,7 +16,7 @@ Or used implicitly:
|
||||
|
||||
```py
|
||||
reveal_type(chr) # revealed: def chr(i: SupportsIndex, /) -> str
|
||||
reveal_type(str) # revealed: Literal[str]
|
||||
reveal_type(str) # revealed: <class 'str'>
|
||||
```
|
||||
|
||||
## Builtin symbol from custom typeshed
|
||||
|
||||
@@ -95,6 +95,7 @@ from typing import Any, Literal
|
||||
`foo.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
```
|
||||
|
||||
## Nested non-exports
|
||||
@@ -187,7 +188,7 @@ reveal_type(Foo) # revealed: Unknown
|
||||
```pyi
|
||||
from b import AnyFoo as Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[AnyFoo]
|
||||
reveal_type(Foo) # revealed: <class 'AnyFoo'>
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
@@ -201,9 +202,9 @@ class AnyFoo: ...
|
||||
Here, the symbol is re-exported using the `__all__` variable.
|
||||
|
||||
```py
|
||||
# TODO: This should *not* be an error but we don't understand `__all__` yet.
|
||||
# error: "Module `a` has no member `Foo`"
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
@@ -220,6 +221,44 @@ __all__ = ['Foo']
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
## Re-exports with `__all__`
|
||||
|
||||
If a symbol is re-exported via redundant alias but is not included in `__all__`, it shouldn't raise
|
||||
an error when using named import.
|
||||
|
||||
`named_import.py`:
|
||||
|
||||
```py
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
from b import Foo as Foo
|
||||
|
||||
__all__ = []
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
However, a star import _would_ raise an error.
|
||||
|
||||
`star_import.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Re-exports in `__init__.pyi`
|
||||
|
||||
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
|
||||
@@ -251,11 +290,13 @@ class Foo: ...
|
||||
`a/b/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
```
|
||||
|
||||
`a/b/c.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
```
|
||||
|
||||
## Conditional re-export in stub file
|
||||
@@ -281,7 +322,7 @@ def coinflip() -> bool: ...
|
||||
if coinflip():
|
||||
Foo: str = ...
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo] | str
|
||||
reveal_type(Foo) # revealed: <class 'Foo'> | str
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
@@ -299,7 +340,7 @@ the other does not.
|
||||
# error: "Member `Foo` of module `a` is possibly unbound"
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
@@ -312,7 +353,7 @@ if coinflip():
|
||||
else:
|
||||
from b import Foo as Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
@@ -327,7 +368,7 @@ class Foo: ...
|
||||
# error: "Member `Foo` of module `a` is possibly unbound"
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
796
crates/ty_python_semantic/resources/mdtest/import/dunder_all.md
Normal file
796
crates/ty_python_semantic/resources/mdtest/import/dunder_all.md
Normal file
@@ -0,0 +1,796 @@
|
||||
# `__all__`
|
||||
|
||||
Reference:
|
||||
<https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols>
|
||||
|
||||
NOTE: This file only includes the usage of `__all__` for named-imports i.e.,
|
||||
`from module import symbol`. For the usage of `__all__` in wildcard imports, refer to
|
||||
[star.md](star.md).
|
||||
|
||||
## Undefined
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: None
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
## Global scope
|
||||
|
||||
The `__all__` variable is only recognized from the global scope of the module. It is not recognized
|
||||
from the local scope of a function or class.
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
|
||||
def foo():
|
||||
__all__.append("B")
|
||||
|
||||
class Foo:
|
||||
__all__ += ["C"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
foo()
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
## Supported idioms
|
||||
|
||||
According to the [specification], the following idioms are supported:
|
||||
|
||||
### List assignment
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter_annotated.py`:
|
||||
|
||||
```py
|
||||
__all__: list[str] = ["C", "D"]
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import exporter_annotated
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
|
||||
# revealed: tuple[Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter_annotated))
|
||||
```
|
||||
|
||||
### List assignment (shadowed)
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
__all__ = ["C", "D"]
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`exporter_annotated.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["X"]
|
||||
|
||||
class X: ...
|
||||
|
||||
__all__: list[str] = ["Y", "Z"]
|
||||
|
||||
class Y: ...
|
||||
class Z: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import exporter_annotated
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
|
||||
# revealed: tuple[Literal["Y"], Literal["Z"]]
|
||||
reveal_type(dunder_all_names(exporter_annotated))
|
||||
```
|
||||
|
||||
### Tuple assignment
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("A", "B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter_annotated.py`:
|
||||
|
||||
```py
|
||||
__all__: tuple[str, ...] = ("C", "D")
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import exporter_annotated
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
|
||||
# revealed: tuple[Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter_annotated))
|
||||
```
|
||||
|
||||
### Tuple assignment (shadowed)
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("A", "B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
__all__ = ("C", "D")
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`exporter_annotated.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("X",)
|
||||
|
||||
class X: ...
|
||||
|
||||
__all__: tuple[str, ...] = ("Y", "Z")
|
||||
|
||||
class Y: ...
|
||||
class Z: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import exporter_annotated
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
|
||||
# revealed: tuple[Literal["Y"], Literal["Z"]]
|
||||
reveal_type(dunder_all_names(exporter_annotated))
|
||||
```
|
||||
|
||||
### Augmenting list with a list or submodule `__all__`
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import subexporter
|
||||
|
||||
__all__ = []
|
||||
__all__ += ["C", "D"]
|
||||
__all__ += subexporter.__all__
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Extending with a list or submodule `__all__`
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import subexporter
|
||||
|
||||
__all__ = []
|
||||
__all__.extend(["C", "D"])
|
||||
__all__.extend(subexporter.__all__)
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Appending a single symbol
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
__all__.append("B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Removing a single symbol
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
__all__.remove("A")
|
||||
|
||||
# Non-existant symbol in `__all__` at this point
|
||||
# TODO: This raises `ValueError` at runtime, maybe we should raise a diagnostic as well?
|
||||
__all__.remove("C")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Mixed
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = []
|
||||
|
||||
__all__ = ["A"]
|
||||
__all__.append("B")
|
||||
__all__.extend(["C"])
|
||||
__all__.remove("B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import subexporter
|
||||
|
||||
__all__ = []
|
||||
__all__ += ["D"]
|
||||
__all__ += subexporter.__all__
|
||||
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
### Unsupported idioms
|
||||
|
||||
Idioms that are not mentioned in the [specification] are not recognized by `ty` and if they're used,
|
||||
`__all__` is considered to be undefined for that module. This is to avoid false positives.
|
||||
|
||||
`bar.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
import bar as bar
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import foo
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
__all__ = []
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(foo.bar))
|
||||
|
||||
# Only direct attribute access of modules are recognized
|
||||
# TODO: warning diagnostic
|
||||
__all__.extend(foo.bar.__all__)
|
||||
# TODO: warning diagnostic
|
||||
__all__ += foo.bar.__all__
|
||||
|
||||
# Augmented assignment is only allowed when the value is a list expression
|
||||
# TODO: warning diagnostic
|
||||
__all__ += ("C",)
|
||||
|
||||
# Other methods on `list` are not recognized
|
||||
# TODO: warning diagnostic
|
||||
__all__.insert(0, "C")
|
||||
# TODO: warning diagnostic
|
||||
__all__.clear()
|
||||
|
||||
__all__.append("C")
|
||||
# `pop` is not valid; use `remove` instead
|
||||
# TODO: warning diagnostic
|
||||
__all__.pop()
|
||||
|
||||
# Sets are not recognized
|
||||
# TODO: warning diagnostic
|
||||
__all__ = {"C", "D"}
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: None
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Non-string elements
|
||||
|
||||
Similarly, if `__all__` contains any non-string elements, we will consider `__all__` to not be
|
||||
defined for that module. This is also to avoid false positives.
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("A", "B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter1.py`:
|
||||
|
||||
```py
|
||||
import subexporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(subexporter))
|
||||
|
||||
# TODO: warning diagnostic
|
||||
__all__ = ("C", *subexporter.__all__)
|
||||
|
||||
class C: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter1
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: None
|
||||
reveal_type(dunder_all_names(exporter1))
|
||||
```
|
||||
|
||||
## Statically known branches
|
||||
|
||||
### Python 3.10
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
__all__ = ["AllVersion"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
__all__ += ["Python312"]
|
||||
elif sys.version_info >= (3, 11):
|
||||
__all__ += ["Python311"]
|
||||
else:
|
||||
__all__ += ["Python310"]
|
||||
|
||||
class AllVersion: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class Python312: ...
|
||||
|
||||
elif sys.version_info >= (3, 11):
|
||||
class Python311: ...
|
||||
|
||||
else:
|
||||
class Python310: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["AllVersion"], Literal["Python310"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Python 3.11
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
__all__ = ["AllVersion"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
__all__ += ["Python312"]
|
||||
elif sys.version_info >= (3, 11):
|
||||
__all__ += ["Python311"]
|
||||
else:
|
||||
__all__ += ["Python310"]
|
||||
|
||||
class AllVersion: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class Python312: ...
|
||||
|
||||
elif sys.version_info >= (3, 11):
|
||||
class Python311: ...
|
||||
|
||||
else:
|
||||
class Python310: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["AllVersion"], Literal["Python311"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Python 3.12
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
__all__ = ["AllVersion"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
__all__ += ["Python312"]
|
||||
elif sys.version_info >= (3, 11):
|
||||
__all__ += ["Python311"]
|
||||
else:
|
||||
__all__ += ["Python310"]
|
||||
|
||||
class AllVersion: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class Python312: ...
|
||||
|
||||
elif sys.version_info >= (3, 11):
|
||||
class Python311: ...
|
||||
|
||||
else:
|
||||
class Python310: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["AllVersion"], Literal["Python312"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Multiple `if` statements
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
__all__ = ["AllVersion"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
__all__ += ["Python312"]
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
__all__ += ["Python311"]
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
__all__ += ["Python310"]
|
||||
|
||||
class AllVersion: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class Python312: ...
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
class Python311: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
class Python310: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["AllVersion"], Literal["Python310"], Literal["Python311"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
## Origin
|
||||
|
||||
`__all__` can be defined in a module mainly in the following three ways:
|
||||
|
||||
### Directly in the module
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
|
||||
class A: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Using named import
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
|
||||
class A: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
from subexporter import __all__
|
||||
|
||||
__all__.append("B")
|
||||
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import subexporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"]]
|
||||
reveal_type(dunder_all_names(subexporter))
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Using wildcard import (1)
|
||||
|
||||
Wildcard import doesn't export `__all__` unless it is explicitly included in the `__all__` of the
|
||||
module.
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "__all__"]
|
||||
|
||||
class A: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
from subexporter import *
|
||||
|
||||
# TODO: Should be `list[str]`
|
||||
# TODO: Should we avoid including `Unknown` for this case?
|
||||
reveal_type(__all__) # revealed: Unknown | list
|
||||
|
||||
__all__.append("B")
|
||||
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import subexporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["__all__"]]
|
||||
reveal_type(dunder_all_names(subexporter))
|
||||
# revealed: tuple[Literal["A"], Literal["B"], Literal["__all__"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Using wildcard import (2)
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
|
||||
class A: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
from subexporter import *
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(__all__) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-reference]
|
||||
__all__.append("B")
|
||||
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import subexporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"]]
|
||||
reveal_type(dunder_all_names(subexporter))
|
||||
# revealed: None
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
[specification]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols
|
||||
@@ -69,12 +69,12 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]]
|
||||
reveal_type(A.__mro__) # revealed: tuple[<class 'A'>, <class 'object'>]
|
||||
import b
|
||||
|
||||
class C(b.B): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>]
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
@@ -84,5 +84,5 @@ from a import A
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[<class 'B'>, <class 'A'>, <class 'object'>]
|
||||
```
|
||||
|
||||
@@ -255,7 +255,7 @@ python-version = "3.13"
|
||||
```py
|
||||
from foo import A
|
||||
|
||||
reveal_type(A) # revealed: Literal[A]
|
||||
reveal_type(A) # revealed: <class 'A'>
|
||||
```
|
||||
|
||||
`/src/.venv/<path-to-site-packages>/foo/__init__.py`:
|
||||
|
||||
@@ -899,8 +899,8 @@ reveal_type(__protected) # revealed: bool
|
||||
reveal_type(__dunder__) # revealed: bool
|
||||
reveal_type(___thunder___) # revealed: bool
|
||||
|
||||
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
|
||||
reveal_type(Y) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Simple list `__all__`
|
||||
@@ -921,8 +921,8 @@ from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
|
||||
reveal_type(Y) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `__all__` with additions later on in the global scope
|
||||
@@ -949,15 +949,13 @@ __all__ = ["A"]
|
||||
__all__ += ["B"]
|
||||
__all__.append("C")
|
||||
__all__.extend(["D"])
|
||||
__all__.extend(("E",))
|
||||
__all__.extend(a.__all__)
|
||||
|
||||
A: bool = True
|
||||
B: bool = True
|
||||
C: bool = True
|
||||
D: bool = True
|
||||
E: bool = True
|
||||
F: bool = False
|
||||
E: bool = False
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
@@ -969,11 +967,10 @@ reveal_type(A) # revealed: bool
|
||||
reveal_type(B) # revealed: bool
|
||||
reveal_type(C) # revealed: bool
|
||||
reveal_type(D) # revealed: bool
|
||||
reveal_type(E) # revealed: bool
|
||||
reveal_type(FOO) # revealed: bool
|
||||
|
||||
# TODO should error with [unresolved-reference] & reveal `Unknown`
|
||||
reveal_type(F) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(E) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `__all__` with subtractions later on in the global scope
|
||||
@@ -985,7 +982,7 @@ one way of subtracting from `__all__` that type checkers are required to support
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
__all__.remove("A")
|
||||
__all__.remove("B")
|
||||
|
||||
A: bool = True
|
||||
B: bool = True
|
||||
@@ -998,8 +995,8 @@ from exporter import *
|
||||
|
||||
reveal_type(A) # revealed: bool
|
||||
|
||||
# TODO should emit an [unresolved-reference] diagnostic & reveal `Unknown`
|
||||
reveal_type(B) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(B) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Invalid `__all__`
|
||||
@@ -1125,8 +1122,8 @@ else:
|
||||
```py
|
||||
from exporter import *
|
||||
|
||||
# TODO: should reveal `Unknown` and emit `[unresolved-reference]`
|
||||
reveal_type(X) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
@@ -1199,8 +1196,8 @@ else:
|
||||
```py
|
||||
from exporter import *
|
||||
|
||||
# TODO: should reveal `Unknown` & emit `[unresolved-reference]
|
||||
reveal_type(X) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
@@ -1235,9 +1232,11 @@ __all__ = []
|
||||
from a import *
|
||||
from b import *
|
||||
|
||||
# TODO: both of these should have [unresolved-reference] diagnostics and reveal `Unknown`
|
||||
reveal_type(X) # revealed: bool
|
||||
reveal_type(Y) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `__all__` in a stub file
|
||||
@@ -1257,7 +1256,11 @@ Y: bool = True
|
||||
```pyi
|
||||
from a import X, Y
|
||||
|
||||
__all__ = ["X"]
|
||||
__all__ = ["X", "Z"]
|
||||
|
||||
Z: bool = True
|
||||
|
||||
Nope: bool = True
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
@@ -1265,18 +1268,21 @@ __all__ = ["X"]
|
||||
```py
|
||||
from b import *
|
||||
|
||||
# TODO: should not error, should reveal `bool`
|
||||
# (`X` is re-exported from `b.pyi` due to presence in `__all__`)
|
||||
# See https://github.com/astral-sh/ruff/issues/16159
|
||||
#
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
# `X` is re-exported from `b.pyi` due to presence in `__all__`
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
# This diagnostic is accurate: `Y` does not use the "redundant alias" convention in `b.pyi`,
|
||||
# nor is it included in `b.__all__`, so it is not exported from `b.pyi`
|
||||
# nor is it included in `b.__all__`, so it is not exported from `b.pyi`. It would still be
|
||||
# an error if it used the "redundant alias" convention as `__all__` would take precedence.
|
||||
#
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
|
||||
# `Z` is defined in `b.pyi` and included in `__all__`
|
||||
reveal_type(Z) # revealed: bool
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Nope) # revealed: Unknown
|
||||
```
|
||||
|
||||
## `global` statements in non-global scopes
|
||||
@@ -1353,12 +1359,9 @@ are present due to `*` imports.
|
||||
```py
|
||||
import collections.abc
|
||||
|
||||
reveal_type(collections.abc.Sequence) # revealed: Literal[Sequence]
|
||||
reveal_type(collections.abc.Sequence) # revealed: <class 'Sequence'>
|
||||
reveal_type(collections.abc.Callable) # revealed: typing.Callable
|
||||
|
||||
# TODO: false positive as it's only re-exported from `_collections.abc` due to presence in `__all__`
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(collections.abc.Set) # revealed: Unknown
|
||||
reveal_type(collections.abc.Set) # revealed: <class 'AbstractSet'>
|
||||
```
|
||||
|
||||
## Invalid `*` imports
|
||||
|
||||
@@ -27,7 +27,7 @@ has been imported.
|
||||
import a
|
||||
|
||||
# Would be an error with flow-sensitive tracking
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
reveal_type(a.b.C) # revealed: <class 'C'>
|
||||
|
||||
import a.b
|
||||
```
|
||||
@@ -53,10 +53,10 @@ submodule `b`, even though `a.b` is never imported in the main module.
|
||||
from q import a, b
|
||||
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(b.C) # revealed: Literal[C]
|
||||
reveal_type(b.C) # revealed: <class 'C'>
|
||||
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
reveal_type(a.b.C) # revealed: <class 'C'>
|
||||
```
|
||||
|
||||
`a/__init__.py`:
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
```py
|
||||
class M(type): ...
|
||||
|
||||
reveal_type(M.__class__) # revealed: Literal[type]
|
||||
reveal_type(M.__class__) # revealed: <class 'type'>
|
||||
```
|
||||
|
||||
## `object`
|
||||
|
||||
```py
|
||||
reveal_type(object.__class__) # revealed: Literal[type]
|
||||
reveal_type(object.__class__) # revealed: <class 'type'>
|
||||
```
|
||||
|
||||
## `type`
|
||||
|
||||
```py
|
||||
reveal_type(type.__class__) # revealed: Literal[type]
|
||||
reveal_type(type.__class__) # revealed: <class 'type'>
|
||||
```
|
||||
|
||||
## Basic
|
||||
@@ -24,7 +24,7 @@ reveal_type(type.__class__) # revealed: Literal[type]
|
||||
class M(type): ...
|
||||
class B(metaclass=M): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
reveal_type(B.__class__) # revealed: <class 'M'>
|
||||
```
|
||||
|
||||
## Invalid metaclass
|
||||
@@ -37,7 +37,7 @@ class M: ...
|
||||
class A(metaclass=M): ...
|
||||
|
||||
# TODO: emit a diagnostic for the invalid metaclass
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
reveal_type(A.__class__) # revealed: <class 'M'>
|
||||
```
|
||||
|
||||
## Linear inheritance
|
||||
@@ -50,7 +50,7 @@ class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
reveal_type(B.__class__) # revealed: <class 'M'>
|
||||
```
|
||||
|
||||
## Linear inheritance with PEP 695 generic class
|
||||
@@ -68,8 +68,8 @@ class A[T](metaclass=M): ...
|
||||
class B(A): ...
|
||||
class C(A[int]): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
reveal_type(C.__class__) # revealed: Literal[M]
|
||||
reveal_type(B.__class__) # revealed: <class 'M'>
|
||||
reveal_type(C.__class__) # revealed: <class 'M'>
|
||||
```
|
||||
|
||||
## Conflict (1)
|
||||
@@ -117,7 +117,7 @@ class A(metaclass=M): ...
|
||||
class B(metaclass=M): ...
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Literal[M]
|
||||
reveal_type(C.__class__) # revealed: <class 'M'>
|
||||
```
|
||||
|
||||
## Metaclass metaclass
|
||||
@@ -131,7 +131,7 @@ class M3(M2): ...
|
||||
class A(metaclass=M3): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M3]
|
||||
reveal_type(A.__class__) # revealed: <class 'M3'>
|
||||
```
|
||||
|
||||
## Diamond inheritance
|
||||
@@ -159,14 +159,14 @@ from nonexistent_module import UnknownClass # error: [unresolved-import]
|
||||
class C(UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[type] & Unknown`
|
||||
reveal_type(C.__class__) # revealed: Literal[type]
|
||||
reveal_type(C.__class__) # revealed: <class 'type'>
|
||||
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[M] & Unknown`
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
reveal_type(B.__class__) # revealed: <class 'M'>
|
||||
```
|
||||
|
||||
## Duplicate
|
||||
@@ -176,7 +176,7 @@ class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, A): ... # error: [duplicate-base] "Duplicate base class `A`"
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
reveal_type(B.__class__) # revealed: <class 'M'>
|
||||
```
|
||||
|
||||
## Non-class
|
||||
@@ -191,14 +191,14 @@ def f(*args, **kwargs) -> int:
|
||||
class A(metaclass=f): ...
|
||||
|
||||
# TODO: Should be `int`
|
||||
reveal_type(A) # revealed: Literal[A]
|
||||
reveal_type(A) # revealed: <class 'A'>
|
||||
reveal_type(A.__class__) # revealed: type[int]
|
||||
|
||||
def _(n: int):
|
||||
# error: [invalid-metaclass]
|
||||
class B(metaclass=n): ...
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(B) # revealed: Literal[B]
|
||||
reveal_type(B) # revealed: <class 'B'>
|
||||
reveal_type(B.__class__) # revealed: type[Unknown]
|
||||
|
||||
def _(flag: bool):
|
||||
@@ -207,7 +207,7 @@ def _(flag: bool):
|
||||
# error: [invalid-metaclass]
|
||||
class C(metaclass=m): ...
|
||||
# TODO: Should be `int | Unknown`
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
reveal_type(C) # revealed: <class 'C'>
|
||||
reveal_type(C.__class__) # revealed: type[Unknown]
|
||||
|
||||
class SignatureMismatch: ...
|
||||
@@ -216,9 +216,9 @@ class SignatureMismatch: ...
|
||||
class D(metaclass=SignatureMismatch): ...
|
||||
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(D) # revealed: Literal[D]
|
||||
reveal_type(D) # revealed: <class 'D'>
|
||||
# TODO: Should be `type[Unknown]`
|
||||
reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
|
||||
reveal_type(D.__class__) # revealed: <class 'SignatureMismatch'>
|
||||
```
|
||||
|
||||
## Cyclic
|
||||
@@ -244,7 +244,7 @@ python-version = "3.12"
|
||||
class M(type): ...
|
||||
class A[T: str](metaclass=M): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
reveal_type(A.__class__) # revealed: <class 'M'>
|
||||
```
|
||||
|
||||
## Metaclasses of metaclasses
|
||||
@@ -255,9 +255,9 @@ class Bar(type, metaclass=Foo): ...
|
||||
class Baz(type, metaclass=Bar): ...
|
||||
class Spam(metaclass=Baz): ...
|
||||
|
||||
reveal_type(Spam.__class__) # revealed: Literal[Baz]
|
||||
reveal_type(Spam.__class__.__class__) # revealed: Literal[Bar]
|
||||
reveal_type(Spam.__class__.__class__.__class__) # revealed: Literal[Foo]
|
||||
reveal_type(Spam.__class__) # revealed: <class 'Baz'>
|
||||
reveal_type(Spam.__class__.__class__) # revealed: <class 'Bar'>
|
||||
reveal_type(Spam.__class__.__class__.__class__) # revealed: <class 'Foo'>
|
||||
|
||||
def test(x: Spam):
|
||||
reveal_type(x.__class__) # revealed: type[Spam]
|
||||
|
||||
@@ -16,13 +16,13 @@ For documentation on method resolution orders, see:
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'object'>]
|
||||
```
|
||||
|
||||
## The special case: `object` itself
|
||||
|
||||
```py
|
||||
reveal_type(object.__mro__) # revealed: tuple[Literal[object]]
|
||||
reveal_type(object.__mro__) # revealed: tuple[<class 'object'>]
|
||||
```
|
||||
|
||||
## Explicit inheritance from `object`
|
||||
@@ -30,7 +30,7 @@ reveal_type(object.__mro__) # revealed: tuple[Literal[object]]
|
||||
```py
|
||||
class C(object): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'object'>]
|
||||
```
|
||||
|
||||
## Explicit inheritance from non-`object` single base
|
||||
@@ -39,7 +39,7 @@ reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[<class 'B'>, <class 'A'>, <class 'object'>]
|
||||
```
|
||||
|
||||
## Linearization of multiple bases
|
||||
@@ -49,7 +49,7 @@ class A: ...
|
||||
class B: ...
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'A'>, <class 'B'>, <class 'object'>]
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (1)
|
||||
@@ -63,8 +63,8 @@ class Y(O): ...
|
||||
class A(X, Y): ...
|
||||
class B(Y, X): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__) # revealed: tuple[<class 'A'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(B.__mro__) # revealed: tuple[<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>]
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (2)
|
||||
@@ -80,11 +80,11 @@ class C(D, F): ...
|
||||
class B(D, E): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'C'>, <class 'D'>, <class 'F'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(C.__mro__)
|
||||
# revealed: tuple[Literal[B], Literal[D], Literal[E], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'B'>, <class 'D'>, <class 'E'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(B.__mro__)
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[C], Literal[D], Literal[E], Literal[F], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'A'>, <class 'B'>, <class 'C'>, <class 'D'>, <class 'E'>, <class 'F'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(A.__mro__)
|
||||
```
|
||||
|
||||
@@ -101,11 +101,11 @@ class C(D, F): ...
|
||||
class B(E, D): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'C'>, <class 'D'>, <class 'F'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(C.__mro__)
|
||||
# revealed: tuple[Literal[B], Literal[E], Literal[D], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'B'>, <class 'E'>, <class 'D'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(B.__mro__)
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'A'>, <class 'B'>, <class 'E'>, <class 'C'>, <class 'D'>, <class 'F'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(A.__mro__)
|
||||
```
|
||||
|
||||
@@ -125,13 +125,13 @@ class K2(D, B, E): ...
|
||||
class K3(D, A): ...
|
||||
class Z(K1, K2, K3): ...
|
||||
|
||||
# revealed: tuple[Literal[K1], Literal[A], Literal[B], Literal[C], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'K1'>, <class 'A'>, <class 'B'>, <class 'C'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(K1.__mro__)
|
||||
# revealed: tuple[Literal[K2], Literal[D], Literal[B], Literal[E], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'K2'>, <class 'D'>, <class 'B'>, <class 'E'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(K2.__mro__)
|
||||
# revealed: tuple[Literal[K3], Literal[D], Literal[A], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'K3'>, <class 'D'>, <class 'A'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(K3.__mro__)
|
||||
# revealed: tuple[Literal[Z], Literal[K1], Literal[K2], Literal[K3], Literal[D], Literal[A], Literal[B], Literal[C], Literal[E], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'Z'>, <class 'K1'>, <class 'K2'>, <class 'K3'>, <class 'D'>, <class 'A'>, <class 'B'>, <class 'C'>, <class 'E'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(Z.__mro__)
|
||||
```
|
||||
|
||||
@@ -147,10 +147,11 @@ class D(A, B, C): ...
|
||||
class E(B, C): ...
|
||||
class F(E, A): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
|
||||
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[A], Unknown, Literal[B], Literal[C], Literal[object]]
|
||||
reveal_type(E.__mro__) # revealed: tuple[Literal[E], Literal[B], Literal[C], Literal[object]]
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], Literal[E], Literal[B], Literal[C], Literal[A], Unknown, Literal[object]]
|
||||
reveal_type(A.__mro__) # revealed: tuple[<class 'A'>, Unknown, <class 'object'>]
|
||||
reveal_type(D.__mro__) # revealed: tuple[<class 'D'>, <class 'A'>, Unknown, <class 'B'>, <class 'C'>, <class 'object'>]
|
||||
reveal_type(E.__mro__) # revealed: tuple[<class 'E'>, <class 'B'>, <class 'C'>, <class 'object'>]
|
||||
# revealed: tuple[<class 'F'>, <class 'E'>, <class 'B'>, <class 'C'>, <class 'A'>, Unknown, <class 'object'>]
|
||||
reveal_type(F.__mro__)
|
||||
```
|
||||
|
||||
## `__bases__` lists that cause errors at runtime
|
||||
@@ -162,11 +163,11 @@ creation to fail, we infer the class's `__mro__` as being `[<class>, Unknown, ob
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[<class 'object'>, <class 'int'>]`"
|
||||
class Foo(object, int): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
|
||||
class Bar(Foo): ...
|
||||
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, <class 'Foo'>, Unknown, <class 'object'>]
|
||||
|
||||
# This is the `TypeError` at the bottom of "ex_2"
|
||||
# in the examples at <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
@@ -176,17 +177,17 @@ class Y(O): ...
|
||||
class A(X, Y): ...
|
||||
class B(Y, X): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__) # revealed: tuple[<class 'A'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(B.__mro__) # revealed: tuple[<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>]
|
||||
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[<class 'A'>, <class 'B'>]`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
|
||||
reveal_type(Z.__mro__) # revealed: tuple[<class 'Z'>, Unknown, <class 'object'>]
|
||||
|
||||
class AA(Z): ...
|
||||
|
||||
reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Literal[object]]
|
||||
reveal_type(AA.__mro__) # revealed: tuple[<class 'AA'>, <class 'Z'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## `__bases__` includes a `Union`
|
||||
@@ -207,12 +208,12 @@ if returns_bool():
|
||||
else:
|
||||
x = B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
|
||||
|
||||
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Foo(x): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## `__bases__` includes multiple `Union`s
|
||||
@@ -236,14 +237,14 @@ if returns_bool():
|
||||
else:
|
||||
y = D
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(y) # revealed: Literal[C, D]
|
||||
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
|
||||
reveal_type(y) # revealed: <class 'C'> | <class 'D'>
|
||||
|
||||
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
# error: 14 [invalid-base] "Invalid class base with type `Literal[C, D]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
# error: 14 [invalid-base] "Invalid class base with type `<class 'C'> | <class 'D'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Foo(x, y): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## `__bases__` lists that cause errors... now with `Union`s
|
||||
@@ -261,14 +262,14 @@ if returns_bool():
|
||||
else:
|
||||
foo = object
|
||||
|
||||
# error: 21 [invalid-base] "Invalid class base with type `Literal[Y, object]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
# error: 21 [invalid-base] "Invalid class base with type `<class 'Y'> | <class 'object'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class PossibleError(foo, X): ...
|
||||
|
||||
reveal_type(PossibleError.__mro__) # revealed: tuple[Literal[PossibleError], Unknown, Literal[object]]
|
||||
reveal_type(PossibleError.__mro__) # revealed: tuple[<class 'PossibleError'>, Unknown, <class 'object'>]
|
||||
|
||||
class A(X, Y): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__) # revealed: tuple[<class 'A'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>]
|
||||
|
||||
if returns_bool():
|
||||
class B(X, Y): ...
|
||||
@@ -276,37 +277,116 @@ if returns_bool():
|
||||
else:
|
||||
class B(Y, X): ...
|
||||
|
||||
# revealed: tuple[Literal[B], Literal[X], Literal[Y], Literal[O], Literal[object]] | tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
# revealed: tuple[<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>] | tuple[<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>]
|
||||
reveal_type(B.__mro__)
|
||||
|
||||
# error: 12 [invalid-base] "Invalid class base with type `Literal[B, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
# error: 12 [invalid-base] "Invalid class base with type `<class 'B'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
|
||||
reveal_type(Z.__mro__) # revealed: tuple[<class 'Z'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## `__bases__` lists with duplicate bases
|
||||
|
||||
```py
|
||||
class Foo(str, str): ... # error: 16 [duplicate-base] "Duplicate base class `str`"
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`"
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
|
||||
class Spam: ...
|
||||
class Eggs: ...
|
||||
class Bar: ...
|
||||
class Baz: ...
|
||||
|
||||
# fmt: off
|
||||
|
||||
# error: [duplicate-base] "Duplicate base class `Spam`"
|
||||
# error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
class Ham(
|
||||
Spam,
|
||||
Eggs,
|
||||
Spam, # error: [duplicate-base] "Duplicate base class `Spam`"
|
||||
Eggs, # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
Bar,
|
||||
Baz,
|
||||
Spam,
|
||||
Eggs,
|
||||
): ...
|
||||
|
||||
reveal_type(Ham.__mro__) # revealed: tuple[Literal[Ham], Unknown, Literal[object]]
|
||||
# fmt: on
|
||||
|
||||
reveal_type(Ham.__mro__) # revealed: tuple[<class 'Ham'>, Unknown, <class 'object'>]
|
||||
|
||||
class Mushrooms: ...
|
||||
class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
|
||||
|
||||
reveal_type(Omelette.__mro__) # revealed: tuple[Literal[Omelette], Unknown, Literal[object]]
|
||||
reveal_type(Omelette.__mro__) # revealed: tuple[<class 'Omelette'>, Unknown, <class 'object'>]
|
||||
|
||||
# fmt: off
|
||||
|
||||
# error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
class VeryEggyOmelette(
|
||||
Eggs,
|
||||
Ham,
|
||||
Spam,
|
||||
Eggs,
|
||||
Mushrooms,
|
||||
Bar,
|
||||
Eggs,
|
||||
Baz,
|
||||
Eggs,
|
||||
): ...
|
||||
|
||||
# fmt: off
|
||||
```
|
||||
|
||||
A `type: ignore` comment can suppress `duplicate-bases` errors if it is on the first or last line of
|
||||
the class "header":
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
class A: ...
|
||||
|
||||
class B( # type: ignore[duplicate-base]
|
||||
A,
|
||||
A,
|
||||
): ...
|
||||
|
||||
class C(
|
||||
A,
|
||||
A
|
||||
): # type: ignore[duplicate-base]
|
||||
x: int
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
But it will not suppress the error if it occurs in the class body, or on the duplicate base itself.
|
||||
The justification for this is that it is the class definition as a whole that will raise an
|
||||
exception at runtime, not a sub-expression in the class's bases list.
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
# error: [duplicate-base]
|
||||
class D(
|
||||
A,
|
||||
# error: [unused-ignore-comment]
|
||||
A, # type: ignore[duplicate-base]
|
||||
): ...
|
||||
|
||||
# error: [duplicate-base]
|
||||
class E(
|
||||
A,
|
||||
A
|
||||
):
|
||||
# error: [unused-ignore-comment]
|
||||
x: int # type: ignore[duplicate-base]
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
## `__bases__` lists with duplicate `Unknown` bases
|
||||
@@ -330,7 +410,7 @@ reveal_type(unknown_object_2) # revealed: Unknown
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[Unknown, Unknown]`"
|
||||
class Foo(unknown_object_1, unknown_object_2): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes
|
||||
@@ -349,15 +429,15 @@ These are invalid, but we need to be able to handle them gracefully without pani
|
||||
```pyi
|
||||
class Foo(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
|
||||
class Bar: ...
|
||||
class Baz: ...
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Boz) # revealed: Literal[Boz]
|
||||
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
|
||||
reveal_type(Boz) # revealed: <class 'Boz'>
|
||||
reveal_type(Boz.__mro__) # revealed: tuple[<class 'Boz'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## Classes with indirect cycles in their MROs
|
||||
@@ -369,9 +449,9 @@ class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, Unknown, <class 'object'>]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[<class 'Baz'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## Classes with cycles in their MROs, and multiple inheritance
|
||||
@@ -382,9 +462,9 @@ class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, Unknown, <class 'object'>]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[<class 'Baz'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## Classes with cycles in their MRO, and a sub-graph
|
||||
@@ -400,8 +480,8 @@ class Bar(Foo): ...
|
||||
class Baz(Bar, BarCycle): ...
|
||||
class Spam(Baz): ...
|
||||
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
reveal_type(Spam.__mro__) # revealed: tuple[Literal[Spam], Unknown, Literal[object]]
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[<class 'FooCycle'>, Unknown, <class 'object'>]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[<class 'BarCycle'>, Unknown, <class 'object'>]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[<class 'Baz'>, Unknown, <class 'object'>]
|
||||
reveal_type(Spam.__mro__) # revealed: tuple[<class 'Spam'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
@@ -43,9 +43,9 @@ def _(flag: bool):
|
||||
C = A if flag else B
|
||||
|
||||
if C != A:
|
||||
reveal_type(C) # revealed: Literal[B]
|
||||
reveal_type(C) # revealed: <class 'B'>
|
||||
else:
|
||||
reveal_type(C) # revealed: Literal[A]
|
||||
reveal_type(C) # revealed: <class 'A'>
|
||||
```
|
||||
|
||||
## `x != y` where `y` has multiple single-valued options
|
||||
|
||||
@@ -14,15 +14,15 @@ def _(flag: bool):
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
if issubclass(t, object):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
reveal_type(t) # revealed: <class 'int'>
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
reveal_type(t) # revealed: <class 'str'>
|
||||
|
||||
if issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
reveal_type(t) # revealed: <class 'str'>
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
@@ -34,16 +34,16 @@ def _(flag1: bool, flag2: bool):
|
||||
t = int if flag1 else str if flag2 else bytes
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
reveal_type(t) # revealed: <class 'int'>
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str, bytes]
|
||||
reveal_type(t) # revealed: <class 'str'> | <class 'bytes'>
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
reveal_type(t) # revealed: <class 'int'>
|
||||
elif issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
reveal_type(t) # revealed: <class 'str'>
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[bytes]
|
||||
reveal_type(t) # revealed: <class 'bytes'>
|
||||
```
|
||||
|
||||
### Multiple derived classes
|
||||
@@ -58,24 +58,24 @@ def _(flag1: bool, flag2: bool, flag3: bool):
|
||||
t1 = Derived1 if flag1 else Derived2
|
||||
|
||||
if issubclass(t1, Base):
|
||||
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
|
||||
reveal_type(t1) # revealed: <class 'Derived1'> | <class 'Derived2'>
|
||||
|
||||
if issubclass(t1, Derived1):
|
||||
reveal_type(t1) # revealed: Literal[Derived1]
|
||||
reveal_type(t1) # revealed: <class 'Derived1'>
|
||||
else:
|
||||
reveal_type(t1) # revealed: Literal[Derived2]
|
||||
reveal_type(t1) # revealed: <class 'Derived2'>
|
||||
|
||||
t2 = Derived1 if flag2 else Base
|
||||
|
||||
if issubclass(t2, Base):
|
||||
reveal_type(t2) # revealed: Literal[Derived1, Base]
|
||||
reveal_type(t2) # revealed: <class 'Derived1'> | <class 'Base'>
|
||||
|
||||
t3 = Derived1 if flag3 else Unrelated
|
||||
|
||||
if issubclass(t3, Base):
|
||||
reveal_type(t3) # revealed: Literal[Derived1]
|
||||
reveal_type(t3) # revealed: <class 'Derived1'>
|
||||
else:
|
||||
reveal_type(t3) # revealed: Literal[Unrelated]
|
||||
reveal_type(t3) # revealed: <class 'Unrelated'>
|
||||
```
|
||||
|
||||
### Narrowing for non-literals
|
||||
@@ -109,10 +109,10 @@ def _(flag: bool):
|
||||
t = int if flag else NoneType
|
||||
|
||||
if issubclass(t, NoneType):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
reveal_type(t) # revealed: <class 'NoneType'>
|
||||
|
||||
if issubclass(t, type(None)):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
reveal_type(t) # revealed: <class 'NoneType'>
|
||||
```
|
||||
|
||||
## `classinfo` contains multiple types
|
||||
@@ -126,9 +126,9 @@ def _(flag1: bool, flag2: bool):
|
||||
t = int if flag1 else str if flag2 else bytes
|
||||
|
||||
if issubclass(t, (int, (Unrelated, (bytes,)))):
|
||||
reveal_type(t) # revealed: Literal[int, bytes]
|
||||
reveal_type(t) # revealed: <class 'int'> | <class 'bytes'>
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
reveal_type(t) # revealed: <class 'str'>
|
||||
```
|
||||
|
||||
## Special cases
|
||||
@@ -175,7 +175,7 @@ def flag() -> bool:
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is aliased
|
||||
@@ -188,7 +188,7 @@ def flag() -> bool:
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass_alias(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
reveal_type(t) # revealed: <class 'int'>
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is imported
|
||||
@@ -201,7 +201,7 @@ def flag() -> bool:
|
||||
|
||||
t = int if flag() else str
|
||||
if imported_issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
reveal_type(t) # revealed: <class 'int'>
|
||||
```
|
||||
|
||||
### Do not narrow if second argument is not a proper `classinfo` argument
|
||||
@@ -217,17 +217,17 @@ t = int if flag() else str
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, "str"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, (bytes, "str")):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, Any):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
|
||||
```
|
||||
|
||||
### Do not narrow if there are keyword arguments
|
||||
@@ -240,7 +240,7 @@ t = int if flag() else str
|
||||
|
||||
# error: [unknown-argument]
|
||||
if issubclass(t, int, foo="bar"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
|
||||
```
|
||||
|
||||
### `type[]` types are narrowed as well as class-literal types
|
||||
|
||||
@@ -108,12 +108,12 @@ def flag() -> bool:
|
||||
return True
|
||||
|
||||
x = int if flag() else str
|
||||
reveal_type(x) # revealed: Literal[int, str]
|
||||
reveal_type(x) # revealed: <class 'int'> | <class 'str'>
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: (Literal[int] & ~AlwaysFalsy) | (Literal[str] & ~AlwaysFalsy)
|
||||
reveal_type(x) # revealed: (<class 'int'> & ~AlwaysFalsy) | (<class 'str'> & ~AlwaysFalsy)
|
||||
else:
|
||||
reveal_type(x) # revealed: (Literal[int] & ~AlwaysTruthy) | (Literal[str] & ~AlwaysTruthy)
|
||||
reveal_type(x) # revealed: (<class 'int'> & ~AlwaysTruthy) | (<class 'str'> & ~AlwaysTruthy)
|
||||
```
|
||||
|
||||
## Determined Truthiness
|
||||
@@ -276,12 +276,12 @@ def _(
|
||||
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
|
||||
|
||||
tf = TruthyClass if flag else FalsyClass
|
||||
reveal_type(tf) # revealed: Literal[TruthyClass, FalsyClass]
|
||||
reveal_type(tf) # revealed: <class 'TruthyClass'> | <class 'FalsyClass'>
|
||||
|
||||
if tf:
|
||||
reveal_type(tf) # revealed: Literal[TruthyClass]
|
||||
reveal_type(tf) # revealed: <class 'TruthyClass'>
|
||||
else:
|
||||
reveal_type(tf) # revealed: Literal[FalsyClass]
|
||||
reveal_type(tf) # revealed: <class 'FalsyClass'>
|
||||
```
|
||||
|
||||
## Narrowing in chained boolean expressions
|
||||
|
||||
@@ -150,7 +150,7 @@ def _(x: Base):
|
||||
```py
|
||||
def _(x: object):
|
||||
if (y := type(x)) is bool:
|
||||
reveal_type(y) # revealed: Literal[bool]
|
||||
reveal_type(y) # revealed: <class 'bool'>
|
||||
if (type(y := x)) is bool:
|
||||
reveal_type(y) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -28,7 +28,7 @@ from typing import Protocol
|
||||
|
||||
class MyProtocol(Protocol): ...
|
||||
|
||||
reveal_type(MyProtocol.__mro__) # revealed: tuple[Literal[MyProtocol], typing.Protocol, typing.Generic, Literal[object]]
|
||||
reveal_type(MyProtocol.__mro__) # revealed: tuple[<class 'MyProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
|
||||
```
|
||||
|
||||
Just like for any other class base, it is an error for `Protocol` to appear multiple times in a
|
||||
@@ -37,7 +37,7 @@ class's bases:
|
||||
```py
|
||||
class Foo(Protocol, Protocol): ... # error: [inconsistent-mro]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
Protocols can also be generic, either by including `Generic[]` in the bases list, subscripting
|
||||
@@ -71,7 +71,7 @@ class DuplicateBases(Protocol, Protocol[T]):
|
||||
x: T
|
||||
|
||||
# TODO: should not have `Protocol` multiple times
|
||||
# revealed: tuple[Literal[DuplicateBases], typing.Protocol, @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'DuplicateBases'>, typing.Protocol, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(DuplicateBases.__mro__)
|
||||
```
|
||||
|
||||
@@ -107,7 +107,7 @@ it is not sufficient for it to have `Protocol` in its MRO.
|
||||
```py
|
||||
class SubclassOfMyProtocol(MyProtocol): ...
|
||||
|
||||
# revealed: tuple[Literal[SubclassOfMyProtocol], Literal[MyProtocol], typing.Protocol, typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'SubclassOfMyProtocol'>, <class 'MyProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
|
||||
reveal_type(SubclassOfMyProtocol.__mro__)
|
||||
|
||||
reveal_type(is_protocol(SubclassOfMyProtocol)) # revealed: Literal[False]
|
||||
@@ -126,7 +126,7 @@ class OtherProtocol(Protocol):
|
||||
|
||||
class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ...
|
||||
|
||||
# revealed: tuple[Literal[ComplexInheritance], Literal[SubProtocol], Literal[MyProtocol], Literal[OtherProtocol], typing.Protocol, typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'ComplexInheritance'>, <class 'SubProtocol'>, <class 'MyProtocol'>, <class 'OtherProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
|
||||
reveal_type(ComplexInheritance.__mro__)
|
||||
|
||||
reveal_type(is_protocol(ComplexInheritance)) # revealed: Literal[True]
|
||||
@@ -139,13 +139,13 @@ or `TypeError` is raised at runtime when the class is created.
|
||||
# error: [invalid-protocol] "Protocol class `Invalid` cannot inherit from non-protocol class `NotAProtocol`"
|
||||
class Invalid(NotAProtocol, Protocol): ...
|
||||
|
||||
# revealed: tuple[Literal[Invalid], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'Invalid'>, <class 'NotAProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
|
||||
reveal_type(Invalid.__mro__)
|
||||
|
||||
# error: [invalid-protocol] "Protocol class `AlsoInvalid` cannot inherit from non-protocol class `NotAProtocol`"
|
||||
class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ...
|
||||
|
||||
# revealed: tuple[Literal[AlsoInvalid], Literal[MyProtocol], Literal[OtherProtocol], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'AlsoInvalid'>, <class 'MyProtocol'>, <class 'OtherProtocol'>, <class 'NotAProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
|
||||
reveal_type(AlsoInvalid.__mro__)
|
||||
```
|
||||
|
||||
@@ -163,7 +163,7 @@ T = TypeVar("T")
|
||||
# type checkers.
|
||||
class Fine(Protocol, object): ...
|
||||
|
||||
reveal_type(Fine.__mro__) # revealed: tuple[Literal[Fine], typing.Protocol, typing.Generic, Literal[object]]
|
||||
reveal_type(Fine.__mro__) # revealed: tuple[<class 'Fine'>, typing.Protocol, typing.Generic, <class 'object'>]
|
||||
|
||||
class StillFine(Protocol, Generic[T], object): ...
|
||||
class EvenThis[T](Protocol, object): ...
|
||||
@@ -177,7 +177,7 @@ And multiple inheritance from a mix of protocol and non-protocol classes is fine
|
||||
```py
|
||||
class FineAndDandy(MyProtocol, OtherProtocol, NotAProtocol): ...
|
||||
|
||||
# revealed: tuple[Literal[FineAndDandy], Literal[MyProtocol], Literal[OtherProtocol], typing.Protocol, typing.Generic, Literal[NotAProtocol], Literal[object]]
|
||||
# revealed: tuple[<class 'FineAndDandy'>, <class 'MyProtocol'>, <class 'OtherProtocol'>, typing.Protocol, typing.Generic, <class 'NotAProtocol'>, <class 'object'>]
|
||||
reveal_type(FineAndDandy.__mro__)
|
||||
```
|
||||
|
||||
@@ -1558,7 +1558,43 @@ def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
|
||||
c: Sized = some_sized
|
||||
```
|
||||
|
||||
## Regression test: narrowing with self-referential protocols
|
||||
## Recursive protocols
|
||||
|
||||
### Properties
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, Any
|
||||
from ty_extensions import is_fully_static, static_assert, is_assignable_to, is_subtype_of, is_equivalent_to
|
||||
|
||||
class RecursiveFullyStatic(Protocol):
|
||||
parent: RecursiveFullyStatic | None
|
||||
x: int
|
||||
|
||||
class RecursiveNonFullyStatic(Protocol):
|
||||
parent: RecursiveNonFullyStatic | None
|
||||
x: Any
|
||||
|
||||
static_assert(is_fully_static(RecursiveFullyStatic))
|
||||
static_assert(not is_fully_static(RecursiveNonFullyStatic))
|
||||
|
||||
static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic))
|
||||
|
||||
# TODO: currently leads to a stack overflow
|
||||
# static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
# static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveFullyStatic))
|
||||
|
||||
class AlsoRecursiveFullyStatic(Protocol):
|
||||
parent: AlsoRecursiveFullyStatic | None
|
||||
x: int
|
||||
|
||||
# TODO: currently leads to a stack overflow
|
||||
# static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic))
|
||||
```
|
||||
|
||||
### Regression test: narrowing with self-referential protocols
|
||||
|
||||
This snippet caused us to panic on an early version of the implementation for protocols.
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name:
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: object, /) -> bool
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||
reveal_type(typing.__class__) # revealed: <class 'ModuleType'>
|
||||
|
||||
reveal_type(typing.__dict__) # revealed: dict[str, Any]
|
||||
```
|
||||
|
||||
@@ -16,7 +16,7 @@ class C:
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `<class 'C'>` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[2]
|
||||
reveal_type(C.y) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
@@ -26,7 +26,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `Literal[C]` is possibly unbound
|
||||
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `<class 'C'>` is possibly unbound
|
||||
--> src/mdtest_snippet.py:6:5
|
||||
|
|
||||
4 | attr: int = 0
|
||||
|
||||
@@ -41,7 +41,7 @@ info: `lint:invalid-assignment` is enabled by default
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-attribute-access: Cannot assign to instance attribute `attr` from the class object `Literal[C]`
|
||||
error: lint:invalid-attribute-access: Cannot assign to instance attribute `attr` from the class object `<class 'C'>`
|
||||
--> src/mdtest_snippet.py:9:1
|
||||
|
|
||||
7 | instance.attr = "wrong" # error: [invalid-assignment]
|
||||
|
||||
@@ -37,7 +37,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-assignment: Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
|
||||
error: lint:invalid-assignment: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'C1'> | <class 'C1'>`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | # TODO: The error message here could be improved to explain why the assignment fails.
|
||||
|
||||
@@ -23,7 +23,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `Literal[C]`.
|
||||
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `<class 'C'>`.
|
||||
--> src/mdtest_snippet.py:3:1
|
||||
|
|
||||
1 | class C: ...
|
||||
|
||||
@@ -46,6 +46,18 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:16:5
|
||||
|
|
||||
14 | # revealed: Unknown
|
||||
15 | # error: [possibly-unresolved-reference]
|
||||
16 | reveal_type(x)
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> src/mdtest_snippet.py:16:17
|
||||
@@ -58,15 +70,3 @@ warning: lint:possibly-unresolved-reference: Name `x` used when possibly not def
|
||||
info: `lint:possibly-unresolved-reference` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:16:5
|
||||
|
|
||||
14 | # revealed: Unknown
|
||||
15 | # error: [possibly-unresolved-reference]
|
||||
16 | reveal_type(x)
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -55,6 +55,18 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:12:1
|
||||
|
|
||||
10 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
11 | # error: [invalid-argument-type]
|
||||
12 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> src/mdtest_snippet.py:12:15
|
||||
@@ -77,15 +89,3 @@ info: Type variable defined here
|
||||
info: `lint:invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:12:1
|
||||
|
|
||||
10 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
11 | # error: [invalid-argument-type]
|
||||
12 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -70,6 +70,18 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:13:1
|
||||
|
|
||||
11 | reveal_type(f(None)) # revealed: None
|
||||
12 | # error: [invalid-argument-type]
|
||||
13 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> src/mdtest_snippet.py:13:15
|
||||
@@ -92,15 +104,3 @@ info: Type variable defined here
|
||||
info: `lint:invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:13:1
|
||||
|
|
||||
11 | reveal_type(f(None)) # revealed: None
|
||||
12 | # error: [invalid-argument-type]
|
||||
13 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -52,6 +52,18 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:9:1
|
||||
|
|
||||
7 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
8 | # error: [invalid-argument-type]
|
||||
9 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> src/mdtest_snippet.py:9:15
|
||||
@@ -73,15 +85,3 @@ info: Type variable defined here
|
||||
info: `lint:invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:9:1
|
||||
|
|
||||
7 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
8 | # error: [invalid-argument-type]
|
||||
9 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -67,6 +67,18 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:10:1
|
||||
|
|
||||
8 | reveal_type(f(None)) # revealed: None
|
||||
9 | # error: [invalid-argument-type]
|
||||
10 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> src/mdtest_snippet.py:10:15
|
||||
@@ -88,15 +100,3 @@ info: Type variable defined here
|
||||
info: `lint:invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:10:1
|
||||
|
|
||||
8 | reveal_type(f(None)) # revealed: None
|
||||
9 | # error: [invalid-argument-type]
|
||||
10 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: mro.md - Method Resolution Order tests - `__bases__` lists with duplicate bases
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`"
|
||||
4 |
|
||||
5 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
6 |
|
||||
7 | class Spam: ...
|
||||
8 | class Eggs: ...
|
||||
9 | class Bar: ...
|
||||
10 | class Baz: ...
|
||||
11 |
|
||||
12 | # fmt: off
|
||||
13 |
|
||||
14 | # error: [duplicate-base] "Duplicate base class `Spam`"
|
||||
15 | # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
16 | class Ham(
|
||||
17 | Spam,
|
||||
18 | Eggs,
|
||||
19 | Bar,
|
||||
20 | Baz,
|
||||
21 | Spam,
|
||||
22 | Eggs,
|
||||
23 | ): ...
|
||||
24 |
|
||||
25 | # fmt: on
|
||||
26 |
|
||||
27 | reveal_type(Ham.__mro__) # revealed: tuple[<class 'Ham'>, Unknown, <class 'object'>]
|
||||
28 |
|
||||
29 | class Mushrooms: ...
|
||||
30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
|
||||
31 |
|
||||
32 | reveal_type(Omelette.__mro__) # revealed: tuple[<class 'Omelette'>, Unknown, <class 'object'>]
|
||||
33 |
|
||||
34 | # fmt: off
|
||||
35 |
|
||||
36 | # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
37 | class VeryEggyOmelette(
|
||||
38 | Eggs,
|
||||
39 | Ham,
|
||||
40 | Spam,
|
||||
41 | Eggs,
|
||||
42 | Mushrooms,
|
||||
43 | Bar,
|
||||
44 | Eggs,
|
||||
45 | Baz,
|
||||
46 | Eggs,
|
||||
47 | ): ...
|
||||
48 |
|
||||
49 | # fmt: off
|
||||
50 | # fmt: off
|
||||
51 |
|
||||
52 | class A: ...
|
||||
53 |
|
||||
54 | class B( # type: ignore[duplicate-base]
|
||||
55 | A,
|
||||
56 | A,
|
||||
57 | ): ...
|
||||
58 |
|
||||
59 | class C(
|
||||
60 | A,
|
||||
61 | A
|
||||
62 | ): # type: ignore[duplicate-base]
|
||||
63 | x: int
|
||||
64 |
|
||||
65 | # fmt: on
|
||||
66 | # fmt: off
|
||||
67 |
|
||||
68 | # error: [duplicate-base]
|
||||
69 | class D(
|
||||
70 | A,
|
||||
71 | # error: [unused-ignore-comment]
|
||||
72 | A, # type: ignore[duplicate-base]
|
||||
73 | ): ...
|
||||
74 |
|
||||
75 | # error: [duplicate-base]
|
||||
76 | class E(
|
||||
77 | A,
|
||||
78 | A
|
||||
79 | ):
|
||||
80 | # error: [unused-ignore-comment]
|
||||
81 | x: int # type: ignore[duplicate-base]
|
||||
82 |
|
||||
83 | # fmt: on
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:duplicate-base: Duplicate base class `str`
|
||||
--> src/mdtest_snippet.py:3:7
|
||||
|
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`"
|
||||
| ^^^^^^^^^^^^^
|
||||
4 |
|
||||
5 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
|
|
||||
info: The definition of class `Foo` will raise `TypeError` at runtime
|
||||
--> src/mdtest_snippet.py:3:11
|
||||
|
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`"
|
||||
| --- ^^^ Class `str` later repeated here
|
||||
| |
|
||||
| Class `str` first included in bases list here
|
||||
4 |
|
||||
5 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
|
|
||||
info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:5:1
|
||||
|
|
||||
3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`"
|
||||
4 |
|
||||
5 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `tuple[<class 'Foo'>, Unknown, <class 'object'>]`
|
||||
6 |
|
||||
7 | class Spam: ...
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:duplicate-base: Duplicate base class `Spam`
|
||||
--> src/mdtest_snippet.py:16:7
|
||||
|
|
||||
14 | # error: [duplicate-base] "Duplicate base class `Spam`"
|
||||
15 | # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
16 | class Ham(
|
||||
| _______^
|
||||
17 | | Spam,
|
||||
18 | | Eggs,
|
||||
19 | | Bar,
|
||||
20 | | Baz,
|
||||
21 | | Spam,
|
||||
22 | | Eggs,
|
||||
23 | | ): ...
|
||||
| |_^
|
||||
24 |
|
||||
25 | # fmt: on
|
||||
|
|
||||
info: The definition of class `Ham` will raise `TypeError` at runtime
|
||||
--> src/mdtest_snippet.py:17:5
|
||||
|
|
||||
15 | # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
16 | class Ham(
|
||||
17 | Spam,
|
||||
| ---- Class `Spam` first included in bases list here
|
||||
18 | Eggs,
|
||||
19 | Bar,
|
||||
20 | Baz,
|
||||
21 | Spam,
|
||||
| ^^^^ Class `Spam` later repeated here
|
||||
22 | Eggs,
|
||||
23 | ): ...
|
||||
|
|
||||
info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:duplicate-base: Duplicate base class `Eggs`
|
||||
--> src/mdtest_snippet.py:16:7
|
||||
|
|
||||
14 | # error: [duplicate-base] "Duplicate base class `Spam`"
|
||||
15 | # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
16 | class Ham(
|
||||
| _______^
|
||||
17 | | Spam,
|
||||
18 | | Eggs,
|
||||
19 | | Bar,
|
||||
20 | | Baz,
|
||||
21 | | Spam,
|
||||
22 | | Eggs,
|
||||
23 | | ): ...
|
||||
| |_^
|
||||
24 |
|
||||
25 | # fmt: on
|
||||
|
|
||||
info: The definition of class `Ham` will raise `TypeError` at runtime
|
||||
--> src/mdtest_snippet.py:18:5
|
||||
|
|
||||
16 | class Ham(
|
||||
17 | Spam,
|
||||
18 | Eggs,
|
||||
| ---- Class `Eggs` first included in bases list here
|
||||
19 | Bar,
|
||||
20 | Baz,
|
||||
21 | Spam,
|
||||
22 | Eggs,
|
||||
| ^^^^ Class `Eggs` later repeated here
|
||||
23 | ): ...
|
||||
|
|
||||
info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:27:1
|
||||
|
|
||||
25 | # fmt: on
|
||||
26 |
|
||||
27 | reveal_type(Ham.__mro__) # revealed: tuple[<class 'Ham'>, Unknown, <class 'object'>]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `tuple[<class 'Ham'>, Unknown, <class 'object'>]`
|
||||
28 |
|
||||
29 | class Mushrooms: ...
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:duplicate-base: Duplicate base class `Mushrooms`
|
||||
--> src/mdtest_snippet.py:30:7
|
||||
|
|
||||
29 | class Mushrooms: ...
|
||||
30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
31 |
|
||||
32 | reveal_type(Omelette.__mro__) # revealed: tuple[<class 'Omelette'>, Unknown, <class 'object'>]
|
||||
|
|
||||
info: The definition of class `Omelette` will raise `TypeError` at runtime
|
||||
--> src/mdtest_snippet.py:30:28
|
||||
|
|
||||
29 | class Mushrooms: ...
|
||||
30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
|
||||
| --------- ^^^^^^^^^ Class `Mushrooms` later repeated here
|
||||
| |
|
||||
| Class `Mushrooms` first included in bases list here
|
||||
31 |
|
||||
32 | reveal_type(Omelette.__mro__) # revealed: tuple[<class 'Omelette'>, Unknown, <class 'object'>]
|
||||
|
|
||||
info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:32:1
|
||||
|
|
||||
30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
|
||||
31 |
|
||||
32 | reveal_type(Omelette.__mro__) # revealed: tuple[<class 'Omelette'>, Unknown, <class 'object'>]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `tuple[<class 'Omelette'>, Unknown, <class 'object'>]`
|
||||
33 |
|
||||
34 | # fmt: off
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:duplicate-base: Duplicate base class `Eggs`
|
||||
--> src/mdtest_snippet.py:37:7
|
||||
|
|
||||
36 | # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
37 | class VeryEggyOmelette(
|
||||
| _______^
|
||||
38 | | Eggs,
|
||||
39 | | Ham,
|
||||
40 | | Spam,
|
||||
41 | | Eggs,
|
||||
42 | | Mushrooms,
|
||||
43 | | Bar,
|
||||
44 | | Eggs,
|
||||
45 | | Baz,
|
||||
46 | | Eggs,
|
||||
47 | | ): ...
|
||||
| |_^
|
||||
48 |
|
||||
49 | # fmt: off
|
||||
|
|
||||
info: The definition of class `VeryEggyOmelette` will raise `TypeError` at runtime
|
||||
--> src/mdtest_snippet.py:38:5
|
||||
|
|
||||
36 | # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
37 | class VeryEggyOmelette(
|
||||
38 | Eggs,
|
||||
| ---- Class `Eggs` first included in bases list here
|
||||
39 | Ham,
|
||||
40 | Spam,
|
||||
41 | Eggs,
|
||||
| ^^^^ Class `Eggs` later repeated here
|
||||
42 | Mushrooms,
|
||||
43 | Bar,
|
||||
44 | Eggs,
|
||||
| ^^^^ Class `Eggs` later repeated here
|
||||
45 | Baz,
|
||||
46 | Eggs,
|
||||
| ^^^^ Class `Eggs` later repeated here
|
||||
47 | ): ...
|
||||
|
|
||||
info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:duplicate-base: Duplicate base class `A`
|
||||
--> src/mdtest_snippet.py:69:7
|
||||
|
|
||||
68 | # error: [duplicate-base]
|
||||
69 | class D(
|
||||
| _______^
|
||||
70 | | A,
|
||||
71 | | # error: [unused-ignore-comment]
|
||||
72 | | A, # type: ignore[duplicate-base]
|
||||
73 | | ): ...
|
||||
| |_^
|
||||
74 |
|
||||
75 | # error: [duplicate-base]
|
||||
|
|
||||
info: The definition of class `D` will raise `TypeError` at runtime
|
||||
--> src/mdtest_snippet.py:70:5
|
||||
|
|
||||
68 | # error: [duplicate-base]
|
||||
69 | class D(
|
||||
70 | A,
|
||||
| - Class `A` first included in bases list here
|
||||
71 | # error: [unused-ignore-comment]
|
||||
72 | A, # type: ignore[duplicate-base]
|
||||
| ^ Class `A` later repeated here
|
||||
73 | ): ...
|
||||
|
|
||||
info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning: lint:unused-ignore-comment
|
||||
--> src/mdtest_snippet.py:72:9
|
||||
|
|
||||
70 | A,
|
||||
71 | # error: [unused-ignore-comment]
|
||||
72 | A, # type: ignore[duplicate-base]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused blanket `type: ignore` directive
|
||||
73 | ): ...
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:duplicate-base: Duplicate base class `A`
|
||||
--> src/mdtest_snippet.py:76:7
|
||||
|
|
||||
75 | # error: [duplicate-base]
|
||||
76 | class E(
|
||||
| _______^
|
||||
77 | | A,
|
||||
78 | | A
|
||||
79 | | ):
|
||||
| |_^
|
||||
80 | # error: [unused-ignore-comment]
|
||||
81 | x: int # type: ignore[duplicate-base]
|
||||
|
|
||||
info: The definition of class `E` will raise `TypeError` at runtime
|
||||
--> src/mdtest_snippet.py:77:5
|
||||
|
|
||||
75 | # error: [duplicate-base]
|
||||
76 | class E(
|
||||
77 | A,
|
||||
| - Class `A` first included in bases list here
|
||||
78 | A
|
||||
| ^ Class `A` later repeated here
|
||||
79 | ):
|
||||
80 | # error: [unused-ignore-comment]
|
||||
|
|
||||
info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning: lint:unused-ignore-comment
|
||||
--> src/mdtest_snippet.py:81:13
|
||||
|
|
||||
79 | ):
|
||||
80 | # error: [unused-ignore-comment]
|
||||
81 | x: int # type: ignore[duplicate-base]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused blanket `type: ignore` directive
|
||||
82 |
|
||||
83 | # fmt: on
|
||||
|
|
||||
|
||||
```
|
||||
@@ -71,23 +71,6 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-overload: Overloaded function `try_from3` does not use the `@classmethod` decorator consistently
|
||||
--> src/mdtest_snippet.py:40:9
|
||||
|
|
||||
38 | def try_from3(cls, x: str) -> None: ...
|
||||
39 | # error: [invalid-overload]
|
||||
40 | def try_from3(cls, x: int | str) -> CheckClassMethod | None:
|
||||
| ---------
|
||||
| |
|
||||
| Missing here
|
||||
41 | if isinstance(x, int):
|
||||
42 | return cls(x)
|
||||
|
|
||||
info: `lint:invalid-overload` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-overload: Overloaded function `try_from1` does not use the `@classmethod` decorator consistently
|
||||
--> src/mdtest_snippet.py:13:9
|
||||
@@ -129,3 +112,20 @@ error: lint:invalid-overload: Overloaded function `try_from2` does not use the `
|
||||
info: `lint:invalid-overload` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-overload: Overloaded function `try_from3` does not use the `@classmethod` decorator consistently
|
||||
--> src/mdtest_snippet.py:40:9
|
||||
|
|
||||
38 | def try_from3(cls, x: str) -> None: ...
|
||||
39 | # error: [invalid-overload]
|
||||
40 | def try_from3(cls, x: int | str) -> CheckClassMethod | None:
|
||||
| ---------
|
||||
| |
|
||||
| Missing here
|
||||
41 | if isinstance(x, int):
|
||||
42 | return cls(x)
|
||||
|
|
||||
info: `lint:invalid-overload` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -64,22 +64,6 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-overload: `@final` decorator should be applied only to the overload implementation
|
||||
--> src/mdtest_snippet.py:27:9
|
||||
|
|
||||
25 | def method3(self, x: str) -> str: ...
|
||||
26 | # error: [invalid-overload]
|
||||
27 | def method3(self, x: int | str) -> int | str:
|
||||
| -------
|
||||
| |
|
||||
| Implementation defined here
|
||||
28 | return x
|
||||
|
|
||||
info: `lint:invalid-overload` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-overload: `@final` decorator should be applied only to the overload implementation
|
||||
--> src/mdtest_snippet.py:18:9
|
||||
@@ -96,6 +80,22 @@ info: `lint:invalid-overload` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-overload: `@final` decorator should be applied only to the overload implementation
|
||||
--> src/mdtest_snippet.py:27:9
|
||||
|
|
||||
25 | def method3(self, x: str) -> str: ...
|
||||
26 | # error: [invalid-overload]
|
||||
27 | def method3(self, x: int | str) -> int | str:
|
||||
| -------
|
||||
| |
|
||||
| Implementation defined here
|
||||
28 | return x
|
||||
|
|
||||
info: `lint:invalid-overload` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-overload: `@final` decorator should be applied only to the first overload
|
||||
--> src/mdtest_snippet.pyi:11:9
|
||||
|
||||
@@ -41,6 +41,19 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:4:1
|
||||
|
|
||||
3 | # error: [call-non-callable]
|
||||
4 | reveal_type(Protocol()) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
5 |
|
||||
6 | class MyProtocol(Protocol):
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:call-non-callable: Object of type `typing.Protocol` is not callable
|
||||
--> src/mdtest_snippet.py:4:13
|
||||
@@ -57,14 +70,14 @@ info: `lint:call-non-callable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:4:1
|
||||
|
|
||||
3 | # error: [call-non-callable]
|
||||
4 | reveal_type(Protocol()) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
5 |
|
||||
6 | class MyProtocol(Protocol):
|
||||
|
|
||||
--> src/mdtest_snippet.py:10:1
|
||||
|
|
||||
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
|
||||
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyProtocol`
|
||||
11 |
|
||||
12 | class GenericProtocol[T](Protocol):
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -93,13 +106,12 @@ info: `lint:call-non-callable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:10:1
|
||||
--> src/mdtest_snippet.py:16:1
|
||||
|
|
||||
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
|
||||
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyProtocol`
|
||||
11 |
|
||||
12 | class GenericProtocol[T](Protocol):
|
||||
15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`"
|
||||
16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol[int]`
|
||||
17 | class SubclassOfMyProtocol(MyProtocol): ...
|
||||
|
|
||||
|
||||
```
|
||||
@@ -126,18 +138,6 @@ info: `lint:call-non-callable` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:16:1
|
||||
|
|
||||
15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`"
|
||||
16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol[int]`
|
||||
17 | class SubclassOfMyProtocol(MyProtocol): ...
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:19:1
|
||||
|
||||
@@ -27,39 +27,42 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md
|
||||
13 | def i() -> typing.Iterable:
|
||||
14 | yield 42
|
||||
15 |
|
||||
16 | def j() -> str: # error: [invalid-return-type]
|
||||
17 | yield 42
|
||||
18 | import types
|
||||
19 | import typing
|
||||
20 |
|
||||
21 | async def f() -> types.AsyncGeneratorType:
|
||||
22 | yield 42
|
||||
16 | def i2() -> typing.Generator:
|
||||
17 | yield from i()
|
||||
18 |
|
||||
19 | def j() -> str: # error: [invalid-return-type]
|
||||
20 | yield 42
|
||||
21 | import types
|
||||
22 | import typing
|
||||
23 |
|
||||
24 | async def g() -> typing.AsyncGenerator:
|
||||
24 | async def f() -> types.AsyncGeneratorType:
|
||||
25 | yield 42
|
||||
26 |
|
||||
27 | async def h() -> typing.AsyncIterator:
|
||||
27 | async def g() -> typing.AsyncGenerator:
|
||||
28 | yield 42
|
||||
29 |
|
||||
30 | async def i() -> typing.AsyncIterable:
|
||||
30 | async def h() -> typing.AsyncIterator:
|
||||
31 | yield 42
|
||||
32 |
|
||||
33 | async def j() -> str: # error: [invalid-return-type]
|
||||
33 | async def i() -> typing.AsyncIterable:
|
||||
34 | yield 42
|
||||
35 |
|
||||
36 | async def j() -> str: # error: [invalid-return-type]
|
||||
37 | yield 42
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-return-type: Return type does not match returned value
|
||||
--> src/mdtest_snippet.py:16:12
|
||||
--> src/mdtest_snippet.py:19:12
|
||||
|
|
||||
14 | yield 42
|
||||
15 |
|
||||
16 | def j() -> str: # error: [invalid-return-type]
|
||||
17 | yield from i()
|
||||
18 |
|
||||
19 | def j() -> str: # error: [invalid-return-type]
|
||||
| ^^^ Expected `str`, found `types.GeneratorType`
|
||||
17 | yield 42
|
||||
18 | import types
|
||||
20 | yield 42
|
||||
21 | import types
|
||||
|
|
||||
info: Function is inferred as returning `types.GeneratorType` because it is a generator function
|
||||
info: See https://docs.python.org/3/glossary.html#term-generator for more details
|
||||
@@ -69,13 +72,13 @@ info: `lint:invalid-return-type` is enabled by default
|
||||
|
||||
```
|
||||
error: lint:invalid-return-type: Return type does not match returned value
|
||||
--> src/mdtest_snippet.py:33:18
|
||||
--> src/mdtest_snippet.py:36:18
|
||||
|
|
||||
31 | yield 42
|
||||
32 |
|
||||
33 | async def j() -> str: # error: [invalid-return-type]
|
||||
| ^^^ Expected `str`, found `types.AsyncGeneratorType`
|
||||
34 | yield 42
|
||||
35 |
|
||||
36 | async def j() -> str: # error: [invalid-return-type]
|
||||
| ^^^ Expected `str`, found `types.AsyncGeneratorType`
|
||||
37 | yield 42
|
||||
|
|
||||
info: Function is inferred as returning `types.AsyncGeneratorType` because it is an async generator function
|
||||
info: See https://docs.python.org/3/glossary.html#term-asynchronous-generator for more details
|
||||
|
||||
@@ -15,8 +15,8 @@ class Foo[T]: ...
|
||||
|
||||
class Bar(Foo[Bar]): ...
|
||||
|
||||
reveal_type(Bar) # revealed: Literal[Bar]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo[Bar]], Literal[object]]
|
||||
reveal_type(Bar) # revealed: <class 'Bar'>
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, <class 'Foo[Bar]'>, <class 'object'>]
|
||||
```
|
||||
|
||||
## Access to attributes declared in stubs
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
```py
|
||||
class NotSubscriptable: ...
|
||||
|
||||
a = NotSubscriptable[0] # error: "Cannot subscript object of type `Literal[NotSubscriptable]` with no `__class_getitem__` method"
|
||||
# error: "Cannot subscript object of type `<class 'NotSubscriptable'>` with no `__class_getitem__` method"
|
||||
a = NotSubscriptable[0]
|
||||
```
|
||||
|
||||
## Class getitem
|
||||
@@ -47,7 +48,7 @@ def _(flag: bool):
|
||||
|
||||
x = A if flag else B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
|
||||
reveal_type(x[0]) # revealed: str | int
|
||||
```
|
||||
|
||||
@@ -62,7 +63,7 @@ def _(flag: bool):
|
||||
|
||||
else:
|
||||
class Spam: ...
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `<class 'Spam'> | <class 'Spam'>` is possibly unbound"
|
||||
# revealed: str
|
||||
reveal_type(Spam[42])
|
||||
```
|
||||
@@ -79,7 +80,7 @@ def _(flag: bool):
|
||||
else:
|
||||
Eggs = 1
|
||||
|
||||
a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method"
|
||||
a = Eggs[42] # error: "Cannot subscript object of type `<class 'Eggs'> | Literal[1]` with no `__getitem__` method"
|
||||
|
||||
# TODO: should _probably_ emit `str | Unknown`
|
||||
reveal_type(a) # revealed: Unknown
|
||||
|
||||
@@ -85,7 +85,7 @@ class A(tuple[int, str]): ...
|
||||
|
||||
# Runtime value: `(A, tuple, object)`
|
||||
# TODO: Generics
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], @Todo(GenericAlias instance), Literal[object]]
|
||||
reveal_type(A.__mro__) # revealed: tuple[<class 'A'>, @Todo(GenericAlias instance), <class 'object'>]
|
||||
```
|
||||
|
||||
## `typing.Tuple`
|
||||
@@ -116,6 +116,6 @@ from typing import Tuple
|
||||
class C(Tuple): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
# revealed: tuple[<class 'C'>, <class 'tuple'>, <class 'Sequence'>, <class 'Reversible'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(C.__mro__)
|
||||
```
|
||||
|
||||
@@ -105,7 +105,7 @@ def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None
|
||||
```py
|
||||
class C(Unknown): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
# revealed: tuple[<class 'C'>, Unknown, <class 'object'>]
|
||||
reveal_type(C.__mro__)
|
||||
|
||||
# error: "Special form `ty_extensions.Unknown` expected no type parameter"
|
||||
|
||||
@@ -147,8 +147,8 @@ _: type[A, B]
|
||||
```py
|
||||
class Foo(type[int]): ...
|
||||
|
||||
# TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], @Todo(GenericAlias instance), Literal[object]]
|
||||
# TODO: should be `tuple[<class 'Foo'>, <class 'type'>, <class 'object'>]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, @Todo(GenericAlias instance), <class 'object'>]
|
||||
```
|
||||
|
||||
## `@final` classes
|
||||
@@ -169,6 +169,6 @@ from typing import final
|
||||
class Foo: ...
|
||||
|
||||
def _(x: type[Foo], y: type[EllipsisType]):
|
||||
reveal_type(x) # revealed: Literal[Foo]
|
||||
reveal_type(y) # revealed: Literal[EllipsisType]
|
||||
reveal_type(x) # revealed: <class 'Foo'>
|
||||
reveal_type(y) # revealed: <class 'EllipsisType'>
|
||||
```
|
||||
|
||||
@@ -28,5 +28,5 @@ class C(Type): ...
|
||||
|
||||
# Runtime value: `(C, type, typing.Generic, object)`
|
||||
# TODO: Add `Generic` to the MRO
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[type], Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'type'>, <class 'object'>]
|
||||
```
|
||||
|
||||
@@ -334,10 +334,19 @@ static_assert(is_subtype_of(TypeOf[typing], ModuleType))
|
||||
|
||||
### Slice literals
|
||||
|
||||
The type of a slice literal is currently inferred as `slice`, which is a generic type whose default
|
||||
specialization includes `Any`. Slice literals therefore do not participate in the subtyping
|
||||
relationship.
|
||||
|
||||
TODO: Infer a specialized type for the slice literal
|
||||
|
||||
```py
|
||||
from ty_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[1:2:3], slice))
|
||||
static_assert(not is_subtype_of(TypeOf[1:2:3], slice))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(TypeOf[1:2:3], slice[int]))
|
||||
```
|
||||
|
||||
### Special forms
|
||||
|
||||
@@ -52,25 +52,25 @@ class Yes:
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Yes]`"
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `<class 'Yes'>`"
|
||||
reveal_type(+Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Yes]`"
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `<class 'Yes'>`"
|
||||
reveal_type(-Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Yes]`"
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `<class 'Yes'>`"
|
||||
reveal_type(~Yes) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Sub]`"
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `<class 'Sub'>`"
|
||||
reveal_type(+Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Sub]`"
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `<class 'Sub'>`"
|
||||
reveal_type(-Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Sub]`"
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `<class 'Sub'>`"
|
||||
reveal_type(~Sub) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `<class 'No'>`"
|
||||
reveal_type(+No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `<class 'No'>`"
|
||||
reveal_type(-No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `<class 'No'>`"
|
||||
reveal_type(~No) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -160,10 +160,10 @@ reveal_type(+Sub) # revealed: bool
|
||||
reveal_type(-Sub) # revealed: str
|
||||
reveal_type(~Sub) # revealed: int
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `<class 'No'>`"
|
||||
reveal_type(+No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `<class 'No'>`"
|
||||
reveal_type(-No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `<class 'No'>`"
|
||||
reveal_type(~No) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -9,7 +9,6 @@ discord.py # some kind of hang, only when multi-threaded?
|
||||
freqtrade # cycle panics (try_metaclass_)
|
||||
hydpy # cycle panics (try_metaclass_)
|
||||
ibis # cycle panics (try_metaclass_)
|
||||
manticore # stack overflow, see https://github.com/astral-sh/ruff/issues/17863
|
||||
pandas # slow
|
||||
pandas-stubs # cycle panics (try_metaclass_)
|
||||
pandera # cycle panics (try_metaclass_)
|
||||
|
||||
@@ -50,6 +50,7 @@ jinja
|
||||
koda-validate
|
||||
kopf
|
||||
kornia
|
||||
manticore
|
||||
materialize
|
||||
meson
|
||||
mitmproxy
|
||||
|
||||
@@ -16,7 +16,7 @@ pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::{default_lint_registry, ProgramSettings, PythonPlatform};
|
||||
@@ -32,6 +32,8 @@ pub(crate) mod tests {
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
@@ -39,30 +41,34 @@ pub(crate) mod tests {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
events: Events,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
let events = Events::default();
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: ty_vendored::file_system().clone(),
|
||||
events: Arc::default(),
|
||||
events,
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
let events = inner.get_mut().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
@@ -129,14 +135,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
impl salsa::Database for TestDb {}
|
||||
|
||||
pub(crate) struct TestDbBuilder<'a> {
|
||||
/// Target Python version
|
||||
|
||||
470
crates/ty_python_semantic/src/dunder_all.rs
Normal file
470
crates/ty_python_semantic/src/dunder_all.rs
Normal file
@@ -0,0 +1,470 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};
|
||||
use ruff_python_ast::{self as ast};
|
||||
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId};
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::semantic_index::{global_scope, semantic_index, SemanticIndex};
|
||||
use crate::symbol::{symbol_from_bindings, Boundness, Symbol};
|
||||
use crate::types::{infer_expression_types, Truthiness};
|
||||
use crate::{resolve_module, Db, ModuleName};
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
fn dunder_all_names_cycle_recover(
|
||||
_db: &dyn Db,
|
||||
_value: &Option<FxHashSet<Name>>,
|
||||
_count: u32,
|
||||
_file: File,
|
||||
) -> salsa::CycleRecoveryAction<Option<FxHashSet<Name>>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn dunder_all_names_cycle_initial(_db: &dyn Db, _file: File) -> Option<FxHashSet<Name>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns a set of names in the `__all__` variable for `file`, [`None`] if it is not defined or
|
||||
/// if it contains invalid elements.
|
||||
pub(crate) fn dunder_all_names(db: &dyn Db, file: File) -> Option<&FxHashSet<Name>> {
|
||||
#[allow(clippy::ref_option)]
|
||||
#[salsa::tracked(return_ref, cycle_fn=dunder_all_names_cycle_recover, cycle_initial=dunder_all_names_cycle_initial)]
|
||||
fn dunder_all_names_impl(db: &dyn Db, file: File) -> Option<FxHashSet<Name>> {
|
||||
let _span = tracing::trace_span!("dunder_all_names", file=?file.path(db)).entered();
|
||||
|
||||
let module = parsed_module(db.upcast(), file);
|
||||
let index = semantic_index(db, file);
|
||||
let mut collector = DunderAllNamesCollector::new(db, file, index);
|
||||
collector.visit_body(module.suite());
|
||||
collector.into_names()
|
||||
}
|
||||
|
||||
dunder_all_names_impl(db, file).as_ref()
|
||||
}
|
||||
|
||||
/// A visitor that collects the names in the `__all__` variable of a module.
|
||||
struct DunderAllNamesCollector<'db> {
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
|
||||
/// The scope in which the `__all__` names are being collected from.
|
||||
///
|
||||
/// This is always going to be the global scope of the module.
|
||||
scope: ScopeId<'db>,
|
||||
|
||||
/// The semantic index for the module.
|
||||
index: &'db SemanticIndex<'db>,
|
||||
|
||||
/// The origin of the `__all__` variable in the current module, [`None`] if it is not defined.
|
||||
origin: Option<DunderAllOrigin>,
|
||||
|
||||
/// A flag indicating whether the module uses unrecognized `__all__` idioms or there are any
|
||||
/// invalid elements in `__all__`.
|
||||
invalid: bool,
|
||||
|
||||
/// A set of names found in `__all__` for the current module.
|
||||
names: FxHashSet<Name>,
|
||||
}
|
||||
|
||||
impl<'db> DunderAllNamesCollector<'db> {
|
||||
fn new(db: &'db dyn Db, file: File, index: &'db SemanticIndex<'db>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
file,
|
||||
scope: global_scope(db, file),
|
||||
index,
|
||||
origin: None,
|
||||
invalid: false,
|
||||
names: FxHashSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the origin of `__all__` in the current module.
|
||||
///
|
||||
/// This will clear existing names if the origin is changed to mimic the behavior of overriding
|
||||
/// `__all__` in the current module.
|
||||
fn update_origin(&mut self, origin: DunderAllOrigin) {
|
||||
if self.origin.is_some() {
|
||||
self.names.clear();
|
||||
}
|
||||
self.origin = Some(origin);
|
||||
}
|
||||
|
||||
/// Extends the current set of names with the names from the given expression which can be
|
||||
/// either a list of names or a module's `__all__` variable.
|
||||
///
|
||||
/// Returns `true` if the expression is a valid list or module `__all__`, `false` otherwise.
|
||||
fn extend_from_list_or_module(&mut self, expr: &ast::Expr) -> bool {
|
||||
match expr {
|
||||
// `__all__ += [...]`
|
||||
// `__all__.extend([...])`
|
||||
ast::Expr::List(ast::ExprList { elts, .. }) => self.add_names(elts),
|
||||
|
||||
// `__all__ += module.__all__`
|
||||
// `__all__.extend(module.__all__)`
|
||||
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
|
||||
if attr != "__all__" {
|
||||
return false;
|
||||
}
|
||||
let Some(name_node) = value.as_name_expr() else {
|
||||
return false;
|
||||
};
|
||||
let Symbol::Type(ty, Boundness::Bound) = symbol_from_bindings(
|
||||
self.db,
|
||||
self.index
|
||||
.use_def_map(self.scope.file_scope_id(self.db))
|
||||
.bindings_at_use(name_node.scoped_use_id(self.db, self.scope)),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let Some(module_literal) = ty.into_module_literal() else {
|
||||
return false;
|
||||
};
|
||||
let Some(module_dunder_all_names) =
|
||||
dunder_all_names(self.db, module_literal.module(self.db).file())
|
||||
else {
|
||||
// The module either does not have a `__all__` variable or it is invalid.
|
||||
return false;
|
||||
};
|
||||
self.names.extend(module_dunder_all_names.iter().cloned());
|
||||
true
|
||||
}
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a call idiom for `__all__` and updates the set of names accordingly.
|
||||
///
|
||||
/// Returns `true` if the call idiom is recognized and valid, `false` otherwise.
|
||||
fn process_call_idiom(
|
||||
&mut self,
|
||||
function_name: &ast::Identifier,
|
||||
arguments: &ast::Arguments,
|
||||
) -> bool {
|
||||
if arguments.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
let Some(argument) = arguments.find_positional(0) else {
|
||||
return false;
|
||||
};
|
||||
match function_name.as_str() {
|
||||
// `__all__.extend([...])`
|
||||
// `__all__.extend(module.__all__)`
|
||||
"extend" => {
|
||||
if !self.extend_from_list_or_module(argument) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// `__all__.append(...)`
|
||||
"append" => {
|
||||
let Some(name) = create_name(argument) else {
|
||||
return false;
|
||||
};
|
||||
self.names.insert(name);
|
||||
}
|
||||
|
||||
// `__all__.remove(...)`
|
||||
"remove" => {
|
||||
let Some(name) = create_name(argument) else {
|
||||
return false;
|
||||
};
|
||||
self.names.remove(&name);
|
||||
}
|
||||
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns the names in `__all__` from the module imported from the given `import_from`
|
||||
/// statement.
|
||||
///
|
||||
/// Returns [`None`] if module resolution fails, invalid syntax, or if the module does not have
|
||||
/// a `__all__` variable.
|
||||
fn dunder_all_names_for_import_from(
|
||||
&self,
|
||||
import_from: &ast::StmtImportFrom,
|
||||
) -> Option<&'db FxHashSet<Name>> {
|
||||
let module_name =
|
||||
ModuleName::from_import_statement(self.db, self.file, import_from).ok()?;
|
||||
let module = resolve_module(self.db, &module_name)?;
|
||||
dunder_all_names(self.db, module.file())
|
||||
}
|
||||
|
||||
/// Evaluate the given expression and return its truthiness.
|
||||
///
|
||||
/// Returns [`None`] if the expression type doesn't implement `__bool__` correctly.
|
||||
fn evaluate_test_expr(&self, expr: &ast::Expr) -> Option<Truthiness> {
|
||||
infer_expression_types(self.db, self.index.expression(expr))
|
||||
.expression_type(expr.scoped_expression_id(self.db, self.scope))
|
||||
.try_bool(self.db)
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Add valid names to the set.
|
||||
///
|
||||
/// Returns `false` if any of the names are invalid.
|
||||
fn add_names(&mut self, exprs: &[ast::Expr]) -> bool {
|
||||
for expr in exprs {
|
||||
let Some(name) = create_name(expr) else {
|
||||
return false;
|
||||
};
|
||||
self.names.insert(name);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Consumes `self` and returns the collected set of names.
|
||||
///
|
||||
/// Returns [`None`] if `__all__` is not defined in the current module or if it contains
|
||||
/// invalid elements.
|
||||
fn into_names(self) -> Option<FxHashSet<Name>> {
|
||||
if self.origin.is_none() {
|
||||
None
|
||||
} else if self.invalid {
|
||||
tracing::debug!("Invalid `__all__` in `{}`", self.file.path(self.db));
|
||||
None
|
||||
} else {
|
||||
Some(self.names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> StatementVisitor<'db> for DunderAllNamesCollector<'db> {
|
||||
fn visit_stmt(&mut self, stmt: &'db ast::Stmt) {
|
||||
if self.invalid {
|
||||
return;
|
||||
}
|
||||
|
||||
match stmt {
|
||||
ast::Stmt::ImportFrom(import_from @ ast::StmtImportFrom { names, .. }) => {
|
||||
for ast::Alias { name, asname, .. } in names {
|
||||
// `from module import *` where `module` is a module with a top-level `__all__`
|
||||
// variable that contains the "__all__" element.
|
||||
if name == "*" {
|
||||
// Here, we need to use the `dunder_all_names` query instead of the
|
||||
// `exported_names` query because a `*`-import does not import the
|
||||
// `__all__` attribute unless it is explicitly included in the `__all__` of
|
||||
// the module.
|
||||
let Some(all_names) = self.dunder_all_names_for_import_from(import_from)
|
||||
else {
|
||||
self.invalid = true;
|
||||
continue;
|
||||
};
|
||||
|
||||
if all_names.contains(&Name::new_static("__all__")) {
|
||||
self.update_origin(DunderAllOrigin::StarImport);
|
||||
self.names.extend(all_names.iter().cloned());
|
||||
}
|
||||
} else {
|
||||
// `from module import __all__`
|
||||
// `from module import __all__ as __all__`
|
||||
if name != "__all__"
|
||||
|| asname.as_ref().is_some_and(|asname| asname != "__all__")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// We could do the `__all__` lookup lazily in case it's not needed. This would
|
||||
// happen if a `__all__` is imported from another module but then the module
|
||||
// redefines it. For example:
|
||||
//
|
||||
// ```python
|
||||
// from module import __all__ as __all__
|
||||
//
|
||||
// __all__ = ["a", "b"]
|
||||
// ```
|
||||
//
|
||||
// I'm avoiding this for now because it doesn't seem likely to happen in
|
||||
// practice.
|
||||
let Some(all_names) = self.dunder_all_names_for_import_from(import_from)
|
||||
else {
|
||||
self.invalid = true;
|
||||
continue;
|
||||
};
|
||||
|
||||
self.update_origin(DunderAllOrigin::ExternalModule);
|
||||
self.names.extend(all_names.iter().cloned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
|
||||
let [target] = targets.as_slice() else {
|
||||
return;
|
||||
};
|
||||
if !is_dunder_all(target) {
|
||||
return;
|
||||
}
|
||||
match &**value {
|
||||
// `__all__ = [...]`
|
||||
// `__all__ = (...)`
|
||||
ast::Expr::List(ast::ExprList { elts, .. })
|
||||
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
self.update_origin(DunderAllOrigin::CurrentModule);
|
||||
if !self.add_names(elts) {
|
||||
self.invalid = true;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.invalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::AugAssign(ast::StmtAugAssign {
|
||||
target,
|
||||
op: ast::Operator::Add,
|
||||
value,
|
||||
..
|
||||
}) => {
|
||||
if self.origin.is_none() {
|
||||
// We can't update `__all__` if it doesn't already exist.
|
||||
return;
|
||||
}
|
||||
if !is_dunder_all(target) {
|
||||
return;
|
||||
}
|
||||
if !self.extend_from_list_or_module(value) {
|
||||
self.invalid = true;
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
target,
|
||||
value: Some(value),
|
||||
..
|
||||
}) => {
|
||||
if !is_dunder_all(target) {
|
||||
return;
|
||||
}
|
||||
match &**value {
|
||||
// `__all__: list[str] = [...]`
|
||||
// `__all__: tuple[str, ...] = (...)`
|
||||
ast::Expr::List(ast::ExprList { elts, .. })
|
||||
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
self.update_origin(DunderAllOrigin::CurrentModule);
|
||||
if !self.add_names(elts) {
|
||||
self.invalid = true;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.invalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::Expr(ast::StmtExpr { value: expr, .. }) => {
|
||||
if self.origin.is_none() {
|
||||
// We can't update `__all__` if it doesn't already exist.
|
||||
return;
|
||||
}
|
||||
let Some(ast::ExprCall {
|
||||
func, arguments, ..
|
||||
}) = expr.as_call_expr()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(ast::ExprAttribute {
|
||||
value,
|
||||
attr,
|
||||
ctx: ast::ExprContext::Load,
|
||||
..
|
||||
}) = func.as_attribute_expr()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !is_dunder_all(value) {
|
||||
return;
|
||||
}
|
||||
if !self.process_call_idiom(attr, arguments) {
|
||||
self.invalid = true;
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
}) => match self.evaluate_test_expr(test) {
|
||||
Some(Truthiness::AlwaysTrue) => self.visit_body(body),
|
||||
Some(Truthiness::AlwaysFalse) => {
|
||||
for ast::ElifElseClause { test, body, .. } in elif_else_clauses {
|
||||
if let Some(test) = test {
|
||||
match self.evaluate_test_expr(test) {
|
||||
Some(Truthiness::AlwaysTrue) => {
|
||||
self.visit_body(body);
|
||||
break;
|
||||
}
|
||||
Some(Truthiness::AlwaysFalse) => {}
|
||||
Some(Truthiness::Ambiguous) | None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.visit_body(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Truthiness::Ambiguous) | None => {}
|
||||
},
|
||||
|
||||
ast::Stmt::For(..)
|
||||
| ast::Stmt::While(..)
|
||||
| ast::Stmt::With(..)
|
||||
| ast::Stmt::Match(..)
|
||||
| ast::Stmt::Try(..) => {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
|
||||
ast::Stmt::FunctionDef(..) | ast::Stmt::ClassDef(..) => {
|
||||
// Avoid recursing into any nested scopes as `__all__` is only valid at the module
|
||||
// level.
|
||||
}
|
||||
|
||||
ast::Stmt::AugAssign(..)
|
||||
| ast::Stmt::AnnAssign(..)
|
||||
| ast::Stmt::Delete(..)
|
||||
| ast::Stmt::Return(..)
|
||||
| ast::Stmt::Raise(..)
|
||||
| ast::Stmt::Assert(..)
|
||||
| ast::Stmt::Import(..)
|
||||
| ast::Stmt::Global(..)
|
||||
| ast::Stmt::Nonlocal(..)
|
||||
| ast::Stmt::TypeAlias(..)
|
||||
| ast::Stmt::Pass(..)
|
||||
| ast::Stmt::Break(..)
|
||||
| ast::Stmt::Continue(..)
|
||||
| ast::Stmt::IpyEscapeCommand(..) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum DunderAllOrigin {
|
||||
/// The `__all__` variable is defined in the current module.
|
||||
CurrentModule,
|
||||
|
||||
/// The `__all__` variable is imported from another module.
|
||||
ExternalModule,
|
||||
|
||||
/// The `__all__` variable is imported from a module via a `*`-import.
|
||||
StarImport,
|
||||
}
|
||||
|
||||
/// Checks if the given expression is a name expression for `__all__`.
|
||||
fn is_dunder_all(expr: &ast::Expr) -> bool {
|
||||
matches!(expr, ast::Expr::Name(ast::ExprName { id, .. }) if id == "__all__")
|
||||
}
|
||||
|
||||
/// Create and return a [`Name`] from the given expression, [`None`] if it is an invalid expression
|
||||
/// for a `__all__` element.
|
||||
fn create_name(expr: &ast::Expr) -> Option<Name> {
|
||||
Some(Name::new(expr.as_string_literal_expr()?.value.to_str()))
|
||||
}
|
||||
@@ -14,6 +14,7 @@ pub use site_packages::SysPrefixPathOrigin;
|
||||
|
||||
pub mod ast_node_ref;
|
||||
mod db;
|
||||
mod dunder_all;
|
||||
pub mod lint;
|
||||
pub(crate) mod list;
|
||||
mod module_name;
|
||||
|
||||
@@ -2310,7 +2310,7 @@ where
|
||||
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
ast::Expr::Yield(_) => {
|
||||
ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) => {
|
||||
let scope = self.current_scope();
|
||||
if self.scopes[scope].kind() == ScopeKind::Function {
|
||||
self.generator_functions.insert(scope);
|
||||
|
||||
@@ -178,12 +178,13 @@ use std::cmp::Ordering;
|
||||
use ruff_index::{Idx, IndexVec};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::predicate::{
|
||||
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId,
|
||||
};
|
||||
use crate::semantic_index::symbol_table;
|
||||
use crate::symbol::imported_symbol;
|
||||
use crate::symbol::{imported_symbol, RequiresExplicitReExport};
|
||||
use crate::types::{infer_expression_type, Truthiness, Type};
|
||||
use crate::Db;
|
||||
|
||||
@@ -655,7 +656,27 @@ impl VisibilityConstraints {
|
||||
PredicateNode::StarImportPlaceholder(star_import) => {
|
||||
let symbol_table = symbol_table(db, star_import.scope(db));
|
||||
let symbol_name = symbol_table.symbol(star_import.symbol_id(db)).name();
|
||||
match imported_symbol(db, star_import.referenced_file(db), symbol_name).symbol {
|
||||
let referenced_file = star_import.referenced_file(db);
|
||||
|
||||
let requires_explicit_reexport = match dunder_all_names(db, referenced_file) {
|
||||
Some(all_names) => {
|
||||
if all_names.contains(symbol_name) {
|
||||
Some(RequiresExplicitReExport::No)
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Symbol `{}` (via star import) not found in `__all__` of `{}`",
|
||||
symbol_name,
|
||||
referenced_file.path(db)
|
||||
);
|
||||
return Truthiness::AlwaysFalse;
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
match imported_symbol(db, referenced_file, symbol_name, requires_explicit_reexport)
|
||||
.symbol
|
||||
{
|
||||
crate::symbol::Symbol::Type(_, crate::symbol::Boundness::Bound) => {
|
||||
Truthiness::AlwaysTrue
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use ruff_db::files::File;
|
||||
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::module_resolver::file_to_module;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId};
|
||||
@@ -285,11 +286,23 @@ pub(crate) fn global_symbol<'db>(
|
||||
}
|
||||
|
||||
/// Infers the public type of an imported symbol.
|
||||
///
|
||||
/// If `requires_explicit_reexport` is [`None`], it will be inferred from the file's source type.
|
||||
/// For stub files, explicit re-export will be required, while for non-stub files, it will not.
|
||||
pub(crate) fn imported_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
name: &str,
|
||||
requires_explicit_reexport: Option<RequiresExplicitReExport>,
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| {
|
||||
if file.is_stub(db.upcast()) {
|
||||
RequiresExplicitReExport::Yes
|
||||
} else {
|
||||
RequiresExplicitReExport::No
|
||||
}
|
||||
});
|
||||
|
||||
// If it's not found in the global scope, check if it's present as an instance on
|
||||
// `types.ModuleType` or `builtins.object`.
|
||||
//
|
||||
@@ -305,13 +318,16 @@ pub(crate) fn imported_symbol<'db>(
|
||||
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with
|
||||
// dynamic imports; we shouldn't use it for `ModuleLiteral` types where we know exactly which
|
||||
// module we're dealing with.
|
||||
external_symbol_impl(db, file, name).or_fall_back_to(db, || {
|
||||
if name == "__getattr__" {
|
||||
Symbol::Unbound.into()
|
||||
} else {
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
}
|
||||
})
|
||||
symbol_impl(db, global_scope(db, file), name, requires_explicit_reexport).or_fall_back_to(
|
||||
db,
|
||||
|| {
|
||||
if name == "__getattr__" {
|
||||
Symbol::Unbound.into()
|
||||
} else {
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the builtins namespace.
|
||||
@@ -324,7 +340,13 @@ pub(crate) fn imported_symbol<'db>(
|
||||
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQualifiers<'db> {
|
||||
resolve_module(db, &KnownModule::Builtins.name())
|
||||
.map(|module| {
|
||||
external_symbol_impl(db, module.file(), symbol).or_fall_back_to(db, || {
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, module.file()),
|
||||
symbol,
|
||||
RequiresExplicitReExport::Yes,
|
||||
)
|
||||
.or_fall_back_to(db, || {
|
||||
// We're looking up in the builtins namespace and not the module, so we should
|
||||
// do the normal lookup in `types.ModuleType` and not the special one as in
|
||||
// `imported_symbol`.
|
||||
@@ -343,7 +365,7 @@ pub(crate) fn known_module_symbol<'db>(
|
||||
symbol: &str,
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
resolve_module(db, &known_module.name())
|
||||
.map(|module| imported_symbol(db, module.file(), symbol))
|
||||
.map(|module| imported_symbol(db, module.file(), symbol, None))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -702,7 +724,7 @@ fn symbol_from_bindings_impl<'db>(
|
||||
let mut bindings_with_constraints = bindings_with_constraints.peekable();
|
||||
|
||||
let is_non_exported = |binding: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !binding.is_reexported(db)
|
||||
requires_explicit_reexport.is_yes() && !is_reexported(db, binding)
|
||||
};
|
||||
|
||||
let unbound_visibility_constraint = match bindings_with_constraints.peek() {
|
||||
@@ -833,7 +855,7 @@ fn symbol_from_declarations_impl<'db>(
|
||||
let mut declarations = declarations.peekable();
|
||||
|
||||
let is_non_exported = |declaration: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !declaration.is_reexported(db)
|
||||
requires_explicit_reexport.is_yes() && !is_reexported(db, declaration)
|
||||
};
|
||||
|
||||
let undeclared_visibility = match declarations.peek() {
|
||||
@@ -911,6 +933,27 @@ fn symbol_from_declarations_impl<'db>(
|
||||
}
|
||||
}
|
||||
|
||||
// Returns `true` if the `definition` is re-exported.
|
||||
//
|
||||
// This will first check if the definition is using the "redundant alias" pattern like `import foo
|
||||
// as foo` or `from foo import bar as bar`. If it's not, it will check whether the symbol is being
|
||||
// exported via `__all__`.
|
||||
fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool {
|
||||
// This information is computed by the semantic index builder.
|
||||
if definition.is_reexported(db) {
|
||||
return true;
|
||||
}
|
||||
// At this point, the definition should either be an `import` or `from ... import` statement.
|
||||
// This is because the default value of `is_reexported` is `true` for any other kind of
|
||||
// definition.
|
||||
let Some(all_names) = dunder_all_names(db, definition.file(db)) else {
|
||||
return false;
|
||||
};
|
||||
let table = symbol_table(db, definition.scope(db));
|
||||
let symbol_name = table.symbol(definition.symbol(db)).name();
|
||||
all_names.contains(symbol_name)
|
||||
}
|
||||
|
||||
mod implicit_globals {
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
@@ -1015,26 +1058,8 @@ mod implicit_globals {
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of looking up a module-global symbol as seen from outside the file (e.g. via
|
||||
/// imports).
|
||||
///
|
||||
/// This will take into account whether the definition of the symbol is being explicitly
|
||||
/// re-exported from a stub file or not.
|
||||
fn external_symbol_impl<'db>(db: &'db dyn Db, file: File, name: &str) -> SymbolAndQualifiers<'db> {
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
name,
|
||||
if file.is_stub(db.upcast()) {
|
||||
RequiresExplicitReExport::Yes
|
||||
} else {
|
||||
RequiresExplicitReExport::No
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
enum RequiresExplicitReExport {
|
||||
pub(crate) enum RequiresExplicitReExport {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use infer::enclosing_class_symbol;
|
||||
use itertools::Either;
|
||||
|
||||
use std::slice::Iter;
|
||||
@@ -932,6 +933,7 @@ impl<'db> Type<'db> {
|
||||
typevar.name(db).clone(),
|
||||
typevar.definition(db),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound.normalized(db))),
|
||||
typevar.variance(db),
|
||||
typevar.default_ty(db),
|
||||
typevar.kind(db),
|
||||
))
|
||||
@@ -942,6 +944,7 @@ impl<'db> Type<'db> {
|
||||
typevar.name(db).clone(),
|
||||
typevar.definition(db),
|
||||
Some(TypeVarBoundOrConstraints::Constraints(union.normalized(db))),
|
||||
typevar.variance(db),
|
||||
typevar.default_ty(db),
|
||||
typevar.kind(db),
|
||||
))
|
||||
@@ -995,12 +998,24 @@ impl<'db> Type<'db> {
|
||||
// Everything is a subtype of `object`.
|
||||
(_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true,
|
||||
|
||||
// A fully static typevar is always a subtype of itself, and is never a subtype of any
|
||||
// other typevar, since there is no guarantee that they will be specialized to the same
|
||||
// type. (This is true even if both typevars are bounded by the same final class, since
|
||||
// you can specialize the typevars to `Never` in addition to that final class.)
|
||||
(Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => {
|
||||
self_typevar == other_typevar
|
||||
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
|
||||
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
|
||||
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
|
||||
// 2. `T` is a constrained TypeVar and all of `T`'s constraints are subtypes of `S`.
|
||||
//
|
||||
// However, there is one exception to this general rule: for any given typevar `T`,
|
||||
// `T` will always be a subtype of any union containing `T`.
|
||||
// A similar rule applies in reverse to intersection types.
|
||||
(Type::TypeVar(_), Type::Union(union)) if union.elements(db).contains(&self) => true,
|
||||
(Type::Intersection(intersection), Type::TypeVar(_))
|
||||
if intersection.positive(db).contains(&target) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::Intersection(intersection), Type::TypeVar(_))
|
||||
if intersection.negative(db).contains(&target) =>
|
||||
{
|
||||
false
|
||||
}
|
||||
|
||||
// A fully static typevar is a subtype of its upper bound, and to something similar to
|
||||
@@ -1019,16 +1034,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
(Type::Union(union), _) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| elem_ty.is_subtype_of(db, target)),
|
||||
|
||||
(_, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| self.is_subtype_of(db, elem_ty)),
|
||||
|
||||
// If the typevar is constrained, there must be multiple constraints, and the typevar
|
||||
// might be specialized to any one of them. However, the constraints do not have to be
|
||||
// disjoint, which means an lhs type might be a subtype of all of the constraints.
|
||||
@@ -1042,6 +1047,16 @@ impl<'db> Type<'db> {
|
||||
true
|
||||
}
|
||||
|
||||
(Type::Union(union), _) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| elem_ty.is_subtype_of(db, target)),
|
||||
|
||||
(_, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| self.is_subtype_of(db, elem_ty)),
|
||||
|
||||
// If both sides are intersections we need to handle the right side first
|
||||
// (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B,
|
||||
// but none of A, B, or C is a subtype of (A & B).
|
||||
@@ -1307,12 +1322,24 @@ impl<'db> Type<'db> {
|
||||
// TODO this special case might be removable once the below cases are comprehensive
|
||||
(_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true,
|
||||
|
||||
// A typevar is always assignable to itself, and is never assignable to any other
|
||||
// typevar, since there is no guarantee that they will be specialized to the same
|
||||
// type. (This is true even if both typevars are bounded by the same final class, since
|
||||
// you can specialize the typevars to `Never` in addition to that final class.)
|
||||
(Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => {
|
||||
self_typevar == other_typevar
|
||||
// In general, a TypeVar `T` is not assignable to a type `S` unless one of the two conditions is satisfied:
|
||||
// 1. `T` is a bound TypeVar and `T`'s upper bound is assignable to `S`.
|
||||
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
|
||||
// 2. `T` is a constrained TypeVar and all of `T`'s constraints are assignable to `S`.
|
||||
//
|
||||
// However, there is one exception to this general rule: for any given typevar `T`,
|
||||
// `T` will always be assignable to any union containing `T`.
|
||||
// A similar rule applies in reverse to intersection types.
|
||||
(Type::TypeVar(_), Type::Union(union)) if union.elements(db).contains(&self) => true,
|
||||
(Type::Intersection(intersection), Type::TypeVar(_))
|
||||
if intersection.positive(db).contains(&target) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::Intersection(intersection), Type::TypeVar(_))
|
||||
if intersection.negative(db).contains(&target) =>
|
||||
{
|
||||
false
|
||||
}
|
||||
|
||||
// A typevar is assignable to its upper bound, and to something similar to the union of
|
||||
@@ -1331,18 +1358,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
// A union is assignable to a type T iff every element of the union is assignable to T.
|
||||
(Type::Union(union), ty) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| elem_ty.is_assignable_to(db, ty)),
|
||||
|
||||
// A type T is assignable to a union iff T is assignable to any element of the union.
|
||||
(ty, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
|
||||
|
||||
// If the typevar is constrained, there must be multiple constraints, and the typevar
|
||||
// might be specialized to any one of them. However, the constraints do not have to be
|
||||
// disjoint, which means an lhs type might be assignable to all of the constraints.
|
||||
@@ -1356,6 +1371,18 @@ impl<'db> Type<'db> {
|
||||
true
|
||||
}
|
||||
|
||||
// A union is assignable to a type T iff every element of the union is assignable to T.
|
||||
(Type::Union(union), ty) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| elem_ty.is_assignable_to(db, ty)),
|
||||
|
||||
// A type T is assignable to a union iff T is assignable to any element of the union.
|
||||
(ty, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
|
||||
|
||||
// If both sides are intersections we need to handle the right side first
|
||||
// (A & B & C) is assignable to (A & B) because the left is assignable to both A and B,
|
||||
// but none of A, B, or C is assignable to (A & B).
|
||||
@@ -1489,6 +1516,10 @@ impl<'db> Type<'db> {
|
||||
false
|
||||
}
|
||||
|
||||
(Type::SliceLiteral(_), _) => KnownClass::Slice
|
||||
.to_instance(db)
|
||||
.is_assignable_to(db, target),
|
||||
|
||||
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
|
||||
self_function_literal
|
||||
.into_callable_type(db)
|
||||
@@ -1570,8 +1601,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
(Type::TypeVar(first), Type::TypeVar(second)) => first == second,
|
||||
|
||||
(Type::NominalInstance(first), Type::NominalInstance(second)) => {
|
||||
first.is_gradual_equivalent_to(db, second)
|
||||
}
|
||||
@@ -1619,6 +1648,13 @@ impl<'db> Type<'db> {
|
||||
false
|
||||
}
|
||||
|
||||
(tvar @ Type::TypeVar(_), Type::Intersection(intersection))
|
||||
| (Type::Intersection(intersection), tvar @ Type::TypeVar(_))
|
||||
if intersection.negative(db).contains(&tvar) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// An unbounded typevar is never disjoint from any other type, since it might be
|
||||
// specialized to any type. A bounded typevar is not disjoint from its bound, and is
|
||||
// only disjoint from other types if its bound is. A constrained typevar is disjoint
|
||||
@@ -4643,6 +4679,7 @@ impl<'db> Type<'db> {
|
||||
pub fn in_type_expression(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
scope_id: ScopeId,
|
||||
) -> Result<Type<'db>, InvalidTypeExpressionError<'db>> {
|
||||
match self {
|
||||
// Special cases for `float` and `complex`
|
||||
@@ -4727,7 +4764,40 @@ impl<'db> Type<'db> {
|
||||
// TODO: Use an opt-in rule for a bare `Callable`
|
||||
KnownInstanceType::Callable => Ok(Type::Callable(CallableType::unknown(db))),
|
||||
|
||||
KnownInstanceType::TypingSelf => Ok(todo_type!("Support for `typing.Self`")),
|
||||
KnownInstanceType::TypingSelf => {
|
||||
let index = semantic_index(db, scope_id.file(db));
|
||||
let Some(class_ty) = enclosing_class_symbol(db, index, scope_id) else {
|
||||
return Err(InvalidTypeExpressionError {
|
||||
fallback_type: Type::unknown(),
|
||||
invalid_expressions: smallvec::smallvec![
|
||||
InvalidTypeExpression::InvalidType(*self)
|
||||
],
|
||||
});
|
||||
};
|
||||
let Some(TypeDefinition::Class(class_def)) = class_ty.definition(db) else {
|
||||
debug_assert!(
|
||||
false,
|
||||
"enclosing_class_symbol must return a type with class definition"
|
||||
);
|
||||
return Ok(Type::unknown());
|
||||
};
|
||||
let Some(instance) = class_ty.to_instance(db) else {
|
||||
debug_assert!(
|
||||
false,
|
||||
"enclosing_class_symbol must return type that can be instantiated"
|
||||
);
|
||||
return Ok(Type::unknown());
|
||||
};
|
||||
Ok(Type::TypeVar(TypeVarInstance::new(
|
||||
db,
|
||||
ast::name::Name::new("Self"),
|
||||
class_def,
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(instance)),
|
||||
TypeVarVariance::Invariant,
|
||||
None,
|
||||
TypeVarKind::Legacy,
|
||||
)))
|
||||
}
|
||||
KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")),
|
||||
KnownInstanceType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")),
|
||||
|
||||
@@ -4794,7 +4864,7 @@ impl<'db> Type<'db> {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
let mut invalid_expressions = smallvec::SmallVec::default();
|
||||
for element in union.elements(db) {
|
||||
match element.in_type_expression(db) {
|
||||
match element.in_type_expression(db, scope_id) {
|
||||
Ok(type_expr) => builder = builder.add(type_expr),
|
||||
Err(InvalidTypeExpressionError {
|
||||
fallback_type,
|
||||
@@ -5618,6 +5688,9 @@ pub struct TypeVarInstance<'db> {
|
||||
/// The upper bound or constraint on the type of this TypeVar
|
||||
bound_or_constraints: Option<TypeVarBoundOrConstraints<'db>>,
|
||||
|
||||
/// The variance of the TypeVar
|
||||
variance: TypeVarVariance,
|
||||
|
||||
/// The default type for this TypeVar
|
||||
default_ty: Option<Type<'db>>,
|
||||
|
||||
@@ -5646,7 +5719,15 @@ impl<'db> TypeVarInstance<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub enum TypeVarVariance {
|
||||
Invariant,
|
||||
Covariant,
|
||||
Contravariant,
|
||||
Bivariant,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub enum TypeVarBoundOrConstraints<'db> {
|
||||
UpperBound(Type<'db>),
|
||||
Constraints(UnionType<'db>),
|
||||
@@ -6957,6 +7038,8 @@ pub enum KnownFunction {
|
||||
IsSingleValued,
|
||||
/// `ty_extensions.generic_context`
|
||||
GenericContext,
|
||||
/// `ty_extensions.dunder_all_names`
|
||||
DunderAllNames,
|
||||
}
|
||||
|
||||
impl KnownFunction {
|
||||
@@ -7013,6 +7096,7 @@ impl KnownFunction {
|
||||
| Self::IsSingleton
|
||||
| Self::IsSubtypeOf
|
||||
| Self::GenericContext
|
||||
| Self::DunderAllNames
|
||||
| Self::StaticAssert => module.is_ty_extensions(),
|
||||
}
|
||||
}
|
||||
@@ -7320,7 +7404,7 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
imported_symbol(db, self.module(db).file(), name).symbol
|
||||
imported_symbol(db, self.module(db).file(), name, None).symbol
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8409,6 +8493,7 @@ pub(crate) mod tests {
|
||||
KnownFunction::IsSingleton
|
||||
| KnownFunction::IsSubtypeOf
|
||||
| KnownFunction::GenericContext
|
||||
| KnownFunction::DunderAllNames
|
||||
| KnownFunction::StaticAssert
|
||||
| KnownFunction::IsFullyStatic
|
||||
| KnownFunction::IsDisjointFrom
|
||||
|
||||
@@ -10,6 +10,7 @@ use super::{
|
||||
InferContext, Signature, Signatures, Type,
|
||||
};
|
||||
use crate::db::Db;
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::symbol::{Boundness, Symbol};
|
||||
use crate::types::diagnostic::{
|
||||
CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT,
|
||||
@@ -20,8 +21,8 @@ use crate::types::generics::{Specialization, SpecializationBuilder, Specializati
|
||||
use crate::types::signatures::{Parameter, ParameterForm};
|
||||
use crate::types::{
|
||||
todo_type, BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators,
|
||||
KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType,
|
||||
TupleType, UnionType, WrapperDescriptorKind,
|
||||
FunctionType, KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind,
|
||||
PropertyInstanceType, TupleType, UnionType, WrapperDescriptorKind,
|
||||
};
|
||||
use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic};
|
||||
use ruff_python_ast as ast;
|
||||
@@ -585,6 +586,30 @@ impl<'db> Bindings<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::DunderAllNames) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(match ty {
|
||||
Type::ModuleLiteral(module_literal) => {
|
||||
match dunder_all_names(db, module_literal.module(db).file())
|
||||
{
|
||||
Some(names) => {
|
||||
let mut names = names.iter().collect::<Vec<_>>();
|
||||
names.sort();
|
||||
TupleType::from_elements(
|
||||
db,
|
||||
names.iter().map(|name| {
|
||||
Type::string_literal(db, name.as_str())
|
||||
}),
|
||||
)
|
||||
}
|
||||
None => Type::none(db),
|
||||
}
|
||||
}
|
||||
_ => Type::none(db),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::Len) => {
|
||||
if let [Some(first_arg)] = overload.parameter_types() {
|
||||
if let Some(len_ty) = first_arg.len(db) {
|
||||
@@ -622,7 +647,7 @@ impl<'db> Bindings<'db> {
|
||||
db,
|
||||
protocol_class
|
||||
.interface(db)
|
||||
.members()
|
||||
.members(db)
|
||||
.map(|member| Type::string_literal(db, member.name()))
|
||||
.collect::<Box<[Type<'db>]>>(),
|
||||
)));
|
||||
@@ -770,29 +795,50 @@ impl<'db> Bindings<'db> {
|
||||
}
|
||||
|
||||
_ => {
|
||||
if let Some(params) = function_type.dataclass_transformer_params(db) {
|
||||
// This is a call to a custom function that was decorated with `@dataclass_transformer`.
|
||||
// If this function was called with a keyword argument like `order=False`, we extract
|
||||
// the argument type and overwrite the corresponding flag in `dataclass_params` after
|
||||
// constructing them from the `dataclass_transformer`-parameter defaults.
|
||||
let mut handle_dataclass_transformer_params =
|
||||
|function_type: &FunctionType| {
|
||||
if let Some(params) =
|
||||
function_type.dataclass_transformer_params(db)
|
||||
{
|
||||
// This is a call to a custom function that was decorated with `@dataclass_transformer`.
|
||||
// If this function was called with a keyword argument like `order=False`, we extract
|
||||
// the argument type and overwrite the corresponding flag in `dataclass_params` after
|
||||
// constructing them from the `dataclass_transformer`-parameter defaults.
|
||||
|
||||
let mut dataclass_params = DataclassParams::from(params);
|
||||
let mut dataclass_params = DataclassParams::from(params);
|
||||
|
||||
if let Some(Some(Type::BooleanLiteral(order))) = callable_signature
|
||||
if let Some(Some(Type::BooleanLiteral(order))) =
|
||||
callable_signature.iter().nth(overload_index).and_then(
|
||||
|signature| {
|
||||
let (idx, _) = signature
|
||||
.parameters()
|
||||
.keyword_by_name("order")?;
|
||||
overload.parameter_types().get(idx)
|
||||
},
|
||||
)
|
||||
{
|
||||
dataclass_params.set(DataclassParams::ORDER, *order);
|
||||
}
|
||||
|
||||
overload.set_return_type(Type::DataclassDecorator(
|
||||
dataclass_params,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Ideally, either the implementation, or exactly one of the overloads
|
||||
// of the function can have the dataclass_transform decorator applied.
|
||||
// However, we do not yet enforce this, and in the case of multiple
|
||||
// applications of the decorator, we will only consider the last one
|
||||
// for the return value, since the prior ones will be over-written.
|
||||
if let Some(overloaded) = function_type.to_overloaded(db) {
|
||||
overloaded
|
||||
.overloads
|
||||
.iter()
|
||||
.nth(overload_index)
|
||||
.and_then(|signature| {
|
||||
let (idx, _) =
|
||||
signature.parameters().keyword_by_name("order")?;
|
||||
overload.parameter_types().get(idx)
|
||||
})
|
||||
{
|
||||
dataclass_params.set(DataclassParams::ORDER, *order);
|
||||
}
|
||||
|
||||
overload
|
||||
.set_return_type(Type::DataclassDecorator(dataclass_params));
|
||||
.for_each(&mut handle_dataclass_transformer_params);
|
||||
}
|
||||
|
||||
handle_dataclass_transformer_params(&function_type);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -290,109 +290,82 @@ impl<'db> ClassType<'db> {
|
||||
})
|
||||
}
|
||||
|
||||
/// If `self` and `other` are generic aliases of the same generic class, returns their
|
||||
/// corresponding specializations.
|
||||
fn compatible_specializations(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: ClassType<'db>,
|
||||
) -> Option<(Specialization<'db>, Specialization<'db>)> {
|
||||
match (self, other) {
|
||||
(ClassType::Generic(self_generic), ClassType::Generic(other_generic)) => {
|
||||
if self_generic.origin(db) == other_generic.origin(db) {
|
||||
Some((
|
||||
self_generic.specialization(db),
|
||||
other_generic.specialization(db),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if `other` is present in this class's MRO.
|
||||
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
|
||||
// participate, so we should not return `True` if we find `Any/Unknown` in the MRO.
|
||||
if self.iter_mro(db).contains(&ClassBase::Class(other)) {
|
||||
return true;
|
||||
}
|
||||
self.iter_mro(db).any(|base| {
|
||||
match base {
|
||||
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
|
||||
// participate.
|
||||
ClassBase::Dynamic(_) => false,
|
||||
|
||||
// `self` is a subclass of `other` if they are both generic aliases of the same generic
|
||||
// class, and their specializations are compatible, taking into account the variance of the
|
||||
// class's typevars.
|
||||
if let Some((self_specialization, other_specialization)) =
|
||||
self.compatible_specializations(db, other)
|
||||
{
|
||||
if self_specialization.is_subtype_of(db, other_specialization) {
|
||||
return true;
|
||||
// Protocol and Generic are not represented by a ClassType.
|
||||
ClassBase::Protocol | ClassBase::Generic(_) => false,
|
||||
|
||||
ClassBase::Class(base) => match (base, other) {
|
||||
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
|
||||
(ClassType::Generic(base), ClassType::Generic(other)) => {
|
||||
base.origin(db) == other.origin(db)
|
||||
&& base
|
||||
.specialization(db)
|
||||
.is_subtype_of(db, other.specialization(db))
|
||||
}
|
||||
(ClassType::Generic(_), ClassType::NonGeneric(_))
|
||||
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
match (self, other) {
|
||||
(ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other,
|
||||
(ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false,
|
||||
|
||||
// `self` is equivalent to `other` if they are both generic aliases of the same generic
|
||||
// class, and their specializations are compatible, taking into account the variance of the
|
||||
// class's typevars.
|
||||
if let Some((self_specialization, other_specialization)) =
|
||||
self.compatible_specializations(db, other)
|
||||
{
|
||||
if self_specialization.is_equivalent_to(db, other_specialization) {
|
||||
return true;
|
||||
(ClassType::Generic(this), ClassType::Generic(other)) => {
|
||||
this.origin(db) == other.origin(db)
|
||||
&& this
|
||||
.specialization(db)
|
||||
.is_equivalent_to(db, other.specialization(db))
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
if self.is_subclass_of(db, other) {
|
||||
return true;
|
||||
}
|
||||
self.iter_mro(db).any(|base| {
|
||||
match base {
|
||||
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) => !other.is_final(db),
|
||||
ClassBase::Dynamic(_) => false,
|
||||
|
||||
// `self` is assignable to `other` if they are both generic aliases of the same generic
|
||||
// class, and their specializations are compatible, taking into account the variance of the
|
||||
// class's typevars.
|
||||
if let Some((self_specialization, other_specialization)) =
|
||||
self.compatible_specializations(db, other)
|
||||
{
|
||||
if self_specialization.is_assignable_to(db, other_specialization) {
|
||||
return true;
|
||||
// Protocol and Generic are not represented by a ClassType.
|
||||
ClassBase::Protocol | ClassBase::Generic(_) => false,
|
||||
|
||||
ClassBase::Class(base) => match (base, other) {
|
||||
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
|
||||
(ClassType::Generic(base), ClassType::Generic(other)) => {
|
||||
base.origin(db) == other.origin(db)
|
||||
&& base
|
||||
.specialization(db)
|
||||
.is_assignable_to(db, other.specialization(db))
|
||||
}
|
||||
(ClassType::Generic(_), ClassType::NonGeneric(_))
|
||||
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_subclass_of_any_or_unknown(db) && !other.is_final(db) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
match (self, other) {
|
||||
(ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other,
|
||||
(ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false,
|
||||
|
||||
// `self` is equivalent to `other` if they are both generic aliases of the same generic
|
||||
// class, and their specializations are compatible, taking into account the variance of the
|
||||
// class's typevars.
|
||||
if let Some((self_specialization, other_specialization)) =
|
||||
self.compatible_specializations(db, other)
|
||||
{
|
||||
if self_specialization.is_gradual_equivalent_to(db, other_specialization) {
|
||||
return true;
|
||||
(ClassType::Generic(this), ClassType::Generic(other)) => {
|
||||
this.origin(db) == other.origin(db)
|
||||
&& this
|
||||
.specialization(db)
|
||||
.is_gradual_equivalent_to(db, other.specialization(db))
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred.
|
||||
@@ -1799,26 +1772,32 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`Span`] of the class's "header": the class name
|
||||
/// Returns a [`Span`] with the range of the class's header.
|
||||
///
|
||||
/// See [`Self::header_range`] for more details.
|
||||
pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
|
||||
Span::from(self.file(db)).with_range(self.header_range(db))
|
||||
}
|
||||
|
||||
/// Returns the range of the class's "header": the class name
|
||||
/// and any arguments passed to the `class` statement. E.g.
|
||||
///
|
||||
/// ```ignore
|
||||
/// class Foo(Bar, metaclass=Baz): ...
|
||||
/// ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
|
||||
pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange {
|
||||
let class_scope = self.body_scope(db);
|
||||
let class_node = class_scope.node(db).expect_class();
|
||||
let class_name = &class_node.name;
|
||||
let header_range = TextRange::new(
|
||||
TextRange::new(
|
||||
class_name.start(),
|
||||
class_node
|
||||
.arguments
|
||||
.as_deref()
|
||||
.map(Ranged::end)
|
||||
.unwrap_or_else(|| class_name.end()),
|
||||
);
|
||||
Span::from(class_scope.file(db)).with_range(header_range)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user