Add support for namespace packages (#1859)

Closes #1817.
This commit is contained in:
Charlie Marsh 2023-01-14 07:31:57 -05:00 committed by GitHub
parent 931d41bff1
commit 027382f891
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 99 additions and 12 deletions

View File

@ -2154,6 +2154,25 @@ line-length = 120
--- ---
#### [`namespace-packages`](#namespace-packages)
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.
**Default value**: `[]`
**Type**: `Vec<PathBuf>`
**Example usage**:
```toml
[tool.ruff]
namespace-packages = ["airflow/providers"]
```
---
#### [`per-file-ignores`](#per-file-ignores) #### [`per-file-ignores`](#per-file-ignores)
A list of mappings from file pattern to rule codes or prefixes to A list of mappings from file pattern to rule codes or prefixes to

View File

@ -423,6 +423,7 @@ mod tests {
ignore: Some(vec![]), ignore: Some(vec![]),
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -488,6 +489,7 @@ mod tests {
ignore: Some(vec![]), ignore: Some(vec![]),
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: Some(100), line_length: Some(100),
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -553,6 +555,7 @@ mod tests {
ignore: Some(vec![]), ignore: Some(vec![]),
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: Some(100), line_length: Some(100),
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -618,6 +621,7 @@ mod tests {
ignore: Some(vec![]), ignore: Some(vec![]),
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -683,6 +687,7 @@ mod tests {
ignore: Some(vec![]), ignore: Some(vec![]),
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -756,6 +761,7 @@ mod tests {
ignore: Some(vec![]), ignore: Some(vec![]),
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -824,6 +830,7 @@ mod tests {
ignore: Some(vec![]), ignore: Some(vec![]),
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,

View File

@ -285,6 +285,16 @@
} }
] ]
}, },
"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.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"pep8-naming": { "pep8-naming": {
"description": "Options for the `pep8-naming` plugin.", "description": "Options for the `pep8-naming` plugin.",
"anyOf": [ "anyOf": [

View File

@ -83,6 +83,8 @@ pub fn run(
.flatten() .flatten()
.map(ignore::DirEntry::path) .map(ignore::DirEntry::path)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
&resolver,
pyproject_strategy,
); );
let start = Instant::now(); let start = Instant::now();
@ -169,7 +171,7 @@ pub fn run_stdin(
}; };
let package_root = filename let package_root = filename
.and_then(Path::parent) .and_then(Path::parent)
.and_then(packaging::detect_package_root); .and_then(|path| packaging::detect_package_root(path, &settings.namespace_packages));
let stdin = read_from_stdin()?; let stdin = read_from_stdin()?;
let mut diagnostics = lint_stdin(filename, package_root, &stdin, settings, autofix)?; let mut diagnostics = lint_stdin(filename, package_root, &stdin, settings, autofix)?;
diagnostics.messages.sort_unstable(); diagnostics.messages.sort_unstable();

View File

@ -51,7 +51,7 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Diagnosti
// Generate diagnostics. // Generate diagnostics.
let diagnostics = check_path( let diagnostics = check_path(
path, path,
packaging::detect_package_root(path), packaging::detect_package_root(path, &settings.namespace_packages),
contents, contents,
tokens, tokens,
&locator, &locator,

View File

@ -105,14 +105,15 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
force_exclude: None, force_exclude: None,
format: None, format: None,
ignore_init_module_imports: None, ignore_init_module_imports: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
show_source: None, show_source: None,
src: None, src: None,
unfixable: None,
typing_modules: None,
task_tags: None, task_tags: None,
typing_modules: None,
unfixable: None,
update_check: None, update_check: None,
// Use default options for all plugins. // Use default options for all plugins.
flake8_annotations: Some(flake8_annotations::settings::Settings::default().into()), flake8_annotations: Some(flake8_annotations::settings::Settings::default().into()),

View File

@ -1,9 +1,11 @@
//! Detect Python package roots and file associations. //! Detect Python package roots and file associations.
use std::path::Path; use std::path::{Path, PathBuf};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use crate::resolver::{PyprojectDiscovery, Resolver};
// If we have a Python package layout like: // If we have a Python package layout like:
// - root/ // - root/
// - foo/ // - foo/
@ -27,15 +29,21 @@ use rustc_hash::FxHashMap;
/// Return `true` if the directory at the given `Path` appears to be a Python /// Return `true` if the directory at the given `Path` appears to be a Python
/// package. /// package.
pub fn is_package(path: &Path) -> bool { pub fn is_package(path: &Path, namespace_packages: &[PathBuf]) -> bool {
path.join("__init__.py").is_file() path.join("__init__.py").is_file()
|| namespace_packages
.iter()
.any(|namespace_package| namespace_package == path)
} }
/// Return the package root for the given Python file. /// Return the package root for the given Python file.
pub fn detect_package_root(path: &Path) -> Option<&Path> { pub fn detect_package_root<'a>(
path: &'a Path,
namespace_packages: &'a [PathBuf],
) -> Option<&'a Path> {
let mut current = None; let mut current = None;
for parent in path.ancestors() { for parent in path.ancestors() {
if !is_package(parent) { if !is_package(parent, namespace_packages) {
return current; return current;
} }
current = Some(parent); current = Some(parent);
@ -46,21 +54,23 @@ pub fn detect_package_root(path: &Path) -> Option<&Path> {
/// A wrapper around `is_package` to cache filesystem lookups. /// A wrapper around `is_package` to cache filesystem lookups.
fn is_package_with_cache<'a>( fn is_package_with_cache<'a>(
path: &'a Path, path: &'a Path,
namespace_packages: &'a [PathBuf],
package_cache: &mut FxHashMap<&'a Path, bool>, package_cache: &mut FxHashMap<&'a Path, bool>,
) -> bool { ) -> bool {
*package_cache *package_cache
.entry(path) .entry(path)
.or_insert_with(|| is_package(path)) .or_insert_with(|| is_package(path, namespace_packages))
} }
/// A wrapper around `detect_package_root` to cache filesystem lookups. /// A wrapper around `detect_package_root` to cache filesystem lookups.
fn detect_package_root_with_cache<'a>( fn detect_package_root_with_cache<'a>(
path: &'a Path, path: &'a Path,
namespace_packages: &'a [PathBuf],
package_cache: &mut FxHashMap<&'a Path, bool>, package_cache: &mut FxHashMap<&'a Path, bool>,
) -> Option<&'a Path> { ) -> Option<&'a Path> {
let mut current = None; let mut current = None;
for parent in path.ancestors() { for parent in path.ancestors() {
if !is_package_with_cache(parent, package_cache) { if !is_package_with_cache(parent, namespace_packages, package_cache) {
return current; return current;
} }
current = Some(parent); current = Some(parent);
@ -69,7 +79,11 @@ fn detect_package_root_with_cache<'a>(
} }
/// Return a mapping from Python file to its package root. /// Return a mapping from Python file to its package root.
pub fn detect_package_roots<'a>(files: &[&'a Path]) -> FxHashMap<&'a Path, Option<&'a Path>> { pub fn detect_package_roots<'a>(
files: &[&'a Path],
resolver: &'a Resolver,
pyproject_strategy: &'a PyprojectDiscovery,
) -> FxHashMap<&'a Path, Option<&'a Path>> {
// Pre-populate the module cache, since the list of files could (but isn't // Pre-populate the module cache, since the list of files could (but isn't
// required to) contain some `__init__.py` files. // required to) contain some `__init__.py` files.
let mut package_cache: FxHashMap<&Path, bool> = FxHashMap::default(); let mut package_cache: FxHashMap<&Path, bool> = FxHashMap::default();
@ -84,13 +98,16 @@ pub fn detect_package_roots<'a>(files: &[&'a Path]) -> FxHashMap<&'a Path, Optio
// Search for the package root for each file. // Search for the package root for each file.
let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default(); let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default();
for file in files { for file in files {
let namespace_packages = &resolver
.resolve(file, pyproject_strategy)
.namespace_packages;
if let Some(package) = file.parent() { if let Some(package) = file.parent() {
if package_roots.contains_key(package) { if package_roots.contains_key(package) {
continue; continue;
} }
package_roots.insert( package_roots.insert(
package, package,
detect_package_root_with_cache(package, &mut package_cache), detect_package_root_with_cache(package, namespace_packages, &mut package_cache),
); );
} }
} }
@ -111,6 +128,7 @@ mod tests {
PathBuf::from(env!("CARGO_MANIFEST_DIR")) PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/package/src/package") .join("resources/test/package/src/package")
.as_path(), .as_path(),
&[],
), ),
Some( Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR")) PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@ -124,6 +142,7 @@ mod tests {
PathBuf::from(env!("CARGO_MANIFEST_DIR")) PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/project/python_modules/core/core") .join("resources/test/project/python_modules/core/core")
.as_path(), .as_path(),
&[],
), ),
Some( Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR")) PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@ -137,6 +156,7 @@ mod tests {
PathBuf::from(env!("CARGO_MANIFEST_DIR")) PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/project/examples/docs/docs/concepts") .join("resources/test/project/examples/docs/docs/concepts")
.as_path(), .as_path(),
&[],
), ),
Some( Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR")) PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@ -150,6 +170,7 @@ mod tests {
PathBuf::from(env!("CARGO_MANIFEST_DIR")) PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("setup.py") .join("setup.py")
.as_path(), .as_path(),
&[],
), ),
None, None,
); );

View File

@ -44,6 +44,7 @@ pub struct Configuration {
pub ignore: Option<Vec<RuleCodePrefix>>, pub ignore: Option<Vec<RuleCodePrefix>>,
pub ignore_init_module_imports: Option<bool>, pub ignore_init_module_imports: Option<bool>,
pub line_length: Option<usize>, pub line_length: Option<usize>,
pub namespace_packages: Option<Vec<PathBuf>>,
pub per_file_ignores: Option<Vec<PerFileIgnore>>, pub per_file_ignores: Option<Vec<PerFileIgnore>>,
pub required_version: Option<Version>, pub required_version: Option<Version>,
pub respect_gitignore: Option<bool>, pub respect_gitignore: Option<bool>,
@ -135,6 +136,10 @@ impl Configuration {
ignore: options.ignore, ignore: options.ignore,
ignore_init_module_imports: options.ignore_init_module_imports, ignore_init_module_imports: options.ignore_init_module_imports,
line_length: options.line_length, line_length: options.line_length,
namespace_packages: options
.namespace_packages
.map(|namespace_package| resolve_src(&namespace_package, project_root))
.transpose()?,
per_file_ignores: options.per_file_ignores.map(|per_file_ignores| { per_file_ignores: options.per_file_ignores.map(|per_file_ignores| {
per_file_ignores per_file_ignores
.into_iter() .into_iter()
@ -211,6 +216,7 @@ impl Configuration {
.ignore_init_module_imports .ignore_init_module_imports
.or(config.ignore_init_module_imports), .or(config.ignore_init_module_imports),
line_length: self.line_length.or(config.line_length), line_length: self.line_length.or(config.line_length),
namespace_packages: self.namespace_packages.or(config.namespace_packages),
per_file_ignores: self.per_file_ignores.or(config.per_file_ignores), per_file_ignores: self.per_file_ignores.or(config.per_file_ignores),
required_version: self.required_version.or(config.required_version), required_version: self.required_version.or(config.required_version),
respect_gitignore: self.respect_gitignore.or(config.respect_gitignore), respect_gitignore: self.respect_gitignore.or(config.respect_gitignore),

View File

@ -55,6 +55,7 @@ pub struct Settings {
pub format: SerializationFormat, pub format: SerializationFormat,
pub ignore_init_module_imports: bool, pub ignore_init_module_imports: bool,
pub line_length: usize, pub line_length: usize,
pub namespace_packages: Vec<PathBuf>,
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet<RuleCode>)>, pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet<RuleCode>)>,
pub required_version: Option<Version>, pub required_version: Option<Version>,
pub respect_gitignore: bool, pub respect_gitignore: bool,
@ -169,6 +170,7 @@ impl Settings {
force_exclude: config.force_exclude.unwrap_or(false), force_exclude: config.force_exclude.unwrap_or(false),
ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(), ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(),
line_length: config.line_length.unwrap_or(88), line_length: config.line_length.unwrap_or(88),
namespace_packages: config.namespace_packages.unwrap_or_default(),
per_file_ignores: resolve_per_file_ignores( per_file_ignores: resolve_per_file_ignores(
config.per_file_ignores.unwrap_or_default(), config.per_file_ignores.unwrap_or_default(),
)?, )?,
@ -235,6 +237,7 @@ impl Settings {
format: SerializationFormat::Text, format: SerializationFormat::Text,
ignore_init_module_imports: false, ignore_init_module_imports: false,
line_length: 88, line_length: 88,
namespace_packages: vec![],
per_file_ignores: vec![], per_file_ignores: vec![],
required_version: None, required_version: None,
respect_gitignore: true, respect_gitignore: true,
@ -279,6 +282,7 @@ impl Settings {
format: SerializationFormat::Text, format: SerializationFormat::Text,
ignore_init_module_imports: false, ignore_init_module_imports: false,
line_length: 88, line_length: 88,
namespace_packages: vec![],
per_file_ignores: vec![], per_file_ignores: vec![],
required_version: None, required_version: None,
respect_gitignore: true, respect_gitignore: true,

View File

@ -352,6 +352,17 @@ pub struct Options {
/// packages in that directory. User home directory and environment /// packages in that directory. User home directory and environment
/// variables will also be expanded. /// variables will also be expanded.
pub src: Option<Vec<String>>, pub src: Option<Vec<String>>,
#[option(
default = r#"[]"#,
value_type = "Vec<PathBuf>",
example = r#"
namespace-packages = ["airflow/providers"]
"#
)]
/// 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.
pub namespace_packages: Option<Vec<String>>,
#[option( #[option(
default = r#""py310""#, default = r#""py310""#,
value_type = "PythonVersion", value_type = "PythonVersion",

View File

@ -181,6 +181,7 @@ mod tests {
ignore: None, ignore: None,
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -239,6 +240,7 @@ line-length = 79
ignore: None, ignore: None,
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: Some(79), line_length: Some(79),
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
respect_gitignore: None, respect_gitignore: None,
required_version: None, required_version: None,
@ -299,6 +301,7 @@ exclude = ["foo.py"]
ignore: None, ignore: None,
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -358,6 +361,7 @@ select = ["E501"]
ignore: None, ignore: None,
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -418,6 +422,7 @@ ignore = ["E501"]
ignore: Some(vec![RuleCodePrefix::E501]), ignore: Some(vec![RuleCodePrefix::E501]),
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
namespace_packages: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None, required_version: None,
respect_gitignore: None, respect_gitignore: None,
@ -515,6 +520,7 @@ other-attribute = 1
fixable: None, fixable: None,
format: None, format: None,
force_exclude: None, force_exclude: None,
namespace_packages: None,
unfixable: None, unfixable: None,
typing_modules: None, typing_modules: None,
task_tags: None, task_tags: None,