Files
ruff/crates/ruff_linter/src/packaging.rs
Alexey Preobrazhenskiy d625f55c05 Nested namespace packages support (#10541)
## Summary
PEP 420 says [nested namespace
packages](https://peps.python.org/pep-0420/#nested-namespace-packages)
are allowed, i.e. marking a directory as a namespace package marks all
subdirectories in the subtree as namespace packages.

`is_package` is modified to use `Path::starts_with` and the order of
checks is reversed to do in-memory checks first before hitting the disk.

## Test Plan
Added unit tests. Previously all tests were run with `namespace_packages
== &[]`. Verified that one of the tests was failing before changing the
implementation.

## Future Improvements
The `is_package_with_cache` can probably be rewritten to avoid repeated
calls to `Path::starts_with`, by caching all directories up to the
`namespace_root`:
```ruff
let namespace_root = namespace_packages
    .iter()
    .filter(|namespace_package| path.starts_with(namespace_package))
    .min();
```
2024-03-24 22:53:32 -04:00

120 lines
3.8 KiB
Rust

//! Detect Python package roots and file associations.
use std::path::{Path, PathBuf};
// If we have a Python package layout like:
// - root/
// - foo/
// - __init__.py
// - bar.py
// - baz/
// - __init__.py
// - qux.py
//
// Then today, if you run with defaults (`src = ["."]`) from `root`, we'll
// detect that `foo.bar`, `foo.baz`, and `foo.baz.qux` are first-party modules
// (since, if you're in `root`, you can see `foo`).
//
// However, we'd also like it to be the case that, even if you run this command
// from `foo`, we still consider `foo.baz.qux` to be first-party when linting
// `foo/bar.py`. More specifically, for each Python file, we should find the
// root of the current package.
//
// Thus, for each file, we iterate up its ancestors, returning the last
// directory containing an `__init__.py`.
/// 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 {
namespace_packages
.iter()
.any(|namespace_package| path.starts_with(namespace_package))
|| path.join("__init__.py").is_file()
}
/// 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) {
return current;
}
current = Some(parent);
}
current
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::packaging::detect_package_root;
use crate::test::test_resource_path;
#[test]
fn package_detection() {
assert_eq!(
detect_package_root(&test_resource_path("package/src/package"), &[],),
Some(test_resource_path("package/src/package").as_path())
);
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/examples/docs/docs/concepts"),
&[],
),
Some(test_resource_path("project/examples/docs/docs").as_path())
);
assert_eq!(
detect_package_root(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("setup.py")
.as_path(),
&[],
),
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())
);
}
}