Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue #285 - Unexpected targets' cleanup order #295

Merged
merged 3 commits into from May 12, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES
Expand Up @@ -20,7 +20,8 @@ Changes
- Make it possible to use a custom encoder when using config_changed with a dict.
- Add configuration `DOIT_CONFIG` `action_string_formatting` to control command action formatter.
- Fix `result_dep`, use result **after** its execution
- Fix #286: Cannot use functools.partial as title
- Fix #286: Support `functools.partial` on tasks' dict metadata `task.title`
- Fix #285: `clean` command, remove targets in reverse DFS order
Copy link
Member

Choose a reason for hiding this comment

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

What is DFS? "Distributed File System"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's Depth First Search in this case. I didn't mean to be pedantic.

Thanks for merging!



0.31.1 (*2018-03-18*)
Expand Down
6 changes: 6 additions & 0 deletions doc/cmd_other.rst
Expand Up @@ -121,6 +121,12 @@ include a *clean* attribute. This attribute can be ``True`` to remove all of its
target files. If there is a folder as a target it will be removed if the folder
is empty, otherwise it will display a warning message.

.. note::

The targets' removal order will be the reverse of their lexical ordering.
This ensures that the directory structure formed by the targets is correctly
removed irrespective of their order in the ``targets`` array.

The *clean* attribute can be a list of actions. An action could be a
string with a shell command or a tuple with a python callable.

Expand Down
32 changes: 13 additions & 19 deletions doit/task.py
Expand Up @@ -573,26 +573,20 @@ def dict_to_task(task_dict):

def clean_targets(task, dryrun):
"""remove all targets from a task"""
files = [path for path in task.targets if os.path.isfile(path)]
dirs = [path for path in task.targets if os.path.isdir(path)]

# remove all files
for file_ in files:
print("%s - removing file '%s'" % (task.name, file_))
if not dryrun:
os.remove(file_)

# remove all directories (if empty)
for dir_ in dirs:
if os.listdir(dir_):
msg = "%s - cannot remove (it is not empty) '%s'"
print(msg % (task.name, dir_))
else:
msg = "%s - removing dir '%s'"
print(msg % (task.name, dir_))
for target in sorted(task.targets, reverse=True):
if os.path.isfile(target):
print("%s - removing file '%s'" % (task.name, target))
if not dryrun:
os.rmdir(dir_)

os.remove(target)
elif os.path.isdir(target):
if os.listdir(target):
msg = "%s - cannot remove (it is not empty) '%s'"
print(msg % (task.name, target))
else:
msg = "%s - removing dir '%s'"
print(msg % (task.name, target))
if not dryrun:
os.rmdir(target)


# uptodate
Expand Down
23 changes: 20 additions & 3 deletions tests/test_task.py
Expand Up @@ -374,8 +374,11 @@ class TestTaskClean(object):
def tmpdir(self, request):
tmpdir = {}
tmpdir['dir'] = tempfile.mkdtemp(prefix='doit-')
tmpdir['subdir'] = tempfile.mkdtemp(dir=tmpdir['dir'])
files = [os.path.join(tmpdir['dir'], fname)
for fname in ['a.txt', 'b.txt']]
for fname in ['a.txt',
'b.txt',
os.path.join(tmpdir['subdir'], 'c.txt')]]
tmpdir['files'] = files
# create empty files
for filename in tmpdir['files']:
Expand Down Expand Up @@ -412,14 +415,15 @@ def test_clean_non_existent_targets(self):

def test_clean_empty_dirs(self, tmpdir):
# Remove empty directories listed in targets
targets = tmpdir['files'] + [tmpdir['dir']]
targets = tmpdir['files'] + [tmpdir['subdir']]
t = task.Task("xxx", None, targets=targets, clean=True)
assert True == t._remove_targets
assert 0 == len(t.clean_actions)
t.clean(StringIO(), False)
for filename in tmpdir['files']:
assert not os.path.exists(filename)
assert not os.path.exists(tmpdir['dir'])
assert not os.path.exists(tmpdir['subdir'])
assert os.path.exists(tmpdir['dir'])

def test_keep_non_empty_dirs(self, tmpdir):
# Keep non empty directories listed in targets
Expand All @@ -433,6 +437,19 @@ def test_keep_non_empty_dirs(self, tmpdir):
assert expected == os.path.exists(filename)
assert os.path.exists(tmpdir['dir'])

def test_clean_in_reverse_lexical_order(self, tmpdir):
Copy link
Member

Choose a reason for hiding this comment

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

The test name is misleading because it actually does not ensure reverse lexical order.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. I'm having a bit of a tough time finding a proper name though. An option I considered is to actually do ensure that the order is that one, by patching both os.rmdir and os.remove, what would you think about this?

# Remove targets in reverse lexical order so that subdirectories' order
# in the targets array is irrelevant
targets = tmpdir['files'] + [tmpdir['dir'], tmpdir['subdir']]
t = task.Task("xxx", None, targets=targets, clean=True)
assert True == t._remove_targets
assert 0 == len(t.clean_actions)
t.clean(StringIO(), False)
for filename in tmpdir['files']:
assert not os.path.exists(filename)
assert not os.path.exists(tmpdir['dir'])
assert not os.path.exists(tmpdir['subdir'])

def test_clean_actions(self, tmpdir):
# a clean action can be anything, it can even not clean anything!
c_path = tmpdir['files'][0]
Expand Down