From 5a0a1d8253a1ced8866e449f57e41421645a2119 Mon Sep 17 00:00:00 2001 From: Francesco Pezzella Date: Thu, 5 Nov 2026 16:22:44 +0100 Subject: [PATCH 1/2] Make PythonScanner skip every file and directory starting with a dot and return only .py files, resolving the incomplete EXCLUDED_DIRS list. --- .../features/type-parameter-modernization.md | 3 +- src/renaissance/project/project_scanner.py | 20 +++++++----- test/project/test_project_scanner.py | 31 +++++++++++++++++-- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/docs/user/features/type-parameter-modernization.md b/docs/user/features/type-parameter-modernization.md index ec56d6dc..2ffa60c0 100644 --- a/docs/user/features/type-parameter-modernization.md +++ b/docs/user/features/type-parameter-modernization.md @@ -421,7 +421,8 @@ python src/rejuvenation/migration_type_recipes.py --py MAJOR.MINOR \ [--report PATH] [--no-ruff] [--ignore FILE...] [--no-localization] [--second-pass] ``` -- ``: a `.py` file or a directory, scanned recursively except `.git`, `__pycache__`, `.pixi`, `.venv` and `venv`. +- ``: a `.py` file or a directory, scanned recursively for `.py` files. Files and directories starting with `.`, + and `__pycache__` and `venv` directories, are skipped. - `--py` (required): the oldest Python version the target project supports, 3.12 or higher. - `--report`: also write the report to a file. - `--no-ruff`: skip the final `ruff` pass. diff --git a/src/renaissance/project/project_scanner.py b/src/renaissance/project/project_scanner.py index 215001fd..d9fa1eaf 100644 --- a/src/renaissance/project/project_scanner.py +++ b/src/renaissance/project/project_scanner.py @@ -53,10 +53,9 @@ def find_sources(self) -> list[str]: class PythonScanner(ProjectScanner): - """Scan a directory tree for Python source files.""" + """Scan a directory tree for Python source files, skipping hidden files and directories.""" - EXCLUDED_DIRS = frozenset({".git", "__pycache__", ".venv", "venv"}) - # TODO: incomplete list, extend this list with more files/directories that should always be ignored + EXCLUDED_DIRS = frozenset({"__pycache__", "venv"}) def __init__(self, root_dir: str = ".", package_dirs: list[str] | None = None) -> None: """Store the scan root and optional package_dirs narrowing. @@ -69,7 +68,11 @@ def __init__(self, root_dir: str = ".", package_dirs: list[str] | None = None) - self.package_dirs = package_dirs def find_sources(self) -> list[str]: - """Return every .py file under root_dir (or package_dirs, if given), sorted, excluding EXCLUDED_DIRS.""" + """Return every .py file under root_dir (or package_dirs, if given), sorted. + + Files and directories whose name starts with "." are skipped, as are EXCLUDED_DIRS subdirectories. + The names of root_dir and package_dirs themselves are never checked. + """ path = Path(self.root_dir) if not path.exists(): message = f"root_dir does not exist: {self.root_dir}" @@ -78,13 +81,16 @@ def find_sources(self) -> list[str]: message = f"root_dir is not a directory: {self.root_dir}" raise NotADirectoryError(message) - roots = [Path(self.root_dir) / d for d in self.package_dirs] if self.package_dirs else [Path(self.root_dir)] + roots = [path / d for d in self.package_dirs] if self.package_dirs else [path] files: list[Path] = [] for root in roots: if not root.exists(): continue - files.extend(path for path in root.rglob("*.py") if not any(part in self.EXCLUDED_DIRS for part in path.parts)) - return sorted(str(path) for path in files) + for dirpath, dirnames, filenames in root.walk(): + # Pruning in place stops walk() from descending into skipped directories at all. + dirnames[:] = [name for name in dirnames if not name.startswith(".") and name not in self.EXCLUDED_DIRS] + files.extend(dirpath / name for name in filenames if name.endswith(".py") and not name.startswith(".")) + return sorted(str(file) for file in files) class BearCppScanner(CppScanner): diff --git a/test/project/test_project_scanner.py b/test/project/test_project_scanner.py index 1086cf9a..1fd228a2 100644 --- a/test/project/test_project_scanner.py +++ b/test/project/test_project_scanner.py @@ -22,6 +22,8 @@ PythonScanner, ) +NOISE_DIR_NAMES = sorted({*PythonScanner.EXCLUDED_DIRS, ".git", ".venv", ".pixi", ".tox"}) + # --------------------------------------------------------------------------- # ProjectScanner (base) # --------------------------------------------------------------------------- @@ -228,7 +230,7 @@ def test_raises_when_root_dir_is_not_a_directory(self, tmp_path) -> None: assert_that(calling(scanner.find_sources), raises(NotADirectoryError)) - @pytest.mark.parametrize("excluded_dir", sorted(PythonScanner.EXCLUDED_DIRS)) + @pytest.mark.parametrize("excluded_dir", NOISE_DIR_NAMES) def test_excludes_known_noise_dirs_in_default_whole_tree_scan(self, tmp_path, excluded_dir) -> None: """Assert PythonScanner.find_sources excludes known noise directories during a default whole-tree scan.""" noise_dir = tmp_path / excluded_dir @@ -241,7 +243,7 @@ def test_excludes_known_noise_dirs_in_default_whole_tree_scan(self, tmp_path, ex assert_that(result, equal_to(["kept.py"])) - @pytest.mark.parametrize("excluded_dir", sorted(PythonScanner.EXCLUDED_DIRS)) + @pytest.mark.parametrize("excluded_dir", NOISE_DIR_NAMES) def test_excludes_known_noise_dirs_within_explicit_package_dirs(self, tmp_path, excluded_dir) -> None: """Assert PythonScanner.find_sources excludes known noise directories within an explicit package_dirs.""" src = tmp_path / "src" @@ -268,6 +270,31 @@ def test_default_package_dirs_scans_whole_root_dir(self, tmp_path) -> None: assert_that(result, equal_to([str(redis_like / "client.py")])) + @pytest.mark.parametrize("excluded_dir", NOISE_DIR_NAMES) + def test_root_dir_inside_excluded_name_is_still_scanned(self, tmp_path: Path, excluded_dir: str) -> None: + """Assert PythonScanner.find_sources only skips excluded names below root_dir, never in its ancestors.""" + root = tmp_path / excluded_dir / "project" + noise_dir = root / excluded_dir + noise_dir.mkdir(parents=True) + (noise_dir / "ignored.py").write_text("") + (root / "kept.py").write_text("") + + scanner = PythonScanner(str(root)) + result = [Path(p).name for p in scanner.find_sources()] + + assert_that(result, equal_to(["kept.py"])) + + @pytest.mark.parametrize("hidden_name", [".hidden.py", ".py"]) + def test_skips_files_starting_with_a_dot(self, tmp_path: Path, hidden_name: str) -> None: + """Assert PythonScanner.find_sources skips Python files whose name starts with a dot.""" + (tmp_path / hidden_name).write_text("") + (tmp_path / "kept.py").write_text("") + + scanner = PythonScanner(str(tmp_path)) + result = [Path(p).name for p in scanner.find_sources()] + + assert_that(result, equal_to(["kept.py"])) + # --------------------------------------------------------------------------- # BearCppScanner From 8408944851d819c47c033732785c77981bf1b0d4 Mon Sep 17 00:00:00 2001 From: Francesco Pezzella Date: Fri, 6 Nov 2026 10:14:28 +0100 Subject: [PATCH 2/2] Rename EXCLUDED_DIRS -> SPECIAL_EXCLUDED_DIRS --- src/renaissance/project/project_scanner.py | 4 ++-- test/project/test_project_scanner.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renaissance/project/project_scanner.py b/src/renaissance/project/project_scanner.py index d9fa1eaf..6e48b810 100644 --- a/src/renaissance/project/project_scanner.py +++ b/src/renaissance/project/project_scanner.py @@ -55,7 +55,7 @@ def find_sources(self) -> list[str]: class PythonScanner(ProjectScanner): """Scan a directory tree for Python source files, skipping hidden files and directories.""" - EXCLUDED_DIRS = frozenset({"__pycache__", "venv"}) + SPECIAL_EXCLUDED_DIRS = frozenset({"__pycache__", "venv"}) def __init__(self, root_dir: str = ".", package_dirs: list[str] | None = None) -> None: """Store the scan root and optional package_dirs narrowing. @@ -88,7 +88,7 @@ def find_sources(self) -> list[str]: continue for dirpath, dirnames, filenames in root.walk(): # Pruning in place stops walk() from descending into skipped directories at all. - dirnames[:] = [name for name in dirnames if not name.startswith(".") and name not in self.EXCLUDED_DIRS] + dirnames[:] = [name for name in dirnames if not name.startswith(".") and name not in self.SPECIAL_EXCLUDED_DIRS] files.extend(dirpath / name for name in filenames if name.endswith(".py") and not name.startswith(".")) return sorted(str(file) for file in files) diff --git a/test/project/test_project_scanner.py b/test/project/test_project_scanner.py index 1fd228a2..68cb3055 100644 --- a/test/project/test_project_scanner.py +++ b/test/project/test_project_scanner.py @@ -22,7 +22,7 @@ PythonScanner, ) -NOISE_DIR_NAMES = sorted({*PythonScanner.EXCLUDED_DIRS, ".git", ".venv", ".pixi", ".tox"}) +NOISE_DIR_NAMES = sorted({*PythonScanner.SPECIAL_EXCLUDED_DIRS, ".git", ".venv", ".pixi", ".tox"}) # --------------------------------------------------------------------------- # ProjectScanner (base)