Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions pyrefly/lib/module/finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ impl FindResult {
/// are not compared.
fn best_result(a: FindResult, b: FindResult) -> Self {
match (&a, &b) {
// A concrete single-file module or compiled module from an earlier root beats any
// package from a later root. Python's sys.path semantics are first-match-wins
// regardless of whether the match is a file or a package directory.
(
FindResult::SingleFilePyModule(_) | FindResult::CompiledModule(_),
FindResult::RegularPackage(..) | FindResult::LegacyNamespacePackage(..),
) => a,
// RegularPackage and LegacyNamespacePackage (LNP) share the top tier: both carry a
// concrete `__init__.py` and resolve to `FileSystem(init_path)`. Tying them lets
// the prefer-`a` approach keep sys.path order ("first concrete-init wins") when
Expand Down Expand Up @@ -5378,4 +5385,49 @@ mod tests {
FindingOrError::new_finding(ModulePath::filesystem(root.join("rules/if.config.cconf")))
);
}

#[test]
fn test_single_file_in_earlier_root_beats_package_in_later_root() {
// Python's sys.path semantics are first-match-wins regardless of
// whether the match is a .py file or a package directory. When root0
// has `widget.py` and root1 has `widget/__init__.py`, resolving `widget`
// should return root0's `widget.py`.
let tempdir = tempfile::tempdir().unwrap();
let root = tempdir.path();
TestPath::setup_test_directory(
root,
vec![
TestPath::dir(
"search_root0",
vec![TestPath::file_with_contents(
"widget.py",
"class WidgetHandler: ...",
)],
),
TestPath::dir(
"search_root1",
vec![TestPath::dir("widget", vec![TestPath::file("__init__.py")])],
),
],
);
let roots = [root.join("search_root0"), root.join("search_root1")];

assert_eq!(
find_module(
ModuleName::from_str("widget"),
roots.iter(),
&mut vec![],
None,
None,
false,
&mut None,
&DirEntryCache::new(true),
None,
)
.unwrap(),
FindingOrError::new_finding(ModulePath::filesystem(
root.join("search_root0/widget.py")
))
);
}
}
31 changes: 31 additions & 0 deletions pyrefly/lib/test/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1766,6 +1766,37 @@ fn test_pkgutil_namespace_absorbs_implicit_namespace() {
.unwrap();
}

// ----------------------------------------------------------------------------
// Search-path ordering regression.
// ----------------------------------------------------------------------------

#[test]
fn test_search_path_module_beats_later_package() {
// A .py module in search_path[0] must beat a package directory in
// search_path[1]. Python's sys.path semantics are first-match-wins
// regardless of whether the match is a .py file or a package.
let tempdir = tempfile::tempdir().unwrap();
let root = tempdir.path();
std::fs::write(root.join("widget.py"), "class WidgetHandler: ...\n").unwrap();
std::fs::create_dir(root.join("widget_pkg")).unwrap();
std::fs::create_dir(root.join("widget_pkg/widget")).unwrap();
std::fs::write(root.join("widget_pkg/widget/__init__.py"), "").unwrap();

let mut env = TestEnv::new().with_site_package_paths(vec![
root.to_path_buf(), // root/widget.py — should win
root.join("widget_pkg"), // root/widget_pkg/widget/__init__.py — should lose
]);
// No error expected: widget.py from the first search path should be
// resolved, making WidgetHandler importable.
env.add_with_path("main", "main.py", "from widget import WidgetHandler\n");
let (state, handle_fn) = env.to_state();
state
.transaction()
.get_errors(&[handle_fn("main")])
.check_against_expectations()
.unwrap();
}

// ----------------------------------------------------------------------------
// Cross-module class rebind tests: importers should observe whichever class
// the visible result chose. See `assign.rs` for the same-module regressions.
Expand Down
Loading