Skip to content

Commit 741a42d

Browse files
authored
Improve pytask clean. (#32)
1 parent 5ddf29c commit 741a42d

File tree

7 files changed

+127
-29
lines changed

7 files changed

+127
-29
lines changed

docs/changes.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@ chronological order. Releases follow `semantic versioning <https://semver.org/>`
66
all releases are available on `Anaconda.org <https://anaconda.org/pytask/pytask>`_.
77

88

9+
0.0.9 - 2020-xx-xx
10+
------------------
11+
12+
- :gh:`31` adds ``pytask collect`` to show information on collected tasks.
13+
- :gh:`32` fixes ``pytask clean``.
14+
15+
916
0.0.8 - 2020-10-04
1017
------------------
1118

1219
- :gh:`30` fixes or adds the session object to some hooks which was missing from the
1320
previous release.
14-
- :gh:`31` adds ``pytask collect`` to show information on collected tasks.
1521

1622

1723
0.0.7 - 2020-10-03

src/_pytask/build.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,6 @@ def build(**config_from_cli):
114114
working directory for tasks to execute them. A report informs you on the results.
115115
116116
"""
117+
config_from_cli["command"] = "build"
117118
session = main(config_from_cli)
118119
sys.exit(session.exit_code)

src/_pytask/clean.py

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import attr
1010
import click
1111
from _pytask.config import hookimpl
12+
from _pytask.config import IGNORED_TEMPORARY_FILES_AND_FOLDERS
1213
from _pytask.enums import ExitCode
1314
from _pytask.exceptions import CollectionError
1415
from _pytask.pluginmanager import get_plugin_manager
@@ -24,7 +25,7 @@
2425
)
2526

2627

27-
@hookimpl
28+
@hookimpl(tryfirst=True)
2829
def pytask_extend_command_line_interface(cli: click.Group):
2930
"""Extend the command line interface."""
3031
cli.add_command(clean)
@@ -44,6 +45,15 @@ def pytask_parse_config(config, config_from_cli):
4445
)
4546

4647

48+
@hookimpl
49+
def pytask_post_parse(config):
50+
"""Correct ignore patterns such that caches, etc. will not be ignored."""
51+
if config["command"] == "clean":
52+
config["ignore"] = [
53+
i for i in config["ignore"] if i not in IGNORED_TEMPORARY_FILES_AND_FOLDERS
54+
]
55+
56+
4757
@click.command()
4858
@click.argument(
4959
"paths", nargs=-1, type=click.Path(exists=True), callback=falsy_to_none_callback
@@ -58,11 +68,23 @@ def pytask_parse_config(config, config_from_cli):
5868
@click.option(
5969
"-q", "--quiet", is_flag=True, help="Do not print the names of the removed paths."
6070
)
71+
@click.option(
72+
"--ignore",
73+
type=str,
74+
multiple=True,
75+
help=(
76+
"A pattern to ignore files or directories. For example, ``task_example.py`` or "
77+
"``src/*``."
78+
),
79+
callback=falsy_to_none_callback,
80+
)
6181
@click.option(
6282
"-c", "--config", type=click.Path(exists=True), help="Path to configuration file."
6383
)
6484
def clean(**config_from_cli):
6585
"""Clean provided paths by removing files unknown to pytask."""
86+
config_from_cli["command"] = "clean"
87+
6688
try:
6789
# Duplication of the same mechanism in :func:`pytask.main.main`.
6890
pm = get_plugin_manager()
@@ -93,24 +115,29 @@ def clean(**config_from_cli):
93115
session, known_paths, include_directories
94116
)
95117

96-
targets = "Files"
97-
if session.config["directories"]:
98-
targets += " and directories"
99-
click.echo(f"\n{targets} which can be removed:\n")
100-
for path in unknown_paths:
101-
if session.config["mode"] == "dry-run":
102-
click.echo(f"Would remove {path}.")
103-
else:
104-
should_be_deleted = session.config[
105-
"mode"
106-
] == "force" or click.confirm(f"Would you like to remove {path}?")
107-
if should_be_deleted:
108-
if not session.config["quiet"]:
109-
click.echo(f"Remove {path}.")
110-
if path.is_dir():
111-
shutil.rmtree(path)
112-
else:
113-
path.unlink()
118+
if unknown_paths:
119+
targets = "Files"
120+
if session.config["directories"]:
121+
targets += " and directories"
122+
click.echo(f"\n{targets} which can be removed:\n")
123+
for path in unknown_paths:
124+
if session.config["mode"] == "dry-run":
125+
click.echo(f"Would remove {path}")
126+
else:
127+
should_be_deleted = session.config[
128+
"mode"
129+
] == "force" or click.confirm(
130+
f"Would you like to remove {path}?"
131+
)
132+
if should_be_deleted:
133+
if not session.config["quiet"]:
134+
click.echo(f"Remove {path}")
135+
if path.is_dir():
136+
shutil.rmtree(path)
137+
else:
138+
path.unlink()
139+
else:
140+
click.echo("\nThere are no files and directories which can be deleted.")
114141

115142
click.echo("\n" + "=" * config["terminal_width"])
116143

@@ -169,7 +196,7 @@ def _find_all_unknown_paths(session, known_paths, include_directories):
169196
170197
"""
171198
recursive_nodes = [
172-
_RecursivePathNode.from_path(path, known_paths)
199+
_RecursivePathNode.from_path(path, known_paths, session)
173200
for path in session.config["paths"]
174201
]
175202
unknown_paths = list(
@@ -218,21 +245,38 @@ class _RecursivePathNode:
218245
is_unknown = attr.ib(type=bool)
219246

220247
@classmethod
221-
def from_path(cls, path: Path, known_paths: list):
248+
def from_path(cls, path: Path, known_paths: list, session):
222249
"""Create a node from a path.
223250
224251
While instantiating the class, subordinate nodes are spawned for all paths
225252
inside a directory.
226253
227254
"""
228255
sub_nodes = (
229-
[_RecursivePathNode.from_path(p, known_paths) for p in path.iterdir()]
256+
[
257+
_RecursivePathNode.from_path(p, known_paths, session)
258+
for p in path.iterdir()
259+
]
230260
if path.is_dir()
261+
# Do not collect sub files and folders for ignored folders.
262+
and not session.hook.pytask_ignore_collect(path=path, config=session.config)
231263
else []
232264
)
233-
is_unknown = (path.is_file() and path not in known_paths) or (
234-
path.is_dir() and all(node.is_unknown for node in sub_nodes)
265+
266+
is_unknown_file = path.is_file() and not (
267+
path in known_paths
268+
# Ignored files are also known.
269+
or session.hook.pytask_ignore_collect(path=path, config=session.config)
270+
)
271+
is_unknown_directory = (
272+
path.is_dir()
273+
# True for folders and ignored folders without any sub nodes.
274+
and all(node.is_unknown for node in sub_nodes)
275+
# True for not ignored paths.
276+
and not session.hook.pytask_ignore_collect(path=path, config=session.config)
235277
)
278+
is_unknown = is_unknown_file or is_unknown_directory
279+
236280
return cls(path, sub_nodes, path.is_dir(), path.is_file(), is_unknown)
237281

238282
def __repr__(self):

src/_pytask/config.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@
1818

1919
hookimpl = pluggy.HookimplMarker("pytask")
2020

21-
2221
IGNORED_FOLDERS = [
2322
".git/*",
2423
".hg/*",
2524
".svn/*",
2625
".venv/*",
26+
]
27+
28+
IGNORED_FILES = [".pre-commit-config.yaml", ".readthedocs.yml", ".codecov.yml"]
29+
30+
31+
IGNORED_FILES_AND_FOLDERS = IGNORED_FILES + IGNORED_FOLDERS
32+
33+
IGNORED_TEMPORARY_FILES_AND_FOLDERS = [
2734
"*.egg-info/*",
2835
".ipynb_checkpoints/*",
2936
".mypy_cache/*",
@@ -33,8 +40,8 @@
3340
"__pycache__/*",
3441
"build/*",
3542
"dist/*",
43+
"pytest_cache/*",
3644
]
37-
"""List[str]: List of expressions to ignore folders."""
3845

3946

4047
@hookimpl
@@ -93,6 +100,8 @@ def pytask_configure(pm, config_from_cli):
93100
@hookimpl
94101
def pytask_parse_config(config, config_from_cli, config_from_file):
95102
"""Parse the configuration."""
103+
config["command"] = config_from_cli.get("command", "build")
104+
96105
config_from_file["ignore"] = parse_value_or_multiline_option(
97106
config_from_file.get("ignore")
98107
)
@@ -105,7 +114,8 @@ def pytask_parse_config(config, config_from_cli, config_from_file):
105114
default=[],
106115
)
107116
)
108-
+ IGNORED_FOLDERS
117+
+ IGNORED_FILES_AND_FOLDERS
118+
+ IGNORED_TEMPORARY_FILES_AND_FOLDERS
109119
)
110120

111121
config["debug_pytask"] = get_first_non_none_value(

src/_pytask/mark/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
)
4343
def markers(**config_from_cli):
4444
"""Show all registered markers."""
45+
config_from_cli["command"] = "markers"
46+
4547
try:
4648
# Duplication of the same mechanism in :func:`pytask.main.main`.
4749
pm = get_plugin_manager()
@@ -96,7 +98,9 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None:
9698
),
9799
]
98100
cli.commands["build"].params.extend(additional_build_parameters)
101+
cli.commands["clean"].params.extend(additional_build_parameters)
99102
cli.commands["collect"].params.extend(additional_build_parameters)
103+
100104
cli.add_command(markers)
101105

102106

tests/test_clean.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
@pytest.fixture()
88
def sample_project_path(tmp_path):
9+
"""Create a sample project to be cleaned."""
910
source = """
1011
import pytask
1112
@@ -24,6 +25,39 @@ def task_dummy(produces):
2425
return tmp_path
2526

2627

28+
@pytest.mark.end_to_end
29+
def test_clean_with_ignored_file(sample_project_path, runner):
30+
result = runner.invoke(
31+
cli, ["clean", "--ignore", "*_1.txt", sample_project_path.as_posix()]
32+
)
33+
34+
assert "to_be_deleted_file_1.txt" not in result.output
35+
assert "to_be_deleted_file_2.txt" in result.output
36+
37+
38+
@pytest.mark.end_to_end
39+
def test_clean_with_ingored_directory(sample_project_path, runner):
40+
result = runner.invoke(
41+
cli,
42+
[
43+
"clean",
44+
"--ignore",
45+
"to_be_deleted_folder_1/*",
46+
sample_project_path.as_posix(),
47+
],
48+
)
49+
50+
assert "to_be_deleted_folder_1/" not in result.output
51+
assert "to_be_deleted_file_1.txt" in result.output
52+
53+
54+
@pytest.mark.end_to_end
55+
def test_clean_with_nothing_to_remove(tmp_path, runner):
56+
result = runner.invoke(cli, ["clean", "--ignore", "*", tmp_path.as_posix()])
57+
58+
assert "There are no files and directories which can be deleted." in result.output
59+
60+
2761
@pytest.mark.end_to_end
2862
def test_clean_dry_run(sample_project_path, runner):
2963
result = runner.invoke(cli, ["clean", sample_project_path.as_posix()])

tests/test_config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010

1111
_IGNORED_FOLDERS = [i.split("/")[0] for i in IGNORED_FOLDERS]
12-
_IGNORED_FOLDERS.remove("*.egg-info")
1312

1413

1514
@pytest.mark.unit

0 commit comments

Comments
 (0)