Skip to content
Open
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
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ long_description_content_type = text/x-rst
packages =
trashcli
trashcli.empty
trashcli.guard
trashcli.lib
trashcli.list
trashcli.list.minor_actions
Expand Down
8 changes: 4 additions & 4 deletions tests/test_empty/components/test_guard.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import unittest

from tests.support.py2mock import Mock, call
from trashcli.empty.guard import Guard, UserIntention
from trashcli.guard.guard import Guard, UserIntention


class TestGuard(unittest.TestCase):
def setUp(self):
self.user = Mock(spec=['do_you_wanna_empty_trash_dirs'])
self.user = Mock(spec=['confirm'])
self.guard = Guard(self.user)

def test_user_says_yes(self):
self.user.do_you_wanna_empty_trash_dirs.return_value = True
self.user.confirm.return_value = True

result = self.guard.ask_the_user(True, ['trash_dirs'])

assert UserIntention(ok_to_empty=True,
trash_dirs=['trash_dirs']) == result

def test_user_says_no(self):
self.user.do_you_wanna_empty_trash_dirs.return_value = False
self.user.confirm.return_value = False

result = self.guard.ask_the_user(True, ['trash_dirs'])

Expand Down
2 changes: 1 addition & 1 deletion tests/test_empty/components/test_parse_reply.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest

from trashcli.empty.parse_reply import parse_reply
from trashcli.guard.parse_reply import parse_reply


class TestParseReply(unittest.TestCase):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_empty/components/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from tests.support.py2mock import Mock, call

from trashcli.empty.user import User
from trashcli.guard.user import User
from trashcli.lib.my_input import HardCodedInput


Expand All @@ -18,7 +18,7 @@ def test(self):
self.parse_reply.return_value = 'result'
self.input.set_reply('reply')

result = self.user.do_you_wanna_empty_trash_dirs(['trash_dirs'])
result = self.user.confirm(['trash_dirs'])

assert [
result,
Expand Down
20 changes: 12 additions & 8 deletions trashcli/empty/empty_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
)
from trashcli.empty.emptier import Emptier
from trashcli.empty.existing_file_remover import ExistingFileRemover
from trashcli.empty.guard import Guard
from trashcli.empty.parse_reply import parse_reply
from trashcli.guard.guard import Guard
from trashcli.guard.parse_reply import parse_reply
from trashcli.empty.prepare_output_message import prepare_output_message
from trashcli.empty.user import User
from trashcli.guard.user import User
from trashcli.fs import ContentsOf
from trashcli.fstab.volume_listing import VolumesListing
from trashcli.fstab.volume_of import VolumeOf
Expand Down Expand Up @@ -65,8 +65,12 @@ def run_action(self,
args.user_specified_trash_dirs,
args.environ,
args.uid)
delete_pass = self.guard.ask_the_user(args.interactive,
trash_dirs)
if delete_pass.ok_to_empty:
self.emptier.do_empty(delete_pass.trash_dirs, args.environ,
args.days, args.dry_run, args.verbose)
trash_dirs = list(trash_dirs)
if trash_dirs: # skip asking the user if there is nothing to delete; avoids being stuck
delete_pass = self.guard.ask_the_user(args.interactive,
trash_dirs)
if delete_pass.ok_to_empty:
self.emptier.do_empty(delete_pass.trash_dirs, args.environ,
args.days, args.dry_run, args.verbose)
else:
print('No trash directories to empty.')
Comment on lines +75 to +76
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch prints directly to stdout via print(), bypassing the Console abstraction that EmptyCmd wires up (and which is used elsewhere for output). This makes it harder to redirect/capture output consistently (e.g., when EmptyCmd is used programmatically with a custom out). Prefer emitting this message through console.out (or add a dedicated Console.print_info() method) instead of using print().

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion trashcli/empty/empty_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from trashcli.empty.empty_action import EmptyAction, EmptyActionArgs
from trashcli.empty.errors import Errors
from trashcli.empty.existing_file_remover import ExistingFileRemover
from trashcli.empty.is_input_interactive import is_input_interactive
from trashcli.guard.is_input_interactive import is_input_interactive
from trashcli.empty.parser import Parser
from trashcli.empty.print_time_action import PrintTimeAction, PrintTimeArgs
from trashcli.fs import ContentsOf
Expand Down
1 change: 1 addition & 0 deletions trashcli/guard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

4 changes: 2 additions & 2 deletions trashcli/empty/guard.py → trashcli/guard/guard.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Iterable, NamedTuple

from trashcli.empty.user import User
from trashcli.guard.user import User
from trashcli.trash_dirs_scanner import TrashDir

UserIntention = NamedTuple('UserIntention',
Expand All @@ -26,7 +26,7 @@ def _interactive(self, trash_dirs, # type: Iterable[TrashDir]
): # type: (...) -> UserIntention
trash_dirs_list = list(trash_dirs) # type: Iterable[TrashDir]
ok_to_empty = \
self.user.do_you_wanna_empty_trash_dirs(trash_dirs_list)
self.user.confirm(trash_dirs_list)
list_result = trash_dirs_list if ok_to_empty else []
return UserIntention(ok_to_empty=ok_to_empty,
trash_dirs=list_result)
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions trashcli/empty/user.py → trashcli/guard/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ def __init__(self,
self.input = input
self.parse_reply = parse_reply

def do_you_wanna_empty_trash_dirs(self, trash_dirs):
reply = self.input.read_input(self.prepare_output_message(trash_dirs))
def confirm(self, items_to_confirm):
reply = self.input.read_input(self.prepare_output_message(items_to_confirm))
return self.parse_reply(reply)
6 changes: 6 additions & 0 deletions trashcli/rm/prepare_output_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def prepare_output_message(files_to_remove):
if not files_to_remove:
return 'No files to be removed.'
else:
return 'The following files / directories will be removed:\n' + '\n'.join(
' - ' + file[0] for file in files_to_remove) + '\nProceed? (y/N) '
18 changes: 16 additions & 2 deletions trashcli/rm/rm_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
from trashcli.compat import Protocol

from trashcli.fs import ContentsOf
from trashcli.guard.guard import Guard
from trashcli.guard.is_input_interactive import is_input_interactive
from trashcli.guard.parse_reply import parse_reply
from trashcli.guard.user import User
Comment thread
abstract-official marked this conversation as resolved.
from trashcli.lib.dir_checker import DirChecker
from trashcli.lib.dir_reader import DirReader
from trashcli.lib.my_input import RealInput
from trashcli.lib.user_info import SingleUserInfoProvider
from trashcli.rm.cleanable_trashcan import CleanableTrashcan
from trashcli.rm.file_remover import FileRemover
from trashcli.rm.filter import Filter
from trashcli.rm.list_trashinfo import ListTrashinfos
from trashcli.rm.prepare_output_message import prepare_output_message
from trashcli.trash_dirs_scanner import TrashDirsScanner, TopTrashDirRules, \
trash_dir_found

Expand Down Expand Up @@ -60,6 +66,7 @@ def run(self, argv, uid):

for event, args in scanner.scan_trash_dirs(self.environ, uid):
if event == trash_dir_found:
files_to_remove = []
path, volume = args
for type, arg in listing.list_from_volume_trashdir(path,
volume):
Expand All @@ -68,8 +75,15 @@ def run(self, argv, uid):
elif type == 'trashed_file':
original_location, info_file = arg
if cmd.matches(original_location):
trashcan.delete_trash_info_and_backup_copy(
info_file)
files_to_remove.append(arg)
if files_to_remove: # skip asking the user if there is nothing to delete; avoids being stuck
user = User(prepare_output_message, RealInput(), parse_reply)
guard = Guard(user)
if guard.ask_the_user(is_input_interactive(), files_to_remove).ok_to_empty:
for file in files_to_remove:
Comment on lines +79 to +83
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change hard-codes interactive detection (is_input_interactive()) and uses RealInput() directly inside run(). That makes behavior depend on whether stdin is a TTY (tests or callers running in a TTY can hang waiting for input), and it’s difficult to unit test the yes/no paths without patching global stdin. Consider injecting an Input implementation (like HardCodedInput) and/or parsing -i/-f flags similarly to trash-empty, so tests and non-interactive callers can reliably control confirmation behavior.

Copilot uses AI. Check for mistakes.
trashcan.delete_trash_info_and_backup_copy(file[1])
else:
print('No files / directories to be removed in {}'.format(path))
Comment on lines +79 to +86
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New guarded-deletion behavior is introduced here, but there are no tests exercising the interactive confirmation branches (user answers yes vs no), nor the “no matches” path. Since tests/test_rm/ already has command-level and integration tests for RmCmd, it would be good to extend them to cover the new prompt flow and ensure it doesn’t regress or hang (especially when stdin is a TTY).

Copilot uses AI. Check for mistakes.

def unable_to_parse_path(self, trashinfo):
self.report_error('{}: unable to parse \'Path\''.format(trashinfo))
Expand Down