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

collective / zpretty / 19173411475

07 Nov 2025 03:45PM UTC coverage: 95.794% (-1.1%) from 96.907%
19173411475

push

github

1116 of 1165 relevant lines covered (95.79%)

4.79 hits per line

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

87.25
/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
10

5✔
11
import re
12

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

15

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

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

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

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

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

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

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

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

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

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

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

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

207
        return sorted(good_paths)
208

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

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

×
232

×
233
def run():
×
234
    CLIRunner().run()  # pragma: no cover
×
235

236

5✔
237
if __name__ == "__main__":
5✔
238
    run()  # pragma: no cover
5✔
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