• 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 os.path import splitext
5✔
3
from pathlib import Path
5✔
4
from sys import stderr
5✔
5
from sys import stdout
5✔
6
from zpretty.prettifier import ZPrettifier
5✔
7
from zpretty.xml import XMLPrettifier
5✔
8
from zpretty.zcml import ZCMLPrettifier
5✔
9

10
import re
5✔
11

12

13
try:
5✔
14
    # Python >= 3.8
15
    from importlib.metadata import version
5✔
16

17
    version = version("zpretty")
5✔
18
except ImportError:
×
19
    # Python < 3.8
20
    from pkg_resources import get_distribution
×
21

22
    version = get_distribution("zpretty").version
×
23

24

25
class CLIRunner:
5✔
26
    """A class to run zpretty from the command line"""
27

28
    _default_include = r"\.(html|pt|xml|zcml)$"
5✔
29
    _default_exclude = (
5✔
30
        r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|"
31
        r"\.svn|\.ipynb_checkpoints|_build|buck-out|build|dist|__pypackages__)/"
32
    )
33

34
    def __init__(self):
5✔
35
        self.errors = []
×
36
        self.config = self.parser.parse_args()
×
37

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

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

145
    def choose_prettifier(self, path):
5✔
146
        """Choose the best prettifier given the config and the input file"""
147
        config = self.config
5✔
148
        if config.zcml:
5✔
149
            return ZCMLPrettifier
5✔
150
        if config.xml:
5✔
151
            return XMLPrettifier
5✔
152
        ext = splitext(path)[-1].lower()
5✔
153
        if ext == ".xml":
5✔
154
            return XMLPrettifier
5✔
155
        if ext == ".zcml":
5✔
156
            return ZCMLPrettifier
5✔
157
        return ZPrettifier
5✔
158

159
    @property
5✔
160
    def good_paths(self):
5✔
161
        """Return a list of good paths"""
162
        good_paths = []
5✔
163

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

172
        try:
5✔
173
            extend_exclude = self.config.extend_exclude and re.compile(
5✔
174
                self.config.extend_exclude
175
            )
176
        except re.error:
5✔
177
            extend_exclude = None
5✔
178
            self.errors.append(
5✔
179
                f"Invalid regular expression for --extend-exclude: "
180
                f"{self.config.extend_exclude!r}"
181
            )
182

183
        try:
5✔
184
            include = re.compile(self.config.include)
5✔
185
        except re.error:
5✔
186
            include = re.compile(self._default_include)
5✔
187
            self.errors.append(
5✔
188
                f"Invalid regular expression for --include: {self.config.include!r}"
189
            )
190

191
        for path in self.config.paths:
5✔
192
            # use Pathlib to check if the file exists and it is a file
193
            if path == "-":
5✔
194
                good_paths.append(path)
5✔
195
                continue
5✔
196
            if exclude.match(path) or (extend_exclude and extend_exclude.match(path)):
5✔
197
                continue
×
198

199
            path_instance = Path(path)
5✔
200
            if path_instance.is_file():
5✔
201
                good_paths.append(path)
5✔
202
            elif path_instance.is_dir():
5✔
203
                for file in path_instance.glob("**/*"):
5✔
204
                    if file.is_file():
5✔
205
                        if (
5✔
206
                            include.search(str(file))
207
                            and not exclude.search(str(file))
208
                            and not (
209
                                extend_exclude and extend_exclude.search(str(file))
210
                            )
211
                        ):
212
                            good_paths.append(str(file))
5✔
213
            else:
214
                self.errors.append(f"Cannot open: {path}")
5✔
215

216
        return sorted(good_paths)
5✔
217

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

236
        if self.errors:
5✔
237
            message = "\n".join(self.errors)
5✔
238
            stderr.write(f"{message}\n")
5✔
239
            exit(1)
5✔
240

241

242
def run():
5✔
243
    CLIRunner().run()  # pragma: no cover
244

245

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