mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-10 05:38:15 +00:00
[ty] Improve and document equivalence for module-literal types (#19243)
This commit is contained in:
parent
59aa869724
commit
934aaa23f3
3 changed files with 89 additions and 18 deletions
|
@ -502,5 +502,55 @@ def f6(a, /): ...
|
||||||
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
|
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Module-literal types
|
||||||
|
|
||||||
|
Two "copies" of a single-file module are considered equivalent types, even if the different copies
|
||||||
|
were originally imported in different first-party modules:
|
||||||
|
|
||||||
|
`module.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import typing
|
||||||
|
```
|
||||||
|
|
||||||
|
`main.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import typing
|
||||||
|
from module import typing as other_typing
|
||||||
|
from ty_extensions import TypeOf, static_assert, is_equivalent_to
|
||||||
|
|
||||||
|
static_assert(is_equivalent_to(TypeOf[typing], TypeOf[other_typing]))
|
||||||
|
static_assert(is_equivalent_to(TypeOf[typing] | int | str, str | int | TypeOf[other_typing]))
|
||||||
|
```
|
||||||
|
|
||||||
|
We currently do not consider module-literal types to be equivalent if the underlying module is a
|
||||||
|
package and the different "copies" of the module were originally imported in different modules. This
|
||||||
|
is because we might consider submodules to be available as attributes on one copy but not on the
|
||||||
|
other, depending on whether those submodules were explicitly imported in the original importing
|
||||||
|
module:
|
||||||
|
|
||||||
|
`module2.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import importlib
|
||||||
|
import importlib.abc
|
||||||
|
```
|
||||||
|
|
||||||
|
`main2.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import importlib
|
||||||
|
from module2 import importlib as other_importlib
|
||||||
|
from ty_extensions import TypeOf, static_assert, is_equivalent_to
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `<module 'importlib'>` has no attribute `abc`"
|
||||||
|
reveal_type(importlib.abc) # revealed: Unknown
|
||||||
|
|
||||||
|
reveal_type(other_importlib.abc) # revealed: <module 'importlib.abc'>
|
||||||
|
|
||||||
|
static_assert(not is_equivalent_to(TypeOf[importlib], TypeOf[other_importlib]))
|
||||||
|
```
|
||||||
|
|
||||||
[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
|
[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
|
||||||
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
|
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
|
||||||
|
|
|
@ -818,7 +818,11 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: &Module) -> Self {
|
pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: &Module) -> Self {
|
||||||
Self::ModuleLiteral(ModuleLiteralType::new(db, importing_file, submodule))
|
Self::ModuleLiteral(ModuleLiteralType::new(
|
||||||
|
db,
|
||||||
|
submodule,
|
||||||
|
submodule.kind().is_package().then_some(importing_file),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn into_module_literal(self) -> Option<ModuleLiteralType<'db>> {
|
pub const fn into_module_literal(self) -> Option<ModuleLiteralType<'db>> {
|
||||||
|
@ -7503,20 +7507,34 @@ pub enum WrapperDescriptorKind {
|
||||||
#[salsa::interned(debug)]
|
#[salsa::interned(debug)]
|
||||||
#[derive(PartialOrd, Ord)]
|
#[derive(PartialOrd, Ord)]
|
||||||
pub struct ModuleLiteralType<'db> {
|
pub struct ModuleLiteralType<'db> {
|
||||||
/// The file in which this module was imported.
|
|
||||||
///
|
|
||||||
/// We need this in order to know which submodules should be attached to it as attributes
|
|
||||||
/// (because the submodules were also imported in this file).
|
|
||||||
pub importing_file: File,
|
|
||||||
|
|
||||||
/// The imported module.
|
/// The imported module.
|
||||||
pub module: Module,
|
pub module: Module,
|
||||||
|
|
||||||
|
/// The file in which this module was imported.
|
||||||
|
///
|
||||||
|
/// If the module is a module that could have submodules (a package),
|
||||||
|
/// we need this in order to know which submodules should be attached to it as attributes
|
||||||
|
/// (because the submodules were also imported in this file). For a package, this should
|
||||||
|
/// therefore always be `Some()`. If the module is not a package, however, this should
|
||||||
|
/// always be `None`: this helps reduce memory usage (the information is redundant for
|
||||||
|
/// single-file modules), and ensures that two module-literal types that both refer to
|
||||||
|
/// the same underlying single-file module are understood by ty as being equivalent types
|
||||||
|
/// in all situations.
|
||||||
|
_importing_file: Option<File>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Salsa heap is tracked separately.
|
// The Salsa heap is tracked separately.
|
||||||
impl get_size2::GetSize for ModuleLiteralType<'_> {}
|
impl get_size2::GetSize for ModuleLiteralType<'_> {}
|
||||||
|
|
||||||
impl<'db> ModuleLiteralType<'db> {
|
impl<'db> ModuleLiteralType<'db> {
|
||||||
|
fn importing_file(self, db: &'db dyn Db) -> Option<File> {
|
||||||
|
debug_assert_eq!(
|
||||||
|
self._importing_file(db).is_some(),
|
||||||
|
self.module(db).kind().is_package()
|
||||||
|
);
|
||||||
|
self._importing_file(db)
|
||||||
|
}
|
||||||
|
|
||||||
fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||||
// `__dict__` is a very special member that is never overridden by module globals;
|
// `__dict__` is a very special member that is never overridden by module globals;
|
||||||
// we should always look it up directly as an attribute on `types.ModuleType`,
|
// we should always look it up directly as an attribute on `types.ModuleType`,
|
||||||
|
@ -7536,8 +7554,8 @@ impl<'db> ModuleLiteralType<'db> {
|
||||||
// the parent module's `__init__.py` file being evaluated. That said, we have
|
// the parent module's `__init__.py` file being evaluated. That said, we have
|
||||||
// chosen to always have the submodule take priority. (This matches pyright's
|
// chosen to always have the submodule take priority. (This matches pyright's
|
||||||
// current behavior, but is the opposite of mypy's current behavior.)
|
// current behavior, but is the opposite of mypy's current behavior.)
|
||||||
|
if let Some(importing_file) = self.importing_file(db) {
|
||||||
if let Some(submodule_name) = ModuleName::new(name) {
|
if let Some(submodule_name) = ModuleName::new(name) {
|
||||||
let importing_file = self.importing_file(db);
|
|
||||||
let imported_submodules = imported_modules(db, importing_file);
|
let imported_submodules = imported_modules(db, importing_file);
|
||||||
let mut full_submodule_name = self.module(db).name().clone();
|
let mut full_submodule_name = self.module(db).name().clone();
|
||||||
full_submodule_name.extend(&submodule_name);
|
full_submodule_name.extend(&submodule_name);
|
||||||
|
@ -7548,6 +7566,7 @@ impl<'db> ModuleLiteralType<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.module(db)
|
self.module(db)
|
||||||
.file()
|
.file()
|
||||||
|
|
|
@ -199,8 +199,10 @@ impl<'db> AllMembers<'db> {
|
||||||
|
|
||||||
let module_name = module.name();
|
let module_name = module.name();
|
||||||
self.members.extend(
|
self.members.extend(
|
||||||
imported_modules(db, literal.importing_file(db))
|
literal
|
||||||
.iter()
|
.importing_file(db)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|file| imported_modules(db, file))
|
||||||
.filter_map(|submodule_name| {
|
.filter_map(|submodule_name| {
|
||||||
let module = resolve_module(db, submodule_name)?;
|
let module = resolve_module(db, submodule_name)?;
|
||||||
let ty = Type::module_literal(db, file, &module);
|
let ty = Type::module_literal(db, file, &module);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue