diff --git a/ontrack.py b/ontrack.py index a1f9155..7135790 100644 --- a/ontrack.py +++ b/ontrack.py @@ -188,8 +188,37 @@ def _is_on_track( return True +def _has_visible_files(directory: str, patterns: list[str]) -> bool: + """Return True iff *directory* directly contains at least one visible file. + + A "visible file" is a regular file whose name is not ``ontrack.yml`` and + does not match any pattern in *patterns*. Entries that cannot be stat'd + are silently skipped. + + Args: + directory: Path to the directory to inspect. + patterns: Shell-style glob patterns (see :func:`_is_ignored`). Files + whose names match any pattern are not considered visible. + """ + try: + for entry in os.scandir(directory): + try: + if ( + entry.is_file(follow_symlinks=False) + and entry.name != _ONTRACK_YML + and not _is_ignored(entry.name, patterns) + ): + return True + except OSError: + pass + except OSError: + pass + return False + + def _find_reporting_directories( - directory: str, ignore_patterns: list[str] | None = None + directory: str, + ignore_patterns: list[str] | None = None, ) -> list[str]: """Return reporting directories within *directory*. @@ -198,9 +227,11 @@ def _find_reporting_directories( If *directory* contains only ignored files and subdirectories, or contains no files at all, the search recurses into each non-ignored subdirectory. An empty directory (no files, no subdirectories) is itself treated as a - reporting directory. Entries that cannot be stat'd are silently skipped. - Subdirectories whose names match *ignore_patterns* are not descended into - and are not considered reporting directories. + reporting directory, unless the result set also contains directories that do + have visible files — in that case the empty directories are dropped so that + they are not reported alongside real content. Entries that cannot be stat'd + are silently skipped. Subdirectories whose names match *ignore_patterns* + are not descended into and are not considered reporting directories. **ontrack.yml special handling:** When an ``ontrack.yml`` file is found in *directory*, descent stops @@ -252,6 +283,14 @@ def _find_reporting_directories( result: list[str] = [] for subdir in subdirs: result.extend(_find_reporting_directories(subdir, patterns)) + + # If some of the collected reporting directories have visible files and + # others are empty leaves, discard the empty ones. An empty directory + # should not be reported alongside real content at the same level. + non_empty = [p for p in result if _has_visible_files(p, patterns)] + if non_empty: + result = non_empty + # Fall back to the current directory if all recursive calls returned nothing # (e.g. every subdirectory raised OSError and could not be scanned). return result if result else [directory] diff --git a/pyproject.toml b/pyproject.toml index f2721d8..9cf7b4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ontrack" -version = "0.2.0" +version = "0.2.1" description = "Scan directory trees and report file statistics from YAML configuration." readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_ontrack.py b/tests/test_ontrack.py index e47fb30..f89d6a2 100644 --- a/tests/test_ontrack.py +++ b/tests/test_ontrack.py @@ -766,6 +766,34 @@ def test_find_reporting_directories_files_and_subdir(tmp_path): assert str(dir012) not in result +def test_find_reporting_directories_empty_sibling_not_reported(tmp_path): + """An empty subdirectory is not reported when a sibling directory contains files. + + Structure: + parent/ + empty_subdir/ <- empty (no files, no subdirs) + project_dir/ + data.txt <- has content + + Expected: project_dir is reported; empty_subdir is NOT reported because it + has a sibling directory with actual content. The parent directory itself + should not appear either. + """ + parent = tmp_path / "parent" + parent.mkdir() + empty_subdir = parent / "empty_subdir" + empty_subdir.mkdir() + project_dir = parent / "project_dir" + project_dir.mkdir() + (project_dir / "data.txt").write_text("content") + + result = _find_reporting_directories(str(parent)) + + assert str(project_dir) in result + assert str(empty_subdir) not in result + assert str(parent) not in result + + # --------------------------------------------------------------------------- # main – group from config file # ---------------------------------------------------------------------------