• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

collective / zpretty / 21162633589

20 Jan 2026 07:10AM UTC coverage: 96.831% (+1.0%) from 95.794%
21162633589

push

github

1100 of 1136 relevant lines covered (96.83%)

4.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.8
/zpretty/cli.py
1
from argparse import ArgumentParser
5✔
2
from importlib.metadata import version
5✔
3
from os.path import splitext
5✔
4
from pathlib import Path
5✔
5
from sys import stderr
5✔
6
from sys import stdout
5✔
7
from zpretty.prettifier import ZPrettifier
5✔
8
from zpretty.xml import XMLPrettifier
5✔
9
from zpretty.zcml import ZCMLPrettifier
5✔
10

11
import re
5✔
12

13

14
version = version("zpretty")
5✔
15

16

17
class CLIRunner:
5✔
18
    """A class to run zpretty from the command line"""
19

20
    _default_include = r"\.(html|pt|xml|zcml)$"
5✔
21
    _default_exclude = (
5✔
22
        r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|"
23
        r"\.svn|\.ipynb_checkpoints|_build|buck-out|build|dist|__pypackages__)/"
24
    )
25

26
    def __init__(self):
5✔
27
        self.errors = []
×
28
        self.config = self.parser.parse_args()
×
29

30
    @property
5✔
31
    def parser(self):
5✔
32
        """The parser we are using to parse the command line arguments"""
33
        parser = ArgumentParser(
5✔
34
            prog="zpretty",
35
            description="An opinionated HTML/XML soup formatter",
36
            epilog=f"The default exclude pattern is: `{self._default_exclude}`",
37
        )
38
        parser.add_argument(
5✔
39
            "--encoding",
40
            help="The file encoding (defaults to utf8)",
41
            action="store",
42
            dest="encoding",
43
            default="utf8",
44
        )
45
        parser.add_argument(
5✔
46
            "-i",
47
            "--inplace",
48
            help="Format files in place (overwrite existing file)",
49
            action="store_true",
50
            dest="inplace",
51
            default=False,
52
        )
53
        parser.add_argument(
5✔
54
            "-v",
55
            "--version",
56
            help="Show zpretty version number",
57
            action="version",
58
            version=f"zpretty {version}",
59
        )
60
        parser.add_argument(
5✔
61
            "-x",
62
            "--xml",
63
            help="Treat the input file(s) as XML",
64
            action="store_true",
65
            dest="xml",
66
            default=False,
67
        )
68
        parser.add_argument(
5✔
69
            "-z",
70
            "--zcml",
71
            help="Treat the input file(s) as XML. Follow the ZCML styleguide",
72
            action="store_true",
73
            dest="zcml",
74
            default=False,
75
        )
76
        parser.add_argument(
5✔
77
            "--check",
78
            help=(
79
                "Return code 0 if nothing would be changed, "
80
                "1 if some files would be reformatted"
81
            ),
82
            action="store_true",
83
            dest="check",
84
            default=False,
85
        )
86
        parser.add_argument(
5✔
87
            "--include",
88
            help=(
89
                f"A regular expression that matches files and "
90
                f" directories that should be included on recursive searches. "
91
                f"An empty value means all files are included regardless of the name. "
92
                f"Use forward slashes for directories on all platforms (Windows, too). "
93
                f"Exclusions are calculated first, inclusions later. "
94
                f"[default: {self._default_include}]"
95
            ),
96
            action="store",
97
            dest="include",
98
            default=self._default_include,
99
        )
100
        parser.add_argument(
5✔
101
            "--exclude",
102
            help=(
103
                f"A regular expression that matches files and "
104
                f"directories that should be excluded on "
105
                f"recursive searches. An empty value means no "
106
                f"paths are excluded. Use forward slashes for "
107
                f"directories on all platforms (Windows, too). "
108
                f"Exclusions are calculated first, inclusions "
109
                f"later. [default: {self._default_exclude}] "
110
            ),
111
            action="store",
112
            dest="exclude",
113
            default=self._default_exclude,
114
        )
115

116
        parser.add_argument(
5✔
117
            "--extend-exclude",
118
            help=(
119
                "Like --exclude,  but adds additional files "
120
                "and directories on top of the excluded ones. "
121
                "(Useful if you simply want to add to the default)"
122
            ),
123
            action="store",
124
            dest="extend_exclude",
125
            default=None,
126
        )
127
        parser.add_argument(
5✔
128
            "paths",
129
            nargs="*",
130
            default="-",
131
            help="The list of files or directory to prettify (defaults to stdin). "
132
            "If a directory is passed, all files and directories matching the regular "
133
            "expression passed to --include will be prettified.",
134
        )
135
        return parser
5✔
136

137
    def choose_prettifier(self, path):
5✔
138
        """Choose the best prettifier given the config and the input file"""
139
        config = self.config
5✔
140
        if config.zcml:
5✔
141
            return ZCMLPrettifier
5✔
142
        if config.xml:
5✔
143
            return XMLPrettifier
5✔
144
        ext = splitext(path)[-1].lower()
5✔
145
        if ext == ".xml":
5✔
146
            return XMLPrettifier
5✔
147
        if ext == ".zcml":
5✔
148
            return ZCMLPrettifier
5✔
149
        return ZPrettifier
5✔
150

151
    @property
5✔
152
    def good_paths(self):
5✔
153
        """Return a list of good paths"""
154
        good_paths = []
5✔
155

156
        try:
5✔
157
            exclude = re.compile(self.config.exclude)
5✔
158
        except re.error:
5✔
159
            exclude = re.compile(self._default_exclude)
5✔
160
            self.errors.append(
5✔
161
                f"Invalid regular expression for --exclude: {self.config.exclude!r}"
162
            )
163

164
        try:
5✔
165
            extend_exclude = self.config.extend_exclude and re.compile(
5✔
166
                self.config.extend_exclude
167
            )
168
        except re.error:
5✔
169
            extend_exclude = None
5✔
170
            self.errors.append(
5✔
171
                f"Invalid regular expression for --extend-exclude: "
172
                f"{self.config.extend_exclude!r}"
173
            )
174

175
        try:
5✔
176
            include = re.compile(self.config.include)
5✔
177
        except re.error:
5✔
178
            include = re.compile(self._default_include)
5✔
179
            self.errors.append(
5✔
180
                f"Invalid regular expression for --include: {self.config.include!r}"
181
            )
182

183
        for path in self.config.paths:
5✔
184
            # use Pathlib to check if the file exists and it is a file
185
            if path == "-":
5✔
186
                good_paths.append(path)
5✔
187
                continue
5✔
188
            if exclude.match(path) or (extend_exclude and extend_exclude.match(path)):
5✔
189
                continue
×
190

191
            path_instance = Path(path)
5✔
192
            if path_instance.is_file():
5✔
193
                good_paths.append(path)
5✔
194
            elif path_instance.is_dir():
5✔
195
                for file in path_instance.glob("**/*"):
5✔
196
                    if file.is_file():
5✔
197
                        if (
5✔
198
                            include.search(str(file))
199
                            and not exclude.search(str(file))
200
                            and not (
201
                                extend_exclude and extend_exclude.search(str(file))
202
                            )
203
                        ):
204
                            good_paths.append(str(file))
5✔
205
            else:
206
                self.errors.append(f"Cannot open: {path}")
5✔
207

208
        return sorted(good_paths)
5✔
209

210
    def run(self):
5✔
211
        """Prettify each filename passed in the command line"""
212
        encoding = self.config.encoding
5✔
213
        for path in self.good_paths:
5✔
214
            # use Pathlib to check if the file exists and it is a file
215
            Prettifier = self.choose_prettifier(path)
5✔
216
            prettifier = Prettifier(path, encoding=encoding)
5✔
217
            if self.config.check:
5✔
218
                if not prettifier.check():
5✔
219
                    self.errors.append(f"This file would be rewritten: {path}")
×
220
                continue
5✔
221
            prettified = prettifier()
×
222
            if self.config.inplace and not path == "-":
×
223
                with open(path, "w") as f:
×
224
                    f.write(prettified)
×
225
                continue
×
226
            stdout.write(prettified)
×
227

228
        if self.errors:
5✔
229
            message = "\n".join(self.errors)
5✔
230
            stderr.write(f"{message}\n")
5✔
231
            exit(1)
5✔
232

233

234
def run():
5✔
235
    CLIRunner().run()  # pragma: no cover
236

237

238
if __name__ == "__main__":
5✔
239
    run()  # pragma: no cover
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc