Skip to content

Commit 2dbdf3c

Browse files
committed
Add basic filter command as suggested in
Support for pre-commit hooks Fixes #19
1 parent c11b9a6 commit 2dbdf3c

File tree

7 files changed

+192
-8
lines changed

7 files changed

+192
-8
lines changed

.pre-commit-config.yaml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ repos:
1414
hooks:
1515
- id: isort
1616
name: Sort import
17-
entry: isort
17+
entry: dfetch
18+
args: ['filter', 'isort']
1819
language: system
1920
types: [file, python]
20-
exclude: ^doc/_ext/sphinxcontrib_asciinema
2121

2222
- id: black
2323
name: Black (auto-format)
24-
entry: black
24+
entry: dfetch
25+
args: ['filter', 'black']
2526
language: system
2627
types: [file, python]
27-
exclude: ^doc/_ext/sphinxcontrib_asciinema
2828

2929
- id: pylint
3030
name: pylint
@@ -101,9 +101,10 @@ repos:
101101
- id: codespell
102102
name: codespell
103103
description: Checks for common misspellings in text files.
104-
entry: codespell
104+
entry: dfetch
105+
args: ['filter', 'codespell']
105106
language: python
106-
exclude: ^doc/_ext/sphinxcontrib_asciinema/_static/asciinema-player_3.12.1.js
107+
# exclude: ^doc/_ext/sphinxcontrib_asciinema/_static/asciinema-player_3.12.1.js
107108
types: [text]
108109
- id: ruff
109110
name: ruff

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Release 0.11.0 (unreleased)
1515
* Handle SVN tags with special characters (#811)
1616
* Don't return non-zero exit code if tool not found during environment (#701)
1717
* Create standalone binaries for Linux, Mac & Windows (#705)
18+
* Add filter command (#19)
1819

1920
Release 0.10.0 (released 2025-03-12)
2021
====================================

dfetch/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import dfetch.commands.check
1111
import dfetch.commands.diff
1212
import dfetch.commands.environment
13+
import dfetch.commands.filter
1314
import dfetch.commands.freeze
1415
import dfetch.commands.import_
1516
import dfetch.commands.init
@@ -42,6 +43,7 @@ def create_parser() -> argparse.ArgumentParser:
4243
dfetch.commands.check.Check.create_menu(subparsers)
4344
dfetch.commands.diff.Diff.create_menu(subparsers)
4445
dfetch.commands.environment.Environment.create_menu(subparsers)
46+
dfetch.commands.filter.Filter.create_menu(subparsers)
4547
dfetch.commands.freeze.Freeze.create_menu(subparsers)
4648
dfetch.commands.import_.Import.create_menu(subparsers)
4749
dfetch.commands.init.Init.create_menu(subparsers)

dfetch/commands/filter.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""*Dfetch* can filter files in the repo.
2+
3+
It can either accept no input to list all files. A list of files can be piped in (such as through ``find``)
4+
or it can be used as a wrapper around a certain tool to block or allow files under control by dfetch.
5+
"""
6+
7+
import argparse
8+
import os
9+
import sys
10+
from pathlib import Path
11+
from typing import Optional
12+
13+
import dfetch.commands.command
14+
import dfetch.log
15+
import dfetch.manifest.manifest
16+
from dfetch.log import get_logger
17+
from dfetch.util.cmdline import run_on_cmdline_uncaptured
18+
from dfetch.util.util import in_directory
19+
20+
logger = get_logger(__name__)
21+
22+
23+
class Filter(dfetch.commands.command.Command):
24+
"""Filter files based on flags and pass on any command.
25+
26+
Based on the provided arguments filter files, and call the given arguments or print them out if no command given.
27+
"""
28+
29+
SILENT = True
30+
31+
@staticmethod
32+
def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None:
33+
"""Add the parser menu for this action."""
34+
parser = dfetch.commands.command.Command.parser(subparsers, Filter)
35+
parser.add_argument(
36+
"--in-manifest",
37+
"-i",
38+
action="store_true",
39+
default=False,
40+
help="Keep files that came here through the manifest.",
41+
)
42+
43+
parser.add_argument(
44+
"cmd",
45+
metavar="<cmd>",
46+
type=str,
47+
nargs="?",
48+
help="Command to call",
49+
)
50+
51+
parser.add_argument(
52+
"args",
53+
metavar="<args>",
54+
type=str,
55+
nargs="*",
56+
help="Arguments to pass to the command",
57+
)
58+
59+
def __call__(self, args: argparse.Namespace) -> None:
60+
"""Perform the filter."""
61+
if not args.verbose:
62+
dfetch.log.set_level("ERROR")
63+
manifest = dfetch.manifest.manifest.get_manifest()
64+
65+
pwd = Path.cwd()
66+
topdir = Path(manifest.path).parent
67+
with in_directory(topdir):
68+
69+
project_paths = {
70+
Path(project.destination).resolve() for project in manifest.projects
71+
}
72+
73+
input_list = self._determine_input_list(args)
74+
block_inside, block_outside = self._filter_files(
75+
pwd, topdir, project_paths, input_list
76+
)
77+
78+
blocklist = block_outside if args.in_manifest else block_inside
79+
80+
filtered_args = [arg for arg in input_list if arg not in blocklist]
81+
82+
if args.cmd:
83+
run_on_cmdline_uncaptured(logger, [args.cmd] + filtered_args)
84+
else:
85+
print(os.linesep.join(filtered_args))
86+
87+
def _determine_input_list(self, args: argparse.Namespace) -> list[str]:
88+
"""Determine list of inputs to process."""
89+
input_list: list[str] = list(str(arg) for arg in args.args)
90+
if not sys.stdin.isatty():
91+
input_list += list(str(arg).strip() for arg in sys.stdin.readlines())
92+
93+
# If no input from stdin or args loop over all files
94+
if not input_list:
95+
input_list = list(
96+
str(file) for file in Path(".").rglob("*") if file.is_file()
97+
)
98+
99+
return input_list
100+
101+
def _filter_files(
102+
self, pwd: Path, topdir: Path, project_paths: set[Path], input_list: list[str]
103+
) -> tuple[list[str], list[str]]:
104+
"""Filter files in input_set in files in one of the project_paths or not."""
105+
block_inside: list[str] = []
106+
block_outside: list[str] = []
107+
108+
for path_or_arg in input_list:
109+
arg_abs_path = Path(pwd / path_or_arg.strip()).resolve()
110+
if not arg_abs_path.exists():
111+
logger.print_info_line(path_or_arg.strip(), "not a file / dir")
112+
continue
113+
try:
114+
arg_abs_path.relative_to(topdir)
115+
except ValueError:
116+
logger.print_info_line(path_or_arg.strip(), "outside project")
117+
block_inside.append(path_or_arg)
118+
block_outside.append(path_or_arg)
119+
continue
120+
121+
containing_dir = self._file_in_project(arg_abs_path, project_paths)
122+
123+
if containing_dir:
124+
block_inside.append(path_or_arg)
125+
logger.print_info_line(
126+
path_or_arg.strip(), f"inside project ({containing_dir})"
127+
)
128+
else:
129+
block_outside.append(path_or_arg)
130+
logger.print_info_line(path_or_arg.strip(), "not inside any project")
131+
132+
return block_inside, block_outside
133+
134+
def _file_in_project(self, file: Path, project_paths: set[Path]) -> Optional[Path]:
135+
"""Check if a specific file is somewhere in one of the project paths."""
136+
for project_path in project_paths:
137+
try:
138+
file.relative_to(project_path)
139+
return project_path
140+
except ValueError:
141+
continue
142+
return None

dfetch/util/cmdline.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,34 @@ def run_on_cmdline(
6969
return proc
7070

7171

72+
def run_on_cmdline_uncaptured(
73+
logger: logging.Logger, cmd: Union[str, list[str]]
74+
) -> "subprocess.CompletedProcess[Any]":
75+
"""Run a command and log the output, and raise if something goes wrong."""
76+
logger.debug(f"Running {cmd}")
77+
78+
if not isinstance(cmd, list):
79+
cmd = cmd.split(" ")
80+
81+
try:
82+
proc = subprocess.run(cmd, capture_output=False, check=True) # nosec
83+
except subprocess.CalledProcessError as exc:
84+
raise SubprocessCommandError(
85+
exc.cmd,
86+
"",
87+
"",
88+
exc.returncode,
89+
) from exc
90+
except FileNotFoundError as exc:
91+
cmd = cmd[0]
92+
raise RuntimeError(f"{cmd} not available on system, please install") from exc
93+
94+
if proc.returncode:
95+
raise SubprocessCommandError(cmd, "", "", proc.returncode)
96+
97+
return proc
98+
99+
72100
def _log_output(proc: subprocess.CompletedProcess, logger: logging.Logger) -> None: # type: ignore
73101
logger.debug(f"Return code: {proc.returncode}")
74102

dfetch/util/util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ def safe_rmtree(path: str) -> None:
6363

6464

6565
@contextmanager
66-
def in_directory(path: str) -> Generator[str, None, None]:
66+
def in_directory(path: Union[str, Path]) -> Generator[str, None, None]:
6767
"""Work temporarily in a given directory."""
6868
pwd = os.getcwd()
6969
if not os.path.isdir(path):
7070
path = os.path.dirname(path)
7171
os.chdir(path)
7272
try:
73-
yield path
73+
yield str(path)
7474
finally:
7575
os.chdir(pwd)
7676

doc/manual.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,13 @@ Import
139139
.. asciinema:: asciicasts/import.cast
140140

141141
.. automodule:: dfetch.commands.import_
142+
143+
Filter
144+
------
145+
.. argparse::
146+
:module: dfetch.__main__
147+
:func: create_parser
148+
:prog: dfetch
149+
:path: filter
150+
151+
.. automodule:: dfetch.commands.filter

0 commit comments

Comments
 (0)