From 36bed855e11b119adc4cd5b3ad87e2da965928ba Mon Sep 17 00:00:00 2001 From: =?utf8?q?=C5=81ukasz=20Langa?= Date: Mon, 4 Jun 2018 10:59:36 -0700 Subject: [PATCH 1/1] Introduce "project root" as a concept This is required for regular expressions in `--include=` and `--exclude=` not to catch false positives from directories outside of the project. --- black.py | 41 +++++++++++++++++++++++++++++++++++++---- tests/test_black.py | 9 ++++++--- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/black.py b/black.py index da00525..730c64d 100644 --- a/black.py +++ b/black.py @@ -253,10 +253,13 @@ def main( except re.error: err(f"Invalid regular expression for exclude given: {exclude!r}") ctx.exit(2) + root = find_project_root(src) for s in src: p = Path(s) if p.is_dir(): - sources.extend(gen_python_files_in_dir(p, include_regex, exclude_regex)) + sources.extend( + gen_python_files_in_dir(p, root, include_regex, exclude_regex) + ) elif p.is_file(): # if a file was explicitly given, we don't care about its extension sources.append(p) @@ -2792,13 +2795,14 @@ def get_future_imports(node: Node) -> Set[str]: def gen_python_files_in_dir( - path: Path, include: Pattern[str], exclude: Pattern[str] + path: Path, root: Path, include: Pattern[str], exclude: Pattern[str] ) -> Iterator[Path]: """Generate all files under `path` whose paths are not excluded by the `exclude` regex, but are included by the `include` regex. """ + assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}" for child in path.iterdir(): - normalized_path = child.resolve().as_posix() + normalized_path = child.resolve().relative_to(root).as_posix() if child.is_dir(): normalized_path += "/" exclude_match = exclude.search(normalized_path) @@ -2806,7 +2810,7 @@ def gen_python_files_in_dir( continue if child.is_dir(): - yield from gen_python_files_in_dir(child, include, exclude) + yield from gen_python_files_in_dir(child, root, include, exclude) elif child.is_file(): include_match = include.search(normalized_path) @@ -2814,6 +2818,35 @@ def gen_python_files_in_dir( yield child +def find_project_root(srcs: List[str]) -> Path: + """Return a directory containing .git, .hg, or pyproject.toml. + + That directory can be one of the directories passed in `srcs` or their + common parent. + + If no directory in the tree contains a marker that would specify it's the + project root, the root of the file system is returned. + """ + if not srcs: + return Path("/").resolve() + + common_base = min(Path(src).resolve() for src in srcs) + if common_base.is_dir(): + # Append a fake file so `parents` below returns `common_base_dir`, too. + common_base /= "fake-file" + for directory in common_base.parents: + if (directory / ".git").is_dir(): + return directory + + if (directory / ".hg").is_dir(): + return directory + + if (directory / "pyproject.toml").is_file(): + return directory + + return directory + + @dataclass class Report: """Provides a reformatting counter. Can be rendered with `str(report)`.""" diff --git a/tests/test_black.py b/tests/test_black.py index 7e50c2f..7389da9 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -864,7 +864,8 @@ class BlackTestCase(unittest.TestCase): Path(THIS_DIR / "include_exclude_tests/b/dont_exclude/a.py"), Path(THIS_DIR / "include_exclude_tests/b/dont_exclude/a.pyi"), ] - sources.extend(black.gen_python_files_in_dir(path, include, exclude)) + this_abs = THIS_DIR.resolve() + sources.extend(black.gen_python_files_in_dir(path, this_abs, include, exclude)) self.assertEqual(sorted(expected), sorted(sources)) def test_empty_include(self) -> None: @@ -882,9 +883,10 @@ class BlackTestCase(unittest.TestCase): Path(path / "b/.definitely_exclude/a.py"), Path(path / "b/.definitely_exclude/a.pyi"), ] + this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files_in_dir( - path, empty, re.compile(black.DEFAULT_EXCLUDES) + path, this_abs, empty, re.compile(black.DEFAULT_EXCLUDES) ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -901,9 +903,10 @@ class BlackTestCase(unittest.TestCase): Path(path / "b/.definitely_exclude/a.py"), Path(path / "b/.definitely_exclude/a.pyi"), ] + this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files_in_dir( - path, re.compile(black.DEFAULT_INCLUDES), empty + path, this_abs, re.compile(black.DEFAULT_INCLUDES), empty ) ) self.assertEqual(sorted(expected), sorted(sources)) -- 2.39.5