diff --git a/crates/ty_python_semantic/resources/mdtest/import/basic.md b/crates/ty_python_semantic/resources/mdtest/import/basic.md index 8e7538190ef06a..45abcf50185333 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/import/basic.md @@ -205,3 +205,18 @@ python-version = "3.13" import aifc # error: [unresolved-import] from distutils import sysconfig # error: [unresolved-import] ``` + +## Cannot shadow core standard library modules + +`types.py`: + +```py +x: int +``` + +```py +# error: [unresolved-import] +from types import x + +from types import FunctionType +``` diff --git a/crates/ty_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs index af4acc966f44c6..be6f40c2465f2a 100644 --- a/crates/ty_python_semantic/src/module_resolver/resolver.rs +++ b/crates/ty_python_semantic/src/module_resolver/resolver.rs @@ -535,14 +535,23 @@ struct ModuleNameIngredient<'db> { pub(super) name: ModuleName, } +/// Returns `true` if the module name refers to a standard library module which can't be shadowed +/// by a first-party module. +/// +/// This includes "builtin" modules, which can never be shadowed at runtime either, as well as the +/// `types` module, which tends to be imported early in Python startup, so can't be consistently +/// shadowed, and is important to type checking. +fn is_non_shadowable(minor_version: u8, module_name: &str) -> bool { + module_name == "types" || ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name) +} + /// Given a module name and a list of search paths in which to lookup modules, /// attempt to resolve the module name fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option { let program = Program::get(db); let python_version = program.python_version(db); let resolver_state = ResolverContext::new(db, python_version); - let is_builtin_module = - ruff_python_stdlib::sys::is_builtin_module(python_version.minor, name.as_str()); + let is_non_shadowable = is_non_shadowable(python_version.minor, name.as_str()); let name = RelaxedModuleName::new(name); let stub_name = name.to_stub_package(); @@ -553,7 +562,8 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option { // 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). - if is_builtin_module && !search_path.is_standard_library() { + // 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; } diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs index 971b4cdc79dc49..799e76dd11ab4c 100644 --- a/crates/ty_python_semantic/tests/corpus.rs +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -116,13 +116,6 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { .with_context(|| format!("Failed to read test file: {path}"))?; let mut check_with_file_name = |path: &SystemPath| { - if relative_path.file_name() == Some("types.pyi") { - println!( - "Skipping {relative_path:?}: paths with `types.pyi` as their final segment cause a stack overflow" - ); - return; - } - db.memory_file_system().write_file_all(path, &code).unwrap(); File::sync_path(&mut db, path);