diff --git a/pyclean/cli.py b/pyclean/cli.py index 242943d..1e78a81 100644 --- a/pyclean/cli.py +++ b/pyclean/cli.py @@ -127,6 +127,7 @@ def parse_arguments(): if args.yes and not args.erase and not args.git_clean: parser.error('Specifying --yes only makes sense with --erase or --git-clean.') + args.debris_without_topics = 'debris' in args and not args.debris if 'debris' in args: if 'all' in args.debris: args.debris = debris_default_topics + debris_optional_topics diff --git a/pyclean/debris.py b/pyclean/debris.py index c16cd08..d6f4882 100644 --- a/pyclean/debris.py +++ b/pyclean/debris.py @@ -149,3 +149,25 @@ def suggest_debris_option(args): 'Hint: Use --debris to also clean up build artifacts ' 'from common Python development tools.', ) + + +def suggest_optional_debris_topics(args): + """Suggest optional topics when --debris was used without topic arguments.""" + detected_topics = set() + for dir_name in args.directory: + dir_path = Path(dir_name) + if dir_path.exists(): + detected_topics.update(detect_debris_in_directory(dir_path)) + + optional_detected = sorted(detected_topics - set(args.debris)) + if optional_detected: + log.info( + 'Hint: Use --debris all or specify optional topics to clean up ' + 'more aggressively. Detected: %s', + ' '.join(optional_detected), + ) + else: + log.info( + 'Hint: Use --debris all or specify optional topics to clean up ' + 'more aggressively.', + ) diff --git a/pyclean/main.py b/pyclean/main.py index cfd3be7..e094a1f 100644 --- a/pyclean/main.py +++ b/pyclean/main.py @@ -8,7 +8,11 @@ from pathlib import Path from .bytecode import BYTECODE_DIRS, BYTECODE_FILES -from .debris import remove_debris_for, suggest_debris_option +from .debris import ( + remove_debris_for, + suggest_debris_option, + suggest_optional_debris_topics, +) from .erase import remove_freeform_targets from .folders import remove_empty_directories from .gitclean import execute_git_clean @@ -67,3 +71,5 @@ def pyclean(args): if not args.debris: suggest_debris_option(args) + elif getattr(args, 'debris_without_topics', False): + suggest_optional_debris_topics(args) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8333663..73ca526 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -145,6 +145,7 @@ def test_debris_default_args(): args = pyclean.cli.parse_arguments() assert args.debris == ['cache', 'coverage', 'package', 'pytest', 'ruff'] + assert args.debris_without_topics def test_debris_optional_args(): @@ -192,6 +193,7 @@ def test_debris_explicit_args(): args = pyclean.cli.parse_arguments() assert args.debris == ['package', 'pytest'] + assert not args.debris_without_topics def test_debris_invalid_args(): diff --git a/tests/test_debris.py b/tests/test_debris.py index 5baa565..af2e8a0 100644 --- a/tests/test_debris.py +++ b/tests/test_debris.py @@ -184,14 +184,59 @@ def test_suggest_debris_with_artifacts(mock_descend, mock_log): @patch('pyclean.main.descend_and_clean') def test_no_suggest_debris_when_used(mock_descend, mock_log): """ - Does pyclean NOT suggest --debris when it's already used? + Does pyclean avoid suggesting the base --debris option when it's already used? """ with TemporaryDirectory() as tmp, ArgvContext('pyclean', tmp, '--debris'): pyclean.cli.main() - # Check that the suggestion was NOT logged + # The base hint is replaced by the optional-topics hint. log_info_calls = [str(call) for call in mock_log.info.call_args_list] - assert not any('Hint: Use --debris' in msg for msg in log_info_calls) + assert not any( + 'Hint: Use --debris to also clean up' in msg for msg in log_info_calls + ) + assert any('Hint: Use --debris all' in msg for msg in log_info_calls) + + +@pytest.mark.parametrize('option', ['-d', '--debris']) +@patch('pyclean.debris.log') +def test_suggest_optional_debris_topics(mock_log, option): + """Does bare --debris identify optional topics left behind?""" + with TemporaryDirectory() as tmp: + directory = Path(tmp) + (directory / '.ipynb_checkpoints').mkdir() + (directory / '.mypy_cache').mkdir() + (directory / '.pytest_cache').mkdir() + + with ArgvContext('pyclean', tmp, option): + pyclean.cli.main() + + mock_log.info.assert_called_once_with( + 'Hint: Use --debris all or specify optional topics to clean up ' + 'more aggressively. Detected: %s', + 'jupyter mypy', + ) + + +@patch('pyclean.debris.log') +def test_suggest_optional_debris_topics_without_artifacts(mock_log): + """Does bare --debris still explain how to include optional topics?""" + with TemporaryDirectory() as tmp, ArgvContext('pyclean', tmp, '--debris'): + pyclean.cli.main() + + mock_log.info.assert_called_once_with( + 'Hint: Use --debris all or specify optional topics to clean up ' + 'more aggressively.', + ) + + +@pytest.mark.parametrize('topic', ['mypy', 'all']) +@patch('pyclean.debris.log') +def test_no_optional_hint_with_explicit_debris_topics(mock_log, topic): + """Does an explicit choice suppress the optional topic hint?""" + with TemporaryDirectory() as tmp, ArgvContext('pyclean', tmp, '-d', topic): + pyclean.cli.main() + + mock_log.info.assert_not_called() def test_ignore_with_debris_cleanup():