Skip to content
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
3 changes: 2 additions & 1 deletion docs/user/features/type-parameter-modernization.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,8 @@ python src/rejuvenation/migration_type_recipes.py <path> --py MAJOR.MINOR \
[--report PATH] [--no-ruff] [--ignore FILE...] [--no-localization] [--second-pass]
```

- `<path>`: a `.py` file or a directory, scanned recursively except `.git`, `__pycache__`, `.pixi`, `.venv` and `venv`.
- `<path>`: 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.
Expand Down
20 changes: 13 additions & 7 deletions src/renaissance/project/project_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Comment thread
FrancescoPezzella marked this conversation as resolved.
Resolved
# TODO: incomplete list, extend this list with more files/directories that should always be ignored
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.
Expand All @@ -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}"
Expand All @@ -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.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)


class BearCppScanner(CppScanner):
Expand Down
31 changes: 29 additions & 2 deletions test/project/test_project_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
PythonScanner,
)

NOISE_DIR_NAMES = sorted({*PythonScanner.SPECIAL_EXCLUDED_DIRS, ".git", ".venv", ".pixi", ".tox"})

# ---------------------------------------------------------------------------
# ProjectScanner (base)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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
Expand Down
Loading