diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a7b667a0080e..7b792a0537ca0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -813,8 +813,8 @@ To understand Ruff's import categorization system, we first need to define two c "project root".) - "Package root": The top-most directory defining the Python package that includes a given Python file. To find the package root for a given Python file, traverse up its parent directories until - you reach a parent directory that doesn't contain an `__init__.py` file (and isn't marked as - a [namespace package](https://docs.astral.sh/ruff/settings/#namespace-packages)); take the directory + you reach a parent directory that doesn't contain an `__init__.py` file (and isn't in a subtree + marked as a [namespace package](https://docs.astral.sh/ruff/settings/#namespace-packages)); take the directory just before that, i.e., the first directory in the package. For example, given: diff --git a/crates/ruff_linter/src/packaging.rs b/crates/ruff_linter/src/packaging.rs index 70fe6ab1c21e0..2d23da0bdfe78 100644 --- a/crates/ruff_linter/src/packaging.rs +++ b/crates/ruff_linter/src/packaging.rs @@ -26,17 +26,14 @@ use std::path::{Path, PathBuf}; /// Return `true` if the directory at the given `Path` appears to be a Python /// package. pub fn is_package(path: &Path, namespace_packages: &[PathBuf]) -> bool { - path.join("__init__.py").is_file() - || namespace_packages - .iter() - .any(|namespace_package| namespace_package == path) + namespace_packages + .iter() + .any(|namespace_package| path.starts_with(namespace_package)) + || path.join("__init__.py").is_file() } -/// Return the package root for the given Python file. -pub fn detect_package_root<'a>( - path: &'a Path, - namespace_packages: &'a [PathBuf], -) -> Option<&'a Path> { +/// Return the package root for the given path to a directory with Python file. +pub fn detect_package_root<'a>(path: &'a Path, namespace_packages: &[PathBuf]) -> Option<&'a Path> { let mut current = None; for parent in path.ancestors() { if !is_package(parent, namespace_packages) { @@ -84,4 +81,39 @@ mod tests { None, ); } + + #[test] + fn package_detection_with_namespace_packages() { + assert_eq!( + detect_package_root(&test_resource_path("project/python_modules/core/core"), &[],), + Some(test_resource_path("project/python_modules/core/core").as_path()) + ); + + assert_eq!( + detect_package_root( + &test_resource_path("project/python_modules/core/core"), + &[test_resource_path("project/python_modules/core"),], + ), + Some(test_resource_path("project/python_modules/core").as_path()) + ); + + assert_eq!( + detect_package_root( + &test_resource_path("project/python_modules/core/core"), + &[ + test_resource_path("project/python_modules/core"), + test_resource_path("project/python_modules"), + ], + ), + Some(test_resource_path("project/python_modules").as_path()) + ); + + assert_eq!( + detect_package_root( + &test_resource_path("project/python_modules/core/core"), + &[test_resource_path("project/python_modules"),], + ), + Some(test_resource_path("project/python_modules").as_path()) + ); + } } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 6e673baa16a76..6fca0e887cd70 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -293,8 +293,8 @@ pub struct Options { pub builtins: Option>, /// Mark the specified directories as namespace packages. For the purpose of - /// module resolution, Ruff will treat those directories as if they - /// contained an `__init__.py` file. + /// module resolution, Ruff will treat those directories and all their subdirectories + /// as if they contained an `__init__.py` file. #[option( default = r#"[]"#, value_type = "list[str]", diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 3db1f65f95db1..e9a5f622d1968 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -210,7 +210,7 @@ impl<'a> Resolver<'a> { /// A wrapper around `detect_package_root` to cache filesystem lookups. fn detect_package_root_with_cache<'a>( path: &'a Path, - namespace_packages: &'a [PathBuf], + namespace_packages: &[PathBuf], package_cache: &mut FxHashMap<&'a Path, bool>, ) -> Option<&'a Path> { let mut current = None; @@ -226,7 +226,7 @@ fn detect_package_root_with_cache<'a>( /// A wrapper around `is_package` to cache filesystem lookups. fn is_package_with_cache<'a>( path: &'a Path, - namespace_packages: &'a [PathBuf], + namespace_packages: &[PathBuf], package_cache: &mut FxHashMap<&'a Path, bool>, ) -> bool { *package_cache diff --git a/ruff.schema.json b/ruff.schema.json index 0d5ae5a283eae..97ea986381351 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -509,7 +509,7 @@ ] }, "namespace-packages": { - "description": "Mark the specified directories as namespace packages. For the purpose of module resolution, Ruff will treat those directories as if they contained an `__init__.py` file.", + "description": "Mark the specified directories as namespace packages. For the purpose of module resolution, Ruff will treat those directories and all their subdirectories as if they contained an `__init__.py` file.", "type": [ "array", "null"