Compare commits

...

6 Commits

Author SHA1 Message Date
Aria Desires
901322f9de more cascady 2026-01-09 12:44:49 -05:00
Aria Desires
a59bf83854 checkpoint working 2026-01-09 12:29:35 -05:00
Aria Desires
4b569cea74 cleanup 2026-01-09 10:23:20 -05:00
Aria Desires
26bf64b9ef fixup 2026-01-07 21:57:44 -05:00
Aria Desires
f87146ea54 try optimization 2026-01-07 21:53:05 -05:00
Aria Desires
b0abf9808e Rework module resolution to be breadth-first instead of depth-first 2026-01-07 20:37:19 -05:00
4 changed files with 326 additions and 374 deletions

View File

@@ -325,6 +325,10 @@ impl ModulePath {
relative_path: relative_path.with_extension("py"),
})
}
pub(crate) fn into_search_path(self) -> SearchPath {
self.search_path
}
}
impl PartialEq<SystemPathBuf> for ModulePath {

View File

@@ -32,11 +32,8 @@ specifies ty's implementation of Python's import resolution algorithm.
*/
use std::borrow::Cow;
use std::fmt;
use std::iter::FusedIterator;
use std::str::Split;
use compact_str::format_compact;
use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_db::files::{File, FilePath, FileRootKind};
@@ -1102,6 +1099,79 @@ fn desperately_resolve_name(
resolve_name_impl(db, name, mode, search_paths.iter().flatten())
}
#[derive(Debug, Clone, Copy)]
enum ResolvedModule {
NamespacePackage,
LegacyNamespacePackage(File),
RegularPackage(File),
Module(File),
}
#[derive(Debug, Clone)]
struct ModuleResolutionCandidate {
path: ModulePath,
module: ResolvedModule,
py_typed: PyTyped,
}
impl ModuleResolutionCandidate {
// Is this some kind of namespace package?
fn is_any_namespace_package(&self) -> bool {
match self.module {
ResolvedModule::NamespacePackage => true,
ResolvedModule::LegacyNamespacePackage(_) => true,
ResolvedModule::RegularPackage(_) => false,
ResolvedModule::Module(_) => false,
}
}
// This is the module we were actually interested in resolving, complete the resolution
fn into_resolved_name(self) -> ResolvedName {
match self.module {
ResolvedModule::NamespacePackage => ResolvedName::NamespacePackage,
// legacy namespace packages behave like regular packages when they're the target of the resolution
ResolvedModule::LegacyNamespacePackage(file) => {
ResolvedName::FileModule(ResolvedFileModule {
kind: ModuleKind::Package,
search_path: self.path.into_search_path(),
file,
})
}
ResolvedModule::RegularPackage(file) => ResolvedName::FileModule(ResolvedFileModule {
kind: ModuleKind::Package,
search_path: self.path.into_search_path(),
file,
}),
ResolvedModule::Module(file) => ResolvedName::FileModule(ResolvedFileModule {
kind: ModuleKind::Module,
search_path: self.path.into_search_path(),
file,
}),
}
}
fn missing_submodule_is_terminal(&self) -> bool {
if matches!(self.py_typed, PyTyped::Partial) {
return false;
}
// Only regular packages are truly terminal, as a later `foo/__init__.py`
// can shadow `foo.py`. Both shadow namespace packages.
matches!(self.module, ResolvedModule::RegularPackage(_))
}
fn to_str<'a>(&self, db: &'a dyn Db) -> Cow<'a, str> {
match self.module {
ResolvedModule::NamespacePackage => {
Cow::Owned(self.path.to_system_path().unwrap_or_default().to_string())
}
ResolvedModule::LegacyNamespacePackage(file) => Cow::Borrowed(file.path(db).as_str()),
ResolvedModule::RegularPackage(file) => Cow::Borrowed(file.path(db).as_str()),
ResolvedModule::Module(file) => Cow::Borrowed(file.path(db).as_str()),
}
}
}
fn resolve_name_impl<'a>(
db: &dyn Db,
name: &ModuleName,
@@ -1109,109 +1179,250 @@ fn resolve_name_impl<'a>(
search_paths: impl Iterator<Item = &'a SearchPath>,
) -> Option<ResolvedName> {
let python_version = db.python_version();
let resolver_state = ResolverContext::new(db, python_version, mode);
let context = ResolverContext::new(db, python_version, mode);
let is_non_shadowable = mode.is_non_shadowable(python_version.minor, name.as_str());
let mut stub_name = None;
let name = RelaxedModuleName::new(name);
let stub_name = name.to_stub_package();
let mut is_namespace_package = false;
let mut cur_candidates = search_paths
.filter_map(|search_path| {
// When a builtin module is imported, standard module resolution is bypassed:
// the module name always resolves to the stdlib module,
// even if there's a module of the same name in the first-party root
// (which would normally result in the stdlib module being overridden).
// TODO: offer a diagnostic if there is a first-party module of the same name
if is_non_shadowable && !search_path.is_standard_library() {
return None;
}
for search_path in search_paths {
// When a builtin module is imported, standard module resolution is bypassed:
// the module name always resolves to the stdlib module,
// even if there's a module of the same name in the first-party root
// (which would normally result in the stdlib module being overridden).
// TODO: offer a diagnostic if there is a first-party module of the same name
if is_non_shadowable && !search_path.is_standard_library() {
continue;
}
Some(ModuleResolutionCandidate {
path: search_path.to_module_path(),
module: ResolvedModule::NamespacePackage,
py_typed: PyTyped::Untyped,
})
})
.collect::<Vec<_>>();
let mut next_candidates = vec![];
if !search_path.is_standard_library() && resolver_state.mode.stubs_allowed() {
match resolve_name_in_search_path(&resolver_state, &stub_name, search_path) {
Ok((package_kind, _, ResolvedName::FileModule(module))) => {
if package_kind.is_root() && module.kind.is_module() {
// FIXME?: because we have to search every candidate on each step of this loop,
// in theory we can search them all in parallel. However we need to join the parallelism
// at the end of each iteration, and after the first iteration in 99% of cases we will have
// reduced down to a single candidate, so maybe meh?
let mut is_root = true;
for component in name.components() {
// Search for the next component in every search-path
for mut candidate in cur_candidates.drain(..) {
// On the first iteration, look for `mypackage-stubs` as well
// Optimization: stdlib never has these `-stubs`
if is_root
&& context.mode.stubs_allowed()
&& !candidate.path.search_path().is_standard_library()
{
let stub_name = stub_name.get_or_insert_with(|| format!("{component}-stubs"));
let mut stub_candidate = candidate.clone();
if resolve_name_in_search_path(&context, &mut stub_candidate, stub_name).is_ok() {
// `mypackage-stubs.py(i)` is not a valid result
if matches!(stub_candidate.module, ResolvedModule::Module(_)) {
tracing::trace!(
"Search path `{search_path}` contains a module \
named `{stub_name}` but a standalone module isn't a valid stub."
"Search path `{}` contains a module \
named `{stub_name}` but a standalone module isn't a valid stub.",
candidate.path.search_path()
);
} else {
return Some(ResolvedName::FileModule(module));
let shadows_all = stub_candidate.missing_submodule_is_terminal();
next_candidates.push(stub_candidate);
if shadows_all {
break;
}
}
}
Ok((_, _, ResolvedName::NamespacePackage)) => {
is_namespace_package = true;
}
Err((PackageKind::Root, _)) => {
tracing::trace!(
"Search path `{search_path}` contains no stub package named `{stub_name}`."
);
}
Err((PackageKind::Regular, PyTyped::Partial)) => {
tracing::trace!(
"Stub-package in `{search_path}` doesn't contain module: \
`{name}` but it is a partial package, keep going."
);
// stub exists, but the module doesn't. But this is a partial package,
// fall through to looking for a non-stub package
}
Err((PackageKind::Regular, _)) => {
tracing::trace!(
"Stub-package in `{search_path}` doesn't contain module: `{name}`"
);
// stub exists, but the module doesn't.
return None;
}
Err((PackageKind::Namespace, _)) => {
tracing::trace!(
"Stub-package in `{search_path}` doesn't contain module: \
`{name}` but it is a namespace package, keep going."
);
// stub exists, but the module doesn't. But this is a namespace package,
// fall through to looking for a non-stub package
}
if resolve_name_in_search_path(&context, &mut candidate, component).is_err() {
if candidate.missing_submodule_is_terminal() {
// Everything after this package should be shadowed out by this failure
// But the previous results are still in play because they would have
// shadowed this one out anyway.
break;
}
continue;
}
let shadows_all = candidate.missing_submodule_is_terminal();
next_candidates.push(candidate);
if shadows_all {
break;
}
}
match resolve_name_in_search_path(&resolver_state, &name, search_path) {
Ok((_, _, ResolvedName::FileModule(module))) => {
return Some(ResolvedName::FileModule(module));
// Now that we have several candidates, we need to reject candidates that are shadowed.
// There are only two valid situations where we should proceed into the next iteration
// with multiple candidates:
//
// * All the candidates are namespace packages
// * `mypackage-stubs` is a candidate with `PyTyped::Partial`
//
// The existence of a single non-namespace package will shadow
// all namespace packages *regardless of search-path order*.
//
// Similarly, the existence of a single regular package will shadow
// all modules (mymod.py) *regardless of search-path order*.
//
// This is implemented with the `retain` that follows.
//
// We can't do this "delete all namespace packages" eagerly because we want a
// `PyTyped::Partial` regular package to shadow namespace packages after it.
// (FIXME: I guess we could just set a flag not to add them...)
// First record what kinds of things we found
let mut found_regular_package = None;
let mut found_module = None;
let mut found_legacy_namespace_package = None;
for candidate in &next_candidates {
match (candidate.module, candidate.py_typed) {
(ResolvedModule::LegacyNamespacePackage(file), _) => {
found_legacy_namespace_package = Some(file);
}
(ResolvedModule::RegularPackage(file), PyTyped::Untyped | PyTyped::Full) => {
found_regular_package = Some(file);
}
(ResolvedModule::Module(file), PyTyped::Untyped | PyTyped::Full) => {
found_module = Some(file);
}
_ => {}
}
Ok((_, _, ResolvedName::NamespacePackage)) => {
is_namespace_package = true;
}
next_candidates.retain(|candidate| {
if let Some(_legacy) = found_legacy_namespace_package && !matches!(candidate.module, ResolvedModule::LegacyNamespacePackage(_)) {
// TODO: it would be nice to emit a warning about this but we just assume it's fine
}
// Regular packages shadow anything that isn't a regular package independent of order
if let Some(package) = found_regular_package && !matches!(candidate.module, ResolvedModule::RegularPackage(_)) {
tracing::trace!("Discarding namespace package `{}` because a regular package of the same name was found: {}",
candidate.to_str(db),
package.path(db).as_str(),
);
return false;
}
// Modules shadow namespace packages independent of order
if let Some(module) = found_module && candidate.is_any_namespace_package() {
tracing::trace!("Discarding namespace package `{}` because a module of the same name was found: {}",
candidate.to_str(db),
module.path(db).as_str(),
);
return false;
}
true
});
if next_candidates.is_empty() {
return None;
}
// Advance to the next level of candidates while reusing allocations
// (we used `drain` so cur_candidates is empty)
std::mem::swap(&mut cur_candidates, &mut next_candidates);
is_root = false;
}
// We now have a list of candidates that are all correct answers, and we just need to take the
// Best one. Because of the filtering we've done in the loop, and sorting stub-packages to come
// first, this is in fact just "the first one".
cur_candidates
.into_iter()
.next()
.map(ModuleResolutionCandidate::into_resolved_name)
}
/// Attempts to resolve a module name in a particular search path.
///
/// `search_path` should be the directory to start looking for the module.
///
/// `name` should be a complete non-empty module name, e.g, `foo` or
/// `foo.bar.baz`.
///
/// Upon success, this returns the kind of the parent package (root, regular
/// package or namespace package) along with the resolved details of the
/// module: its kind (single-file module or package), the search path in
/// which it was found (guaranteed to be equal to the one given) and the
/// corresponding `File`.
///
/// Upon error, the kind of the parent package is returned.
fn resolve_name_in_search_path(
context: &ResolverContext,
candidate: &mut ModuleResolutionCandidate,
module_name: &str,
) -> Result<(), ()> {
if matches!(candidate.module, ResolvedModule::Module(_)) {
tracing::trace!(
"The non-package {} cannot have child",
candidate.to_str(context.db)
);
return Err(());
}
let package_path = &mut candidate.path;
package_path.push(module_name);
// Check for a regular package first (highest priority)
package_path.push("__init__");
if let Some(init) = resolve_file_module(package_path, context) {
// Remove the `__init__` component for any potential next step
package_path.pop();
candidate.py_typed = package_path
.py_typed(context)
.inherit_parent(candidate.py_typed);
if is_legacy_namespace_package(package_path, context, init) {
candidate.module = ResolvedModule::LegacyNamespacePackage(init);
} else {
candidate.module = ResolvedModule::RegularPackage(init);
}
return Ok(());
}
// Check for a file module next
package_path.pop();
if let Some(file_module) = resolve_file_module(package_path, context) {
candidate.module = ResolvedModule::Module(file_module);
return Ok(());
}
// Last resort, check if a folder with the given name exists. If so,
// then this is a namespace package. We need to skip this check for
// typeshed because the `resolve_file_module` can also return `None` if the
// `__init__.py` exists but isn't available for the current Python version.
// Let's assume that the `xml` module is only available on Python 3.11+ and
// we're resolving for Python 3.10:
//
// * `resolve_file_module("xml/__init__.pyi")` returns `None` even though
// the file exists but the module isn't available for the current Python
// version.
// * The check here would now return `true` because the `xml` directory
// exists, resulting in a false positive for a namespace package.
//
// Since typeshed doesn't use any namespace packages today (May 2025),
// simply skip this check which also helps performance. If typeshed
// ever uses namespace packages, ensure that this check also takes the
// `VERSIONS` file into consideration.
if !package_path.search_path().is_standard_library() && package_path.is_directory(context) {
if let Some(path) = package_path.to_system_path() {
let system = context.db.system();
if system.case_sensitivity().is_case_sensitive()
|| system.path_exists_case_sensitive(
&path,
package_path.search_path().as_system_path().unwrap(),
)
{
candidate.py_typed = package_path
.py_typed(context)
.inherit_parent(candidate.py_typed);
candidate.module = ResolvedModule::NamespacePackage;
return Ok(());
}
Err(kind) => match kind {
(PackageKind::Root, _) => {
tracing::trace!(
"Search path `{search_path}` contains no package named `{name}`."
);
}
(PackageKind::Regular, PyTyped::Partial) => {
tracing::trace!(
"Package in `{search_path}` doesn't contain module: \
`{name}` but it is a partial package, keep going."
);
}
(PackageKind::Regular, _) => {
// For regular packages, don't search the next search path. All files of that
// package must be in the same location
tracing::trace!("Package in `{search_path}` doesn't contain module: `{name}`");
return None;
}
(PackageKind::Namespace, _) => {
tracing::trace!(
"Package in `{search_path}` doesn't contain module: \
`{name}` but it is a namespace package, keep going."
);
}
},
}
}
if is_namespace_package {
return Some(ResolvedName::NamespacePackage);
}
None
Err(())
}
#[derive(Debug)]
@@ -1234,101 +1445,6 @@ struct ResolvedFileModule {
file: File,
}
/// Attempts to resolve a module name in a particular search path.
///
/// `search_path` should be the directory to start looking for the module.
///
/// `name` should be a complete non-empty module name, e.g, `foo` or
/// `foo.bar.baz`.
///
/// Upon success, this returns the kind of the parent package (root, regular
/// package or namespace package) along with the resolved details of the
/// module: its kind (single-file module or package), the search path in
/// which it was found (guaranteed to be equal to the one given) and the
/// corresponding `File`.
///
/// Upon error, the kind of the parent package is returned.
fn resolve_name_in_search_path(
context: &ResolverContext,
name: &RelaxedModuleName,
search_path: &SearchPath,
) -> Result<(PackageKind, PyTyped, ResolvedName), (PackageKind, PyTyped)> {
let mut components = name.components();
let module_name = components.next_back().unwrap();
let resolved_package = resolve_package(search_path, components, context)?;
let mut package_path = resolved_package.path;
package_path.push(module_name);
// Check for a regular package first (highest priority)
package_path.push("__init__");
if let Some(regular_package) = resolve_file_module(&package_path, context) {
return Ok((
resolved_package.kind,
resolved_package.typed,
ResolvedName::FileModule(ResolvedFileModule {
search_path: search_path.clone(),
kind: ModuleKind::Package,
file: regular_package,
}),
));
}
// Check for a file module next
package_path.pop();
if let Some(file_module) = resolve_file_module(&package_path, context) {
return Ok((
resolved_package.kind,
resolved_package.typed,
ResolvedName::FileModule(ResolvedFileModule {
file: file_module,
kind: ModuleKind::Module,
search_path: search_path.clone(),
}),
));
}
// Last resort, check if a folder with the given name exists. If so,
// then this is a namespace package. We need to skip this check for
// typeshed because the `resolve_file_module` can also return `None` if the
// `__init__.py` exists but isn't available for the current Python version.
// Let's assume that the `xml` module is only available on Python 3.11+ and
// we're resolving for Python 3.10:
//
// * `resolve_file_module("xml/__init__.pyi")` returns `None` even though
// the file exists but the module isn't available for the current Python
// version.
// * The check here would now return `true` because the `xml` directory
// exists, resulting in a false positive for a namespace package.
//
// Since typeshed doesn't use any namespace packages today (May 2025),
// simply skip this check which also helps performance. If typeshed
// ever uses namespace packages, ensure that this check also takes the
// `VERSIONS` file into consideration.
if !search_path.is_standard_library() && package_path.is_directory(context) {
if let Some(path) = package_path.to_system_path() {
let system = context.db.system();
if system.case_sensitivity().is_case_sensitive()
|| system.path_exists_case_sensitive(
&path,
package_path.search_path().as_system_path().unwrap(),
)
{
return Ok((
resolved_package.kind,
resolved_package.typed,
ResolvedName::NamespacePackage,
));
}
}
}
Err((resolved_package.kind, resolved_package.typed))
}
/// If `module` exists on disk with either a `.pyi` or `.py` extension,
/// return the [`File`] corresponding to that path.
///
@@ -1366,90 +1482,6 @@ pub(super) fn resolve_file_module(
Some(file)
}
/// Attempt to resolve the parent package of a module.
///
/// `module_search_path` should be the directory to start looking for the
/// parent package.
///
/// `components` should be the full module name of the parent package. This
/// specifically should not include the basename of the module. So e.g.,
/// for `foo.bar.baz`, `components` should be `[foo, bar]`. It follows that
/// `components` may be empty (in which case, the parent package is the root).
///
/// Upon success, the path to the package and its "kind" (root, regular or
/// namespace) is returned. Upon error, the kind of the package is still
/// returned based on how many components were found and whether `__init__.py`
/// is present.
fn resolve_package<'a, 'db, I>(
module_search_path: &SearchPath,
components: I,
resolver_state: &ResolverContext<'db>,
) -> Result<ResolvedPackage, (PackageKind, PyTyped)>
where
I: Iterator<Item = &'a str>,
{
let mut package_path = module_search_path.to_module_path();
// `true` if inside a folder that is a namespace package (has no `__init__.py`).
// Namespace packages are special because they can be spread across multiple search paths.
// https://peps.python.org/pep-0420/
let mut in_namespace_package = false;
// `true` if resolving a sub-package. For example, `true` when resolving `bar` of `foo.bar`.
let mut in_sub_package = false;
let mut typed = package_path.py_typed(resolver_state);
// For `foo.bar.baz`, test that `foo` and `bar` both contain a `__init__.py`.
for folder in components {
package_path.push(folder);
typed = package_path.py_typed(resolver_state).inherit_parent(typed);
let is_regular_package = package_path.is_regular_package(resolver_state);
if is_regular_package {
// This is the only place where we need to consider the existence of legacy namespace
// packages, as we are explicitly searching for the *parent* package of the module
// we actually want. Here, such a package should be treated as a PEP-420 ("modern")
// namespace package. In all other contexts it acts like a normal package and needs
// no special handling.
in_namespace_package = is_legacy_namespace_package(&package_path, resolver_state);
} else if package_path.is_directory(resolver_state)
// Pure modules hide namespace packages with the same name
&& resolve_file_module(&package_path, resolver_state).is_none()
{
// A directory without an `__init__.py(i)` is a namespace package,
// continue with the next folder.
in_namespace_package = true;
} else if in_namespace_package {
// Package not found but it is part of a namespace package.
return Err((PackageKind::Namespace, typed));
} else if in_sub_package {
// A regular sub package wasn't found.
return Err((PackageKind::Regular, typed));
} else {
// We couldn't find `foo` for `foo.bar.baz`, search the next search path.
return Err((PackageKind::Root, typed));
}
in_sub_package = true;
}
let kind = if in_namespace_package {
PackageKind::Namespace
} else if in_sub_package {
PackageKind::Regular
} else {
PackageKind::Root
};
Ok(ResolvedPackage {
kind,
path: package_path,
typed,
})
}
/// Determines whether a package is a legacy namespace package.
///
/// Before PEP 420 introduced implicit namespace packages, the ecosystem developed
@@ -1479,19 +1511,14 @@ where
/// we will just get confused if you mess it up).
fn is_legacy_namespace_package(
package_path: &ModulePath,
resolver_state: &ResolverContext,
context: &ResolverContext,
init: File,
) -> bool {
// Just an optimization, the stdlib and typeshed are never legacy namespace packages
if package_path.search_path().is_standard_library() {
return false;
}
let mut package_path = package_path.clone();
package_path.push("__init__");
let Some(init) = resolve_file_module(&package_path, resolver_state) else {
return false;
};
// This is all syntax-only analysis so it *could* be fooled but it's really unlikely.
//
// The benefit of being syntax-only is speed and avoiding circular dependencies
@@ -1499,44 +1526,13 @@ fn is_legacy_namespace_package(
//
// The downside is if you write slightly different syntax we will fail to detect the idiom,
// but hey, this is better than nothing!
let parsed = ruff_db::parsed::parsed_module(resolver_state.db, init);
let parsed = ruff_db::parsed::parsed_module(context.db, init);
let mut visitor = LegacyNamespacePackageVisitor::default();
visitor.visit_body(parsed.load(resolver_state.db).suite());
visitor.visit_body(parsed.load(context.db).suite());
visitor.is_legacy_namespace_package
}
#[derive(Debug)]
struct ResolvedPackage {
path: ModulePath,
kind: PackageKind,
typed: PyTyped,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum PackageKind {
/// A root package or module. E.g. `foo` in `foo.bar.baz` or just `foo`.
Root,
/// A regular sub-package where the parent contains an `__init__.py`.
///
/// For example, `bar` in `foo.bar` when the `foo` directory contains an `__init__.py`.
Regular,
/// A sub-package in a namespace package. A namespace package is a package
/// without an `__init__.py`.
///
/// For example, `bar` in `foo.bar` if the `foo` directory contains no
/// `__init__.py`.
Namespace,
}
impl PackageKind {
pub(crate) const fn is_root(self) -> bool {
matches!(self, PackageKind::Root)
}
}
/// Info about the `py.typed` file for this package
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub(crate) enum PyTyped {
@@ -1587,34 +1583,6 @@ impl<'db> ResolverContext<'db> {
}
}
/// A [`ModuleName`] but with relaxed semantics to allow `<package>-stubs.path`
#[derive(Debug)]
struct RelaxedModuleName(compact_str::CompactString);
impl RelaxedModuleName {
fn new(name: &ModuleName) -> Self {
Self(name.as_str().into())
}
fn components(&self) -> Split<'_, char> {
self.0.split('.')
}
fn to_stub_package(&self) -> Self {
if let Some((package, rest)) = self.0.split_once('.') {
Self(format_compact!("{package}-stubs.{rest}"))
} else {
Self(format_compact!("{package}-stubs", package = self.0))
}
}
}
impl fmt::Display for RelaxedModuleName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// Detects if a module contains a statement of the form:
/// ```python
/// __path__ = pkgutil.extend_path(__path__, __name__)
@@ -1926,14 +1894,12 @@ mod tests {
asyncio: 3.8- # 'Regular' package on py38+
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
functools: 3.8- # Top-level single-file module
xml: 3.8-3.8 # Namespace package on py38 only
";
const STDLIB: &[FileSpec] = &[
("asyncio/__init__.pyi", ""),
("asyncio/tasks.pyi", ""),
("functools.pyi", ""),
("xml/etree.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
@@ -1946,7 +1912,7 @@ mod tests {
.with_python_version(PythonVersion::PY38)
.build();
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
let existing_modules = create_module_names(&["asyncio", "functools"]);
for module_name in existing_modules {
let resolved_module =
resolve_module_confident(&db, &module_name).unwrap_or_else(|| {
@@ -1970,16 +1936,12 @@ mod tests {
asyncio: 3.8- # 'Regular' package on py38+
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
collections: 3.9- # 'Regular' package on py39+
importlib: 3.9- # Namespace package on py39+
xml: 3.8-3.8 # Namespace package on 3.8 only
";
const STDLIB: &[FileSpec] = &[
("collections/__init__.pyi", ""),
("asyncio/__init__.pyi", ""),
("asyncio/tasks.pyi", ""),
("importlib/abc.pyi", ""),
("xml/etree.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
@@ -1992,13 +1954,7 @@ mod tests {
.with_python_version(PythonVersion::PY38)
.build();
let nonexisting_modules = create_module_names(&[
"collections",
"importlib",
"importlib.abc",
"xml",
"asyncio.tasks",
]);
let nonexisting_modules = create_module_names(&["collections", "asyncio.tasks"]);
for module_name in nonexisting_modules {
assert!(
@@ -2015,7 +1971,6 @@ mod tests {
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
collections: 3.9- # 'Regular' package on py39+
functools: 3.8- # Top-level single-file module
importlib: 3.9- # Namespace package on py39+
";
const STDLIB: &[FileSpec] = &[
@@ -2023,7 +1978,6 @@ mod tests {
("asyncio/tasks.pyi", ""),
("collections/__init__.pyi", ""),
("functools.pyi", ""),
("importlib/abc.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
@@ -2036,13 +1990,8 @@ mod tests {
.with_python_version(PythonVersion::PY39)
.build();
let existing_modules = create_module_names(&[
"asyncio",
"functools",
"importlib.abc",
"collections",
"asyncio.tasks",
]);
let existing_modules =
create_module_names(&["asyncio", "functools", "collections", "asyncio.tasks"]);
for module_name in existing_modules {
let resolved_module =
@@ -2444,7 +2393,7 @@ mod tests {
fn adding_file_to_search_path_with_lower_priority_does_not_invalidate_query() {
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: "functools: 3.8-",
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
stdlib_files: &[("functools/__init__.pyi", "def update_wrapper(): ...")],
};
let TestCase {
@@ -2458,7 +2407,7 @@ mod tests {
.build();
let functools_module_name = ModuleName::new_static("functools").unwrap();
let stdlib_functools_path = stdlib.join("functools.pyi");
let stdlib_functools_path = stdlib.join("functools/__init__.pyi");
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
assert_eq!(functools_module.search_path(&db).unwrap(), &stdlib);
@@ -2470,7 +2419,7 @@ mod tests {
// Adding a file to site-packages does not invalidate the query,
// since site-packages takes lower priority in the module resolution
db.clear_salsa_events();
let site_packages_functools_path = site_packages.join("functools.py");
let site_packages_functools_path = site_packages.join("functools/__init__.py");
db.write_file(&site_packages_functools_path, "f: int")
.unwrap();
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();

View File

@@ -341,11 +341,11 @@ class Impl:
```py
from foo.bar.both import Both
from foo.bar.impl import Impl
from foo.bar.fake import Fake # error: "Cannot resolve"
from foo.bar.impl import Impl # error: [unresolved-import]
from foo.bar.fake import Fake # error: [unresolved-import]
reveal_type(Both().both) # revealed: str
reveal_type(Impl().impl) # revealed: str
reveal_type(Impl().impl) # revealed: Unknown
reveal_type(Fake().fake) # revealed: Unknown
```

View File

@@ -191,8 +191,7 @@ reveal_type(Hexagon().area) # revealed: Unknown
The runtime package is a regular package but the stubs are namespace packages. Pyright skips the
stub package if the "regular" package isn't a namespace package. I'm not aware that the behavior
here is specified, and using the stubs without probing the runtime package first requires slightly
fewer lookups.
here is specified, but we currently agree with pyright here.
```toml
[environment]
@@ -202,17 +201,13 @@ extra-paths = ["/packages"]
`/packages/shapes-stubs/polygons/pentagon.pyi`:
```pyi
class Pentagon:
sides: int
area: float
class Pentagon: ...
```
`/packages/shapes-stubs/polygons/hexagon.pyi`:
```pyi
class Hexagon:
sides: int
area: float
class Hexagon: ...
```
`/packages/shapes/__init__.py`:
@@ -228,13 +223,17 @@ class Hexagon:
`/packages/shapes/polygons/pentagon.py`:
```py
class Pentagon: ...
class Pentagon:
sides: int
area: float
```
`/packages/shapes/polygons/hexagon.py`:
```py
class Hexagon: ...
class Hexagon:
sides: int
area: float
```
`main.py`: