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

collective / zpretty / 27270382024

11 Jun 2026 08:47AM UTC coverage: 96.613%. First build
27270382024

Pull #230

github

Pull Request #230: fix: never silently emit or overwrite a file with truncated XML

59 of 66 new or added lines in 4 files covered. (89.39%)

1198 of 1240 relevant lines covered (96.61%)

4.83 hits per line

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

88.24
/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

5✔
11
import re
12

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

5✔
15

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

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

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

5✔
30
    @property
×
31
    def parser(self):
×
32
        """The parser we are using to parse the command line arguments"""
33
        parser = ArgumentParser(
5✔
34
            prog="zpretty",
5✔
35
            description="An opinionated HTML/XML soup formatter",
36
            epilog=f"The default exclude pattern is: `{self._default_exclude}`",
5✔
37
        )
38
        parser.add_argument(
39
            "--encoding",
40
            help="The file encoding (defaults to utf8)",
41
            action="store",
5✔
42
            dest="encoding",
43
            default="utf8",
44
        )
45
        parser.add_argument(
46
            "-i",
47
            "--inplace",
48
            help="Format files in place (overwrite existing file)",
5✔
49
            action="store_true",
50
            dest="inplace",
51
            default=False,
52
        )
53
        parser.add_argument(
54
            "-v",
55
            "--version",
56
            help="Show zpretty version number",
5✔
57
            action="version",
58
            version=f"zpretty {version}",
59
        )
60
        parser.add_argument(
61
            "-x",
62
            "--xml",
63
            help="Treat the input file(s) as XML",
5✔
64
            action="store_true",
65
            dest="xml",
66
            default=False,
67
        )
68
        parser.add_argument(
69
            "-z",
70
            "--zcml",
71
            help="Treat the input file(s) as XML. Follow the ZCML styleguide",
5✔
72
            action="store_true",
73
            dest="zcml",
74
            default=False,
75
        )
76
        parser.add_argument(
77
            "--check",
78
            help=(
79
                "Return code 0 if nothing would be changed, "
5✔
80
                "1 if some files would be reformatted"
81
            ),
82
            action="store_true",
83
            dest="check",
84
            default=False,
85
        )
86
        parser.add_argument(
87
            "--include",
88
            help=(
89
                f"A regular expression that matches files and "
5✔
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(
101
            "--exclude",
102
            help=(
103
                f"A regular expression that matches files and "
5✔
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(
117
            "--extend-exclude",
118
            help=(
119
                "Like --exclude,  but adds additional files "
5✔
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(
128
            "paths",
129
            nargs="*",
130
            default="-",
5✔
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
136

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

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

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

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

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

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

5✔
191
            path_instance = Path(path)
5✔
192
            if path_instance.is_file():
×
193
                good_paths.append(path)
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))
5✔
199
                            and not exclude.search(str(file))
5✔
200
                            and not (
5✔
201
                                extend_exclude and extend_exclude.search(str(file))
202
                            )
203
                        ):
204
                            good_paths.append(str(file))
205
            else:
206
                self.errors.append(f"Cannot open: {path}")
207

5✔
208
        return sorted(good_paths)
209

5✔
210
    def run(self):
211
        """Prettify each filename passed in the command line"""
5✔
212
        encoding = self.config.encoding
213
        for path in self.good_paths:
5✔
214
            # use Pathlib to check if the file exists and it is a file
5✔
215
            Prettifier = self.choose_prettifier(path)
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}")
5✔
220
                continue
5✔
221
            prettified = prettifier()
5✔
222
            if self.config.inplace and not path == "-":
5✔
223
                with open(path, "w") as f:
5✔
224
                    f.write(prettified)
5✔
NEW
225
                continue
×
NEW
226
            stdout.write(prettified)
×
227

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

5✔
233

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

5✔
237

5✔
238
if __name__ == "__main__":
5✔
NEW
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