diff --git a/Cargo.lock b/Cargo.lock index 18abb48759..638866184c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -796,6 +796,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "globset" version = "0.4.9" @@ -1841,6 +1847,7 @@ dependencies = [ "fern", "filetime", "getrandom 0.2.8", + "glob", "globset", "insta", "itertools", diff --git a/Cargo.toml b/Cargo.toml index dd6624ec39..ac5594a310 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ common-path = { version = "1.0.0" } dirs = { version = "4.0.0" } fern = { version = "0.6.1" } filetime = { version = "0.2.17" } +glob = { version = "0.3.0" } globset = { version = "0.4.9" } itertools = { version = "0.10.5" } libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" } diff --git a/README.md b/README.md index af639843b9..f4b97ead0d 100644 --- a/README.md +++ b/README.md @@ -1744,6 +1744,25 @@ show-source = true The source code paths to consider, e.g., when resolving first- vs. third-party imports. +As an example: given a Python package structure like: + +```text +my_package/ + pyproject.toml + src/ + my_package/ + __init__.py + foo.py + bar.py +``` + +The `src` directory should be included in `source` (e.g., `source = ["src"]`), such that +when resolving imports, `my_package.foo` is considered a first-party import. + +This field supports globs. For example, if you have a series of Python packages in +a `python_modules` directory, `src = ["python_modules/*"]` would expand to incorporate +all of the packages in that directory. + **Default value**: `["."]` **Type**: `Vec` diff --git a/resources/test/project/README.md b/resources/test/project/README.md index 68938cebc9..a44660e4f8 100644 --- a/resources/test/project/README.md +++ b/resources/test/project/README.md @@ -9,24 +9,26 @@ Running from the repo root should pick up and enforce the appropriate settings f ``` ∴ cargo run resources/test/project/ -Found 4 error(s). +Found 5 error(s). resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used resources/test/project/src/file.py:1:8: F401 `os` imported but unused resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used -2 potentially fixable with the --fix option. +resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted +3 potentially fixable with the --fix option. ``` Running from the project directory itself should exhibit the same behavior: ``` ∴ cd resources/test/project/ && cargo run . -Found 4 error(s). +Found 5 error(s). examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used src/file.py:1:8: F401 `os` imported but unused src/file.py:5:5: F841 Local variable `x` is assigned to but never used -2 potentially fixable with the --fix option. +src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted +3 potentially fixable with the --fix option. ``` Running from the sub-package directory should exhibit the same behavior, but omit the top-level @@ -45,7 +47,7 @@ file paths from the current working directory: ``` ∴ cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/ -Found 8 error(s). +Found 9 error(s). resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused resources/test/project/examples/docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused @@ -54,7 +56,8 @@ resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used resources/test/project/src/file.py:1:8: F401 `os` imported but unused resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used -5 potentially fixable with the --fix option. +resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted +6 potentially fixable with the --fix option. ``` Running from a parent directory should this "ignore" the `exclude` (hence, `concepts/file.py` gets diff --git a/resources/test/project/pyproject.toml b/resources/test/project/pyproject.toml index 4335ce71c5..25282c2a48 100644 --- a/resources/test/project/pyproject.toml +++ b/resources/test/project/pyproject.toml @@ -1,2 +1,3 @@ [tool.ruff] -src = ["."] +src = [".", "python_modules/*"] +extend-select = ["I001"] diff --git a/resources/test/project/python_modules/app/app/__init__.py b/resources/test/project/python_modules/app/app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/test/project/python_modules/app/app/app_file.py b/resources/test/project/python_modules/app/app/app_file.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/test/project/python_modules/core/core/__init__.py b/resources/test/project/python_modules/core/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/test/project/python_modules/core/core/core_file.py b/resources/test/project/python_modules/core/core/core_file.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/test/project/src/import_file.py b/resources/test/project/src/import_file.py new file mode 100644 index 0000000000..1ef59aa0d2 --- /dev/null +++ b/resources/test/project/src/import_file.py @@ -0,0 +1,7 @@ +import numpy as np +from app import app_file +from core import core_file + +np.array([1, 2, 3]) +app_file() +core_file() diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 6d15e6a1dd..c32959c8c7 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; +use glob::{glob, GlobError, Paths, PatternError}; use regex::Regex; use crate::checks_gen::CheckCodePrefix; @@ -65,11 +66,10 @@ impl Configuration { .map(|pattern| Regex::new(&pattern)) .transpose() .map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?, - src: options.src.map(|src| { - src.iter() - .map(|path| fs::normalize_path_to(Path::new(path), project_root)) - .collect() - }), + src: options + .src + .map(|src| resolve_src(&src, project_root)) + .transpose()?, target_version: options.target_version, exclude: options.exclude.map(|paths| { paths @@ -216,3 +216,19 @@ impl Configuration { } } } + +/// Given a list of source paths, which could include glob patterns, resolve the +/// matching paths. +pub fn resolve_src(src: &[String], project_root: &Path) -> Result> { + let globs = src + .iter() + .map(Path::new) + .map(|path| fs::normalize_path_to(path, project_root)) + .map(|path| glob(&path.to_string_lossy())) + .collect::, PatternError>>()?; + let paths: Vec = globs + .into_iter() + .flatten() + .collect::, GlobError>>()?; + Ok(paths) +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs index a8a6b6ada0..dd227bfb13 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -19,7 +19,7 @@ use crate::settings::configuration::Configuration; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat}; use crate::{ flake8_annotations, flake8_bugbear, flake8_import_conventions, flake8_quotes, - flake8_tidy_imports, fs, isort, mccabe, pep8_naming, pyupgrade, + flake8_tidy_imports, isort, mccabe, pep8_naming, pyupgrade, }; pub mod configuration; @@ -296,12 +296,6 @@ pub fn resolve_per_file_ignores( .collect() } -pub fn resolve_src(src: Vec, project_root: &Path) -> Vec { - src.into_iter() - .map(|path| fs::normalize_path_to(&path, project_root)) - .collect() -} - /// Given a set of selected and ignored prefixes, resolve the set of enabled /// error codes. fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet { diff --git a/src/settings/options.rs b/src/settings/options.rs index 10bf283292..8e1bdd25a8 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -235,8 +235,28 @@ pub struct Options { )] pub show_source: Option, #[option( - doc = "The source code paths to consider, e.g., when resolving first- vs. third-party \ - imports.", + doc = r#" + The source code paths to consider, e.g., when resolving first- vs. third-party imports. + + As an example: given a Python package structure like: + + ```text + my_package/ + pyproject.toml + src/ + my_package/ + __init__.py + foo.py + bar.py + ``` + + The `src` directory should be included in `source` (e.g., `source = ["src"]`), such that + when resolving imports, `my_package.foo` is considered a first-party import. + + This field supports globs. For example, if you have a series of Python packages in + a `python_modules` directory, `src = ["python_modules/*"]` would expand to incorporate + all of the packages in that directory. + "#, default = r#"["."]"#, value_type = "Vec", example = r#"