From c057319e6e8e5c18776c77a69181d48213f9fd8d Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Mon, 30 Mar 2026 19:03:03 +0200 Subject: [PATCH 1/2] Don't log file deletion when --ignore matches --- pyclean/erase.py | 4 +++- tests/test_erase.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pyclean/erase.py b/pyclean/erase.py index e06a3a7..940e850 100644 --- a/pyclean/erase.py +++ b/pyclean/erase.py @@ -40,6 +40,9 @@ def delete_filesystem_objects( all_names = sorted(directory.glob(path_glob), reverse=True) if Runner.ignore: all_names = [n for n in all_names if not Runner.is_ignored(n)] + if not all_names: + return + log.debug('Erase file system objects matching: %s', path_glob) dirs = (name for name in all_names if name.is_dir() and not name.is_symlink()) files = (name for name in all_names if not name.is_dir() or name.is_symlink()) @@ -87,5 +90,4 @@ def remove_freeform_targets( object is shown (unless the ``--yes`` option is used, in addition). """ for path_glob in glob_patterns: - log.debug('Erase file system objects matching: %s', path_glob) delete_filesystem_objects(directory, path_glob, prompt=not yes, dry_run=dry_run) diff --git a/tests/test_erase.py b/tests/test_erase.py index 8da0c76..b5c0206 100644 --- a/tests/test_erase.py +++ b/tests/test_erase.py @@ -4,6 +4,7 @@ """Tests for the erase module.""" +import logging from argparse import Namespace from pathlib import Path from unittest.mock import call, patch @@ -253,6 +254,34 @@ def test_path_is_not_ignored_for_unrelated_path(): assert not pyclean.main.Runner.is_ignored(Path('other/foo.txt')) +def test_erase_ignored_dir_produces_no_output(tmp_path, caplog): + """ + Does erase suppress all output for ignored directories? + + When a glob pattern matches only ignored paths, the erase feature + should not produce any log output about those patterns (regression + test for the --ignore flag not fully suppressing --erase targets). + """ + ignored_dir = tmp_path / 'foo' + ignored_dir.mkdir() + (ignored_dir / 'file.txt').write_text('test') + (ignored_dir / 'sub').mkdir() + (ignored_dir / 'sub' / 'deep.txt').write_text('test') + + args = Namespace(dry_run=True, ignore=['foo']) + pyclean.main.Runner.configure(args) + + with caplog.at_level(logging.DEBUG): + remove_freeform_targets(tmp_path, ['foo/**/*', 'foo'], yes=True, dry_run=True) + + assert pyclean.main.Runner.unlink_count == 0 + assert pyclean.main.Runner.rmdir_count == 0 + for record in caplog.records: + assert 'foo' not in record.message, ( + f'Ignored pattern should not appear in output: {record.message!r}' + ) + + def test_delete_filesystem_objects_skips_ignored_dirs(tmp_path): """ Does delete_filesystem_objects skip files and directories in ignored paths? From a00317267d0c877c71a8a8f2831ef6248a50b8eb Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Mon, 30 Mar 2026 19:23:52 +0200 Subject: [PATCH 2/2] Reorganize/consolidate test modules --- tests/test_erase.py | 34 --------------- tests/test_ignore.py | 96 +++++++++++++++++++++++++++++++++++++++++ tests/test_runner.py | 34 +++++++++++++++ tests/test_traversal.py | 88 ------------------------------------- 4 files changed, 130 insertions(+), 122 deletions(-) create mode 100644 tests/test_ignore.py diff --git a/tests/test_erase.py b/tests/test_erase.py index b5c0206..64ff0ca 100644 --- a/tests/test_erase.py +++ b/tests/test_erase.py @@ -220,40 +220,6 @@ def test_confirm_no(mock_input): assert confirm('Test message') is False -def test_path_is_ignored_for_dir_itself(): - """ - Does Runner.is_ignored return True for an ignored directory itself? - """ - pyclean.main.Runner.ignore = ['allure-results'] - assert pyclean.main.Runner.is_ignored(Path('allure-results')) - - -def test_path_is_ignored_for_file_in_ignored_dir(): - """ - Does Runner.is_ignored return True for a file inside an ignored directory? - """ - pyclean.main.Runner.ignore = ['allure-results'] - assert pyclean.main.Runner.is_ignored(Path('allure-results/foo.txt')) - - -def test_path_is_ignored_for_nested_path_in_ignored_dir(): - """ - Does Runner.is_ignored return True for a deeply nested path inside an ignored - directory? - """ - pyclean.main.Runner.ignore = ['allure-results'] - assert pyclean.main.Runner.is_ignored(Path('allure-results/sub/deep/file.txt')) - - -def test_path_is_not_ignored_for_unrelated_path(): - """ - Does Runner.is_ignored return False for a path not matching any ignore pattern? - """ - pyclean.main.Runner.ignore = ['allure-results'] - assert not pyclean.main.Runner.is_ignored(Path('keep.txt')) - assert not pyclean.main.Runner.is_ignored(Path('other/foo.txt')) - - def test_erase_ignored_dir_produces_no_output(tmp_path, caplog): """ Does erase suppress all output for ignored directories? diff --git a/tests/test_ignore.py b/tests/test_ignore.py new file mode 100644 index 0000000..d533406 --- /dev/null +++ b/tests/test_ignore.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2020 Peter Bittner +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Tests for the ignore module.""" + +import platform + +import pytest + +from pyclean.ignore import normalize, should_ignore + + +@pytest.mark.parametrize( + ('path_str', 'patterns', 'expected'), + [ + # Simple name matches + ('foo/bar', ['bar'], True), + ('foo/baz/bar', ['bar'], True), + ('bar', ['bar'], True), + ('foo/bar', ['baz'], False), + # Path matches + ('foo/bar', ['foo/bar'], True), + ('baz/foo/bar', ['foo/bar'], True), + ('test/foo/bar', ['foo/bar'], True), + ('foo/bar/baz', ['foo/bar'], True), # Subdirectories are also ignored + ('bar/foo', ['foo/bar'], False), + ('foo/baz', ['foo/bar'], False), + # Multiple patterns + ('foo/bar', ['baz', 'bar'], True), + ('foo/bar', ['baz', 'foo/bar'], True), + ('foo/bar', ['test', 'data'], False), + # Edge cases + ('bar', ['foo/bar'], False), + ('foo', ['foo/bar'], False), + # Pattern longer than path + ('baz', ['foo/bar/baz'], False), + ('bar/baz', ['foo/bar/baz'], False), + # None/empty patterns + ('foo/bar', None, False), + ('foo/bar', [], False), + # Subdirectories of ignored paths + ('foo/bar/baz/deep', ['foo/bar'], True), + ('src/foo/bar/models', ['foo/bar'], True), + ('foo/bar/baz', ['foo/bar/baz'], True), + ], +) +def test_should_ignore(path_str, patterns, expected): + """ + Does should_ignore correctly match path patterns? + """ + result = should_ignore(path_str, patterns) + assert result == expected + + +@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows-specific test') +@pytest.mark.parametrize( + ('path_str', 'patterns', 'expected'), + [ + # Windows-style pattern (backslash) matching filesystem paths + ('foo/bar', [r'foo\bar'], True), + ('foo/bar/baz', [r'foo\bar'], True), + ('src/foo/bar', [r'foo\bar'], True), + ], +) +def test_should_ignore_windows_paths(path_str, patterns, expected): + """ + Does should_ignore correctly handle Windows-style backslash patterns? + This test only runs on Windows where backslash is a path separator. + """ + result = should_ignore(path_str, patterns) + assert result == expected + + +@pytest.mark.skipif( + platform.system() not in ['Linux', 'Darwin'], + reason='Unix-specific test', +) +def test_normalize_pattern_posix(): + """ + Does normalize preserve patterns on Unix? + On Unix, backslash can be part of a filename. + """ + assert normalize('foo/bar') == 'foo/bar' + assert normalize(r'foo\bar') == r'foo\bar' # Preserved on Linux and macOS + assert normalize('bar') == 'bar' + + +@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows-specific test') +def test_normalize_pattern_windows(): + """ + Does normalize convert backslashes to forward slashes on Windows? + """ + assert normalize('foo/bar') == 'foo/bar' + assert normalize(r'foo\bar') == 'foo/bar' # Normalized + assert normalize('bar') == 'bar' diff --git a/tests/test_runner.py b/tests/test_runner.py index 24e661e..3f73fd6 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -109,3 +109,37 @@ def test_dryrun( assert not mock_real_rmdir.called assert mock_dry_unlink.called assert mock_dry_rmdir.called + + +def test_is_ignored_for_dir_itself(): + """ + Does Runner.is_ignored return True for an ignored directory itself? + """ + pyclean.main.Runner.ignore = ['allure-results'] + assert pyclean.main.Runner.is_ignored(Path('allure-results')) + + +def test_is_ignored_for_file_in_ignored_dir(): + """ + Does Runner.is_ignored return True for a file inside an ignored directory? + """ + pyclean.main.Runner.ignore = ['allure-results'] + assert pyclean.main.Runner.is_ignored(Path('allure-results/foo.txt')) + + +def test_is_ignored_for_nested_path_in_ignored_dir(): + """ + Does Runner.is_ignored return True for a deeply nested path inside an ignored + directory? + """ + pyclean.main.Runner.ignore = ['allure-results'] + assert pyclean.main.Runner.is_ignored(Path('allure-results/sub/deep/file.txt')) + + +def test_is_not_ignored_for_unrelated_path(): + """ + Does Runner.is_ignored return False for a path not matching any ignore pattern? + """ + pyclean.main.Runner.ignore = ['allure-results'] + assert not pyclean.main.Runner.is_ignored(Path('keep.txt')) + assert not pyclean.main.Runner.is_ignored(Path('other/foo.txt')) diff --git a/tests/test_traversal.py b/tests/test_traversal.py index 982c671..6d9d121 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -4,18 +4,15 @@ """Tests for the traversal module.""" -import platform from argparse import Namespace from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import call, patch -import pytest from conftest import SymlinkMock import pyclean.main from pyclean.bytecode import BYTECODE_DIRS, BYTECODE_FILES -from pyclean.ignore import normalize, should_ignore from pyclean.traversal import descend_and_clean @@ -55,91 +52,6 @@ def test_skip_ignored_directories(mock_log): mock_log.debug.assert_called_with('Skipping %s', '.git') -@pytest.mark.parametrize( - ('path_str', 'patterns', 'expected'), - [ - # Simple name matches - ('foo/bar', ['bar'], True), - ('foo/baz/bar', ['bar'], True), - ('bar', ['bar'], True), - ('foo/bar', ['baz'], False), - # Path matches - ('foo/bar', ['foo/bar'], True), - ('baz/foo/bar', ['foo/bar'], True), - ('test/foo/bar', ['foo/bar'], True), - ('foo/bar/baz', ['foo/bar'], True), # Subdirectories are also ignored - ('bar/foo', ['foo/bar'], False), - ('foo/baz', ['foo/bar'], False), - # Multiple patterns - ('foo/bar', ['baz', 'bar'], True), - ('foo/bar', ['baz', 'foo/bar'], True), - ('foo/bar', ['test', 'data'], False), - # Edge cases - ('bar', ['foo/bar'], False), - ('foo', ['foo/bar'], False), - # Pattern longer than path - ('baz', ['foo/bar/baz'], False), - ('bar/baz', ['foo/bar/baz'], False), - # None/empty patterns - ('foo/bar', None, False), - ('foo/bar', [], False), - # Subdirectories of ignored paths - ('foo/bar/baz/deep', ['foo/bar'], True), - ('src/foo/bar/models', ['foo/bar'], True), - ('foo/bar/baz', ['foo/bar/baz'], True), - ], -) -def test_should_ignore(path_str, patterns, expected): - """ - Does should_ignore correctly match path patterns? - """ - result = should_ignore(path_str, patterns) - assert result == expected - - -@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows-specific test') -@pytest.mark.parametrize( - ('path_str', 'patterns', 'expected'), - [ - # Windows-style pattern (backslash) matching filesystem paths - ('foo/bar', [r'foo\bar'], True), - ('foo/bar/baz', [r'foo\bar'], True), - ('src/foo/bar', [r'foo\bar'], True), - ], -) -def test_should_ignore_windows_paths(path_str, patterns, expected): - """ - Does should_ignore correctly handle Windows-style backslash patterns? - This test only runs on Windows where backslash is a path separator. - """ - result = should_ignore(path_str, patterns) - assert result == expected - - -@pytest.mark.skipif( - platform.system() not in ['Linux', 'Darwin'], - reason='Unix-specific test', -) -def test_normalize_pattern_posix(): - """ - Does normalize preserve patterns on Unix? - On Unix, backslash can be part of a filename. - """ - assert normalize('foo/bar') == 'foo/bar' - assert normalize(r'foo\bar') == r'foo\bar' # Preserved on Linux and macOS - assert normalize('bar') == 'bar' - - -@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows-specific test') -def test_normalize_pattern_windows(): - """ - Does normalize convert backslashes to forward slashes on Windows? - """ - assert normalize('foo/bar') == 'foo/bar' - assert normalize(r'foo\bar') == 'foo/bar' # Normalized - assert normalize('bar') == 'bar' - - def test_ignore_with_simple_name(): """ Does --ignore with a simple name match directories anywhere?