Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for namespace packages #1859

Merged
merged 1 commit into from
Jan 14, 2023
Merged
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

A list of mappings from file pattern to rule codes or prefixes to
Expand Down
7 changes: 7 additions & 0 deletions flake8_to_ruff/src/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -488,6 +489,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -553,6 +555,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -618,6 +621,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -683,6 +687,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -756,6 +761,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -824,6 +830,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down
10 changes: 10 additions & 0 deletions ruff.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
"description": "Options for the `pep8-naming` plugin.",
"anyOf": [
Expand Down
4 changes: 3 additions & 1 deletion ruff_cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ pub fn run(
.flatten()
.map(ignore::DirEntry::path)
.collect::<Vec<_>>(),
&resolver,
pyproject_strategy,
);

let start = Instant::now();
Expand Down Expand Up @@ -169,7 +171,7 @@ pub fn run_stdin(
};
let package_root = filename
.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 mut diagnostics = lint_stdin(filename, package_root, &stdin, settings, autofix)?;
diagnostics.messages.sort_unstable();
Expand Down
2 changes: 1 addition & 1 deletion src/lib_native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Diagnosti
// Generate diagnostics.
let diagnostics = check_path(
path,
packaging::detect_package_root(path),
packaging::detect_package_root(path, &settings.namespace_packages),
contents,
tokens,
&locator,
Expand Down
5 changes: 3 additions & 2 deletions src/lib_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,15 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
force_exclude: None,
format: None,
ignore_init_module_imports: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
show_source: None,
src: None,
unfixable: None,
typing_modules: None,
task_tags: None,
typing_modules: None,
unfixable: None,
update_check: None,
// Use default options for all plugins.
flake8_annotations: Some(flake8_annotations::settings::Settings::default().into()),
Expand Down
37 changes: 29 additions & 8 deletions src/packaging.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
//! Detect Python package roots and file associations.

use std::path::Path;
use std::path::{Path, PathBuf};

use rustc_hash::FxHashMap;

use crate::resolver::{PyprojectDiscovery, Resolver};

// If we have a Python package layout like:
// - root/
// - foo/
Expand All @@ -27,15 +29,21 @@ use rustc_hash::FxHashMap;

/// Return `true` if the directory at the given `Path` appears to be a Python
/// package.
pub fn is_package(path: &Path) -> bool {
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)
}

/// 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;
for parent in path.ancestors() {
if !is_package(parent) {
if !is_package(parent, namespace_packages) {
return current;
}
current = Some(parent);
Expand All @@ -46,21 +54,23 @@ pub fn detect_package_root(path: &Path) -> Option<&Path> {
/// A wrapper around `is_package` to cache filesystem lookups.
fn is_package_with_cache<'a>(
path: &'a Path,
namespace_packages: &'a [PathBuf],
package_cache: &mut FxHashMap<&'a Path, bool>,
) -> bool {
*package_cache
.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.
fn detect_package_root_with_cache<'a>(
path: &'a Path,
namespace_packages: &'a [PathBuf],
package_cache: &mut FxHashMap<&'a Path, bool>,
) -> Option<&'a Path> {
let mut current = None;
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;
}
current = Some(parent);
Expand All @@ -69,7 +79,11 @@ fn detect_package_root_with_cache<'a>(
}

/// 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
// required to) contain some `__init__.py` files.
let mut package_cache: FxHashMap<&Path, bool> = FxHashMap::default();
Expand All @@ -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.
let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default();
for file in files {
let namespace_packages = &resolver
.resolve(file, pyproject_strategy)
.namespace_packages;
if let Some(package) = file.parent() {
if package_roots.contains_key(package) {
continue;
}
package_roots.insert(
package,
detect_package_root_with_cache(package, &mut package_cache),
detect_package_root_with_cache(package, namespace_packages, &mut package_cache),
);
}
}
Expand All @@ -111,6 +128,7 @@ mod tests {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/package/src/package")
.as_path(),
&[],
),
Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
Expand All @@ -124,6 +142,7 @@ mod tests {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/project/python_modules/core/core")
.as_path(),
&[],
),
Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
Expand All @@ -137,6 +156,7 @@ mod tests {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/project/examples/docs/docs/concepts")
.as_path(),
&[],
),
Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
Expand All @@ -150,6 +170,7 @@ mod tests {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("setup.py")
.as_path(),
&[],
),
None,
);
Expand Down
6 changes: 6 additions & 0 deletions src/settings/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub struct Configuration {
pub ignore: Option<Vec<RuleCodePrefix>>,
pub ignore_init_module_imports: Option<bool>,
pub line_length: Option<usize>,
pub namespace_packages: Option<Vec<PathBuf>>,
pub per_file_ignores: Option<Vec<PerFileIgnore>>,
pub required_version: Option<Version>,
pub respect_gitignore: Option<bool>,
Expand Down Expand Up @@ -135,6 +136,10 @@ impl Configuration {
ignore: options.ignore,
ignore_init_module_imports: options.ignore_init_module_imports,
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
.into_iter()
Expand Down Expand Up @@ -211,6 +216,7 @@ impl Configuration {
.ignore_init_module_imports
.or(config.ignore_init_module_imports),
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),
required_version: self.required_version.or(config.required_version),
respect_gitignore: self.respect_gitignore.or(config.respect_gitignore),
Expand Down
4 changes: 4 additions & 0 deletions src/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub struct Settings {
pub format: SerializationFormat,
pub ignore_init_module_imports: bool,
pub line_length: usize,
pub namespace_packages: Vec<PathBuf>,
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet<RuleCode>)>,
pub required_version: Option<Version>,
pub respect_gitignore: bool,
Expand Down Expand Up @@ -169,6 +170,7 @@ impl Settings {
force_exclude: config.force_exclude.unwrap_or(false),
ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(),
line_length: config.line_length.unwrap_or(88),
namespace_packages: config.namespace_packages.unwrap_or_default(),
per_file_ignores: resolve_per_file_ignores(
config.per_file_ignores.unwrap_or_default(),
)?,
Expand Down Expand Up @@ -235,6 +237,7 @@ impl Settings {
format: SerializationFormat::Text,
ignore_init_module_imports: false,
line_length: 88,
namespace_packages: vec![],
per_file_ignores: vec![],
required_version: None,
respect_gitignore: true,
Expand Down Expand Up @@ -279,6 +282,7 @@ impl Settings {
format: SerializationFormat::Text,
ignore_init_module_imports: false,
line_length: 88,
namespace_packages: vec![],
per_file_ignores: vec![],
required_version: None,
respect_gitignore: true,
Expand Down
11 changes: 11 additions & 0 deletions src/settings/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,17 @@ pub struct Options {
/// packages in that directory. User home directory and environment
/// variables will also be expanded.
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(
default = r#""py310""#,
value_type = "PythonVersion",
Expand Down
6 changes: 6 additions & 0 deletions src/settings/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ mod tests {
ignore: None,
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -239,6 +240,7 @@ line-length = 79
ignore: None,
ignore_init_module_imports: None,
line_length: Some(79),
namespace_packages: None,
per_file_ignores: None,
respect_gitignore: None,
required_version: None,
Expand Down Expand Up @@ -299,6 +301,7 @@ exclude = ["foo.py"]
ignore: None,
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -358,6 +361,7 @@ select = ["E501"]
ignore: None,
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -418,6 +422,7 @@ ignore = ["E501"]
ignore: Some(vec![RuleCodePrefix::E501]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
Expand Down Expand Up @@ -515,6 +520,7 @@ other-attribute = 1
fixable: None,
format: None,
force_exclude: None,
namespace_packages: None,
unfixable: None,
typing_modules: None,
task_tags: None,
Expand Down