Compare commits

...

27 Commits

Author SHA1 Message Date
Micha Reiser
05854142e7 Test Salsa #851 2025-05-08 10:53:51 +02:00
David Peter
33eb008fb6 Disabled event handler if tracing is not enabled 2025-05-08 10:28:59 +02:00
David Peter
04d4febfc4 [ty] Update salsa 2025-05-08 10:17:32 +02:00
Shaygan Hooshyari
d566636ca5 Support typing.Self in methods (#17689)
## Summary

Fixes: astral-sh/ty#159 

This PR adds support for using `Self` in methods.
When the type of an annotation is `TypingSelf` it is converted to a type
var based on:
https://typing.python.org/en/latest/spec/generics.html#self

I just skipped Protocols because it had more problems and the tests was
not useful.
Also I need to create a follow up PR that implicitly assumes `self`
argument has type `Self`.

In order to infer the type in the `in_type_expression` method I needed
to have scope id and semantic index available. I used the idea from
[this PR](https://github.com/astral-sh/ruff/pull/17589/files) to pass
additional context to this method.
Also I think in all places that `in_type_expression` is called we need
to have this context because `Self` can be there so I didn't split the
method into one version with context and one without.

## Test Plan

Added new tests from spec.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-07 15:58:00 -07:00
Alex Waygood
51cef5a72b [ty] Recognise functions containing yield from expressions as being generator functions (#17930) 2025-05-07 23:29:44 +01:00
Douglas Creager
2cf5cba7ff [ty] Check base classes when determining subtyping etc for generic aliases (#17927)
#17897 added variance handling for legacy typevars — but they were only
being considered when checking generic aliases of the same class:

```py
class A: ...
class B(A): ...

class C[T]: ...

static_assert(is_subtype_of(C[B], C[A]))
```

and not for generic subclasses:

```py
class D[U](C[U]): ...

static_assert(is_subtype_of(D[B], C[A]))
```

Now we check those too!

Closes https://github.com/astral-sh/ty/issues/101
2025-05-07 15:21:11 -04:00
yunchi
ce0800fccf [pylint] add fix safety section (PLC2801) (#17825)
parent: #15584 
fix was introduced at: #9587 
reasoning: #9572
2025-05-07 14:34:34 -04:00
Micha Reiser
d03a7069ad Add instructions on how to upgrade to a newer Rust version (#17928) 2025-05-07 20:11:58 +02:00
Abhijeet Prasad Bodas
f5096f2050 [parser] Flag single unparenthesized generator expr with trailing comma in arguments. (#17893)
Fixes #17867

## Summary

The CPython parser does not allow generator expressions which are the
sole arguments in an argument list to have a trailing comma.
With this change, we start flagging such instances.

## Test Plan

Added new inline tests.
2025-05-07 14:11:35 -04:00
Alex Waygood
895b6161a6 [ty] Ensure that T is disjoint from ~T even when T is a TypeVar (#17922)
Same as https://github.com/astral-sh/ruff/pull/17910 but for
disjointness
2025-05-07 10:59:16 -07:00
Alex Waygood
74fe7982ba [ty] Sort collected diagnostics before snapshotting them in mdtest (#17926) 2025-05-07 18:23:22 +01:00
Micha Reiser
51386b3c7a [ty] Add basic file watching to server (#17912) 2025-05-07 19:03:30 +02:00
Charlie Marsh
51e2effd2d Make completions an opt-in LSP feature (#17921)
## Summary

We now expect the client to send initialization options to opt-in to
experimental (but LSP-standardized) features, like completion support.
Specifically, the client should set `"experimental.completions.enable":
true`.

Closes https://github.com/astral-sh/ty/issues/74.
2025-05-07 16:39:35 +00:00
Dhruv Manilawala
82d31a6014 Add link to ty issue tracker (#17924)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-07 16:33:27 +00:00
Dhruv Manilawala
78054824c0 [ty] Add support for __all__ (#17856)
## Summary

This PR adds support for the `__all__` module variable.

Reference spec:
https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols

This PR adds a new `dunder_all_names` query that returns a set of
`Name`s defined in the `__all__` variable of the given `File`. The query
works by implementing the `StatementVisitor` and collects all the names
by recognizing the supported idioms as mentioned in the spec. Any idiom
that's not recognized are ignored.

The current implementation is minimum to what's required for us to
remove all the false positives that this is causing. Refer to the
"Follow-ups" section below to see what we can do next. I'll a open
separate issue to keep track of them.

Closes: astral-sh/ty#106 
Closes: astral-sh/ty#199

### Follow-ups

* Diagnostics:
* Add warning diagnostics for unrecognized `__all__` idioms, `__all__`
containing non-string element
* Add an error diagnostic for elements that are present in `__all__` but
not defined in the module. This could lead to runtime error
* Maybe we should return `<type>` instead of `Unknown | <type>` for
`module.__all__`. For example:
https://playknot.ruff.rs/2a6fe5d7-4e16-45b1-8ec3-d79f2d4ca894
* Mark a symbol that's mentioned in `__all__` as used otherwise it could
raise (possibly in the future) "unused-name" diagnostic

Supporting diagnostics will require that we update the return type of
the query to be something other than `Option<FxHashSet<Name>>`,
something that behaves like a result and provides a way to check whether
a name exists in `__all__`, loop over elements in `__all__`, loop over
the invalid elements, etc.

## Ecosystem analysis

The following are the maximum amount of diagnostics **removed** in the
ecosystem:

* "Type <module '...'> has no attribute ..."
    * `collections.abc` - 14
    * `numpy` - 35534
    * `numpy.ma` - 296
    * `numpy.char` - 37
    * `numpy.testing` - 175
    * `hashlib` - 311
    * `scipy.fft` - 2
    * `scipy.stats` - 38
* "Module '...' has no member ..."
    * `collections.abc` - 85
    * `numpy` - 508
    * `numpy.testing` - 741
    * `hashlib` - 36
    * `scipy.stats` - 68
    * `scipy.interpolate` - 7
    * `scipy.signal` - 5

The following modules have dynamic `__all__` definition, so `ty` assumes
that `__all__` doesn't exists in that module:
* `scipy.stats`
(95a5d6ea8b/scipy/stats/__init__.py (L665))
* `scipy.interpolate`
(95a5d6ea8b/scipy/interpolate/__init__.py (L221))
* `scipy.signal` (indirectly via
95a5d6ea8b/scipy/signal/_signal_api.py (L30))
* `numpy.testing`
(de784cd6ee/numpy/testing/__init__.py (L16-L18))

~There's this one category of **false positives** that have been added:~
Fixed the false positives by also ignoring `__all__` from a module that
uses unrecognized idioms.

<details><summary>Details about the false postivie:</summary>
<p>

The `scipy.stats` module has dynamic `__all__` and it imports a bunch of
symbols via star imports. Some of those modules have a mix of valid and
invalid `__all__` idioms. For example, in
95a5d6ea8b/scipy/stats/distributions.py (L18-L24),
2 out of 4 `__all__` idioms are invalid but currently `ty` recognizes
two of them and says that the module has a `__all__` with 5 values. This
leads to around **2055** newly added false positives of the form:
```
Type <module 'scipy.stats'> has no attribute ...
```

I think the fix here is to completely ignore `__all__`, not only if
there are invalid elements in it, but also if there are unrecognized
idioms used in the module.

</p>
</details> 

## Test Plan

Add a bunch of test cases using the new `ty_extensions.dunder_all_names`
function to extract a module's `__all__` names.

Update various test cases to remove false positives around `*` imports
and re-export convention.

Add new test cases for named import behavior as `*` imports covers all
of it already (thanks Alex!).
2025-05-07 21:42:42 +05:30
Alex Waygood
c6f4929cdc [ty] fix assigning a typevar to a union with itself (#17910)
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-07 15:50:22 +00:00
Alex Waygood
2ec0d7e072 [ty] Improve UX for [duplicate-base] diagnostics (#17914) 2025-05-07 15:27:37 +00:00
Charlie Marsh
ad658f4d68 Clean up some Ruff references in the ty server (#17920)
## Summary

Anything user-facing, etc.
2025-05-07 10:55:16 -04:00
Abhijeet Prasad Bodas
3dedd70a92 [ty] Detect overloads decorated with @dataclass_transform (#17835)
## Summary

Fixes #17541

Before this change, in the case of overloaded functions,
`@dataclass_transform` was detected only when applied to the
implementation, not the overloads.
However, the spec also allows this decorator to be applied to any of the
overloads as well.
With this PR, we start handling `@dataclass_transform`s applied to
overloads.

## Test Plan

Fixed existing TODOs in the test suite.
2025-05-07 15:51:13 +02:00
David Peter
fab862c8cd [ty] Ecosystem checks: activate running on 'manticore' (#17916)
## Summary

This is sort of an anticlimactic resolution to #17863, but now that we
understand what the root cause for the stack overflows was, I think it's
fine to enable running on this project. See the linked ticket for the
full analysis.

closes #17863

## Test Plan

Ran lots of times locally and never observed a crash at worker thread
stack sizes > 8 MiB.
2025-05-07 06:27:36 -07:00
Douglas Creager
0d9b6a0975 [ty] Handle explicit variance in legacy typevars (#17897)
We now track the variance of each typevar, and obey the `covariant` and
`contravariant` parameters to the legacy `TypeVar` constructor. We still
don't yet infer variance for PEP-695 typevars or for the
`infer_variance` legacy constructor parameter.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-05-07 08:44:51 -04:00
Micha Reiser
c5e299e796 Update salsa (#17895) 2025-05-07 09:51:15 +02:00
Victor Hugo Gomes
c504001b32 [pyupgrade] Add spaces between tokens as necessary to avoid syntax errors in UP018 autofix (#17648)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-07 09:34:08 +02:00
David Peter
04457f99b6 [ty] Protocols: Fixpoint iteration for fully-static check (#17880)
## Summary

A recursive protocol like the following would previously lead to stack
overflows when attempting to create the union type for the `P | None`
member, because `UnionBuilder` checks if element types are fully static,
and the fully-static check on `P` would in turn list all members and
check whether all of them were fully static, leading to a cycle.

```py
from __future__ import annotations

from typing import Protocol

class P(Protocol):
    parent: P | None
```

Here, we make the fully-static check on protocols a salsa query and add
fixpoint iteration, starting with `true` as the initial value (assume
that the recursive protocol is fully-static). If the recursive protocol
has any non-fully-static members, we still return `false` when
re-executing the query (see newly added tests).

closes #17861

## Test Plan

Added regression test
2025-05-07 08:55:21 +02:00
InSync
a33d0d4bf4 [ty] Support generate-shell-completion (#17879)
## Summary

Resolves #15502.

`ty generate-shell-completion` now works in a similar manner to `ruff
generate-shell-completion`.

## Test Plan

Manually:

<details>

```shell
$ cargo run --package ty generate-shell-completion nushell
module completions {

  # An extremely fast Python type checker.
  export extern ty [
    --help(-h)                # Print help
    --version(-V)             # Print version
  ]
  
  # ...

}

export use completions *
```
</details>
2025-05-06 18:04:57 -07:00
Charlie Marsh
443f62e98d Remove condensed display type enum (#17902)
## Summary

See: https://github.com/astral-sh/ruff/pull/17889#discussion_r2076556002
2025-05-06 18:04:03 -07:00
Charlie Marsh
a2e9a7732a Update class literal display to use <class 'Foo'> style (#17889)
## Summary

Closes https://github.com/astral-sh/ruff/issues/17238.
2025-05-06 20:11:25 -04:00
124 changed files with 4874 additions and 1169 deletions

View File

@@ -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.

View File

@@ -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
View File

@@ -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",

View File

@@ -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"] }

View File

@@ -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

View File

@@ -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 {}
}

View File

@@ -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 {}

View File

@@ -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()

View File

@@ -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__()

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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),)

View File

@@ -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)),)

View File

@@ -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);
}
}

View File

@@ -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]

View File

@@ -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
|

View File

@@ -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: [],
},
},
),
},
),
],
},
)

View File

@@ -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 }

View File

@@ -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)]

View File

@@ -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)
}
}
}

View File

@@ -6,6 +6,7 @@ use ruff_text_size::TextSize;
use crate::Db;
#[derive(Debug, Clone)]
pub struct Completion {
pub label: String,
}

View File

@@ -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 {}
}

View File

@@ -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 {}
}

View File

@@ -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

View File

@@ -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'>]
```

View File

@@ -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()

View 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)
```

View File

@@ -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__)
```

View File

@@ -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

View File

@@ -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

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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): ...

View File

@@ -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

View File

@@ -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]
```

View File

@@ -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

View File

@@ -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:

View File

@@ -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"

View File

@@ -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

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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`:

View File

@@ -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

View File

@@ -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`:

View 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

View File

@@ -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'>]
```

View File

@@ -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`:

View File

@@ -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

View File

@@ -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`:

View File

@@ -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]

View File

@@ -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'>]
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
```

View File

@@ -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.

View File

@@ -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]
```

View File

@@ -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]
```

View File

@@ -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

View File

@@ -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]

View File

@@ -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.

View File

@@ -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: ...

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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
|
```

View File

@@ -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
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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__)
```

View File

@@ -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"

View File

@@ -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'>
```

View File

@@ -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'>]
```

View File

@@ -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

View File

@@ -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
```

View File

@@ -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_)

View File

@@ -50,6 +50,7 @@ jinja
koda-validate
kopf
kornia
manticore
materialize
meson
mitmproxy

View File

@@ -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

View 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()))
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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);
}
},

View File

@@ -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