zpretty 3.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. zpretty/__init__.py +0 -0
  2. zpretty/attributes.py +261 -0
  3. zpretty/cli.py +247 -0
  4. zpretty/elements.py +401 -0
  5. zpretty/prettifier.py +146 -0
  6. zpretty/tests/__init__.py +0 -0
  7. zpretty/tests/broken/broken.xml +2 -0
  8. zpretty/tests/include-exclude/.venv/bar.html +0 -0
  9. zpretty/tests/include-exclude/.venv/bar.pt +0 -0
  10. zpretty/tests/include-exclude/.venv/bar.txt +0 -0
  11. zpretty/tests/include-exclude/.venv/bar.xml +0 -0
  12. zpretty/tests/include-exclude/.venv/bar.zcml +0 -0
  13. zpretty/tests/include-exclude/.venv/foo.html +0 -0
  14. zpretty/tests/include-exclude/.venv/foo.pt +0 -0
  15. zpretty/tests/include-exclude/.venv/foo.txt +0 -0
  16. zpretty/tests/include-exclude/.venv/foo.xml +0 -0
  17. zpretty/tests/include-exclude/.venv/foo.zcml +0 -0
  18. zpretty/tests/include-exclude/foo/bar/bar.html +0 -0
  19. zpretty/tests/include-exclude/foo/bar/bar.pt +0 -0
  20. zpretty/tests/include-exclude/foo/bar/bar.txt +0 -0
  21. zpretty/tests/include-exclude/foo/bar/bar.xml +0 -0
  22. zpretty/tests/include-exclude/foo/bar/bar.zcml +0 -0
  23. zpretty/tests/include-exclude/foo/bar/foo.html +0 -0
  24. zpretty/tests/include-exclude/foo/bar/foo.pt +0 -0
  25. zpretty/tests/include-exclude/foo/bar/foo.txt +0 -0
  26. zpretty/tests/include-exclude/foo/bar/foo.xml +0 -0
  27. zpretty/tests/include-exclude/foo/bar/foo.zcml +0 -0
  28. zpretty/tests/include-exclude/venv/bar.html +0 -0
  29. zpretty/tests/include-exclude/venv/bar.pt +0 -0
  30. zpretty/tests/include-exclude/venv/bar.txt +0 -0
  31. zpretty/tests/include-exclude/venv/bar.xml +0 -0
  32. zpretty/tests/include-exclude/venv/bar.zcml +0 -0
  33. zpretty/tests/include-exclude/venv/foo.html +0 -0
  34. zpretty/tests/include-exclude/venv/foo.pt +0 -0
  35. zpretty/tests/include-exclude/venv/foo.txt +0 -0
  36. zpretty/tests/include-exclude/venv/foo.xml +0 -0
  37. zpretty/tests/include-exclude/venv/foo.zcml +0 -0
  38. zpretty/tests/mock.py +7 -0
  39. zpretty/tests/original/sample.txt +1 -0
  40. zpretty/tests/original/sample.zcml +66 -0
  41. zpretty/tests/original/sample_dtml.dtml +10 -0
  42. zpretty/tests/original/sample_html.html +102 -0
  43. zpretty/tests/original/sample_html4.html +102 -0
  44. zpretty/tests/original/sample_html_with_preprocessing_instruction.html +17 -0
  45. zpretty/tests/original/sample_pt.pt +52 -0
  46. zpretty/tests/original/sample_xml.xml +39 -0
  47. zpretty/tests/original/text_with_markup.md +12 -0
  48. zpretty/tests/test_attributes.py +79 -0
  49. zpretty/tests/test_cli.py +221 -0
  50. zpretty/tests/test_elements.py +110 -0
  51. zpretty/tests/test_functions.py +27 -0
  52. zpretty/tests/test_readme.py +59 -0
  53. zpretty/tests/test_xml.py +53 -0
  54. zpretty/tests/test_zcml.py +244 -0
  55. zpretty/tests/test_zpretty.py +221 -0
  56. zpretty/text.py +40 -0
  57. zpretty/xml.py +90 -0
  58. zpretty/zcml.py +580 -0
  59. zpretty-3.1.1.dist-info/METADATA +386 -0
  60. zpretty-3.1.1.dist-info/RECORD +64 -0
  61. zpretty-3.1.1.dist-info/WHEEL +5 -0
  62. zpretty-3.1.1.dist-info/entry_points.txt +2 -0
  63. zpretty-3.1.1.dist-info/licenses/LICENSE +24 -0
  64. zpretty-3.1.1.dist-info/top_level.txt +1 -0
zpretty/__init__.py ADDED
File without changes
zpretty/attributes.py ADDED
@@ -0,0 +1,261 @@
1
+ from logging import getLogger
2
+
3
+
4
+ try:
5
+ from html import escape
6
+ except ImportError: # pragma: no cover
7
+ # Python < 3.8
8
+ from cgi import escape
9
+
10
+
11
+ logger = getLogger(__name__)
12
+
13
+
14
+ class PrettyAttributes(object):
15
+ """Render attributes in a pretty way.
16
+
17
+ - one per line
18
+ - sorted semantically and alphabetically
19
+ - with properly indented and escaped values
20
+ """
21
+
22
+ _attribute_template = "{name}={quote}{value}{quote}"
23
+ _boolean_attributes_are_allowed = True
24
+
25
+ _known_boolean_attributes = (
26
+ "allowfullscreen",
27
+ "allowpaymentrequest",
28
+ "async",
29
+ "autofocus",
30
+ "autoplay",
31
+ "checked",
32
+ "controls",
33
+ "default",
34
+ "disabled",
35
+ "formnovalidate",
36
+ "hidden",
37
+ "ismap",
38
+ "itemscope",
39
+ "loop",
40
+ "multiple",
41
+ "muted",
42
+ "nomodule",
43
+ "novalidate",
44
+ "open",
45
+ "playsinline",
46
+ "readonly",
47
+ "required",
48
+ "reversed",
49
+ "selected",
50
+ "truespeed",
51
+ )
52
+ _multiline_prefix = " "
53
+ _multiline_attributes = ()
54
+ _tal_multiline_attributes = (
55
+ "attributes",
56
+ "define",
57
+ "tal:attributes",
58
+ "tal:define",
59
+ )
60
+
61
+ _tal_attribute_order = (
62
+ "tal:define",
63
+ "tal:switch",
64
+ "tal:condition",
65
+ "tal:repeat",
66
+ "tal:case",
67
+ "tal:content",
68
+ "tal:replace",
69
+ "tal:omit-tag",
70
+ "tal:attributes",
71
+ "tal:on-error",
72
+ )
73
+
74
+ _i18n_attributes = (
75
+ "i18n:translate",
76
+ "i18n:domain",
77
+ "i18n:context",
78
+ "i18n:source",
79
+ "i18n:target",
80
+ "i18n:name",
81
+ "i18n:attributes",
82
+ "i18n:data",
83
+ "i18n:comment",
84
+ "i18n:ignore",
85
+ "i18n:ignore-attributes",
86
+ )
87
+
88
+ def __init__(self, attributes, element=None):
89
+ """attributes is a dict like object"""
90
+ self.attributes = attributes
91
+ self.element = element
92
+
93
+ def __len__(self):
94
+ return len(self.attributes)
95
+
96
+ @property
97
+ def prefix(self):
98
+ """Return the prefix for the attributes
99
+
100
+ The returned value will be a number of spaces equal to the tag name length + 2,
101
+ e.g., in this case it will be 8
102
+ (6 for foobar + 2 (the leading < and the space after foobar)):
103
+ <foobar foo="1"
104
+ ________bar="2"
105
+ />
106
+ """
107
+ if not self.element:
108
+ return " "
109
+ return " " * (len(self.element.tag or "") + 2)
110
+
111
+ def sort_attributes(self, name):
112
+ """This sorts the attribute trying to group them semantically
113
+
114
+ Starting from the top:
115
+
116
+ 1. xml namespaces
117
+ 2. class, id
118
+ 3. attributes not belonging in to other categories (default)
119
+ 4. data- attributes
120
+ 5. tal attributes
121
+ 6. i18n attributes
122
+ """
123
+ if name.startswith("xmlns"):
124
+ return (0, name)
125
+ if name in ("class", "id"):
126
+ return (100, name)
127
+ if name.startswith("data"):
128
+ return (300, name)
129
+ if "tal:" + name in self._tal_attribute_order:
130
+ tal_index = self._tal_attribute_order.index("tal:" + name)
131
+ return (400 + tal_index, name)
132
+ if name in self._tal_attribute_order:
133
+ tal_index = self._tal_attribute_order.index(name)
134
+ return (400 + tal_index, name)
135
+ if name in self._i18n_attributes:
136
+ return (900, name)
137
+ return (200, name)
138
+
139
+ def format_multiline(self, name, value):
140
+ """"""
141
+ value_lines = filter(None, value.split())
142
+ line_joiner = "\n" + (" " * (len(name) + 2))
143
+ return line_joiner.join(value_lines)
144
+
145
+ def format_tal_multiline(self, value):
146
+ """There are some tal specific attributes that contain ; separated
147
+ statements.
148
+ They are used to define variables or set other attributes.
149
+ You can define many variables by adding statements separated by ';'.
150
+ If the statement contains a ';', it will be escaped as ';;'.
151
+
152
+ It is convenient to always have those attribute in the form:
153
+
154
+ tal:define="
155
+ var1 statement1;
156
+ var2 statement2;
157
+ ...
158
+ "
159
+ """
160
+ # temp skip ';;' the escape sequence to enter a ';' in a statement
161
+ statements = value.replace(";;", "<>").split(";")
162
+
163
+ # We always want an empty line first...
164
+ lines = [""]
165
+ try:
166
+ line_prefix = self.element.prefix + self.prefix + self._multiline_prefix
167
+ except AttributeError:
168
+ line_prefix = self._multiline_prefix
169
+
170
+ for statement in statements:
171
+ statement = statement.strip()
172
+ if statement:
173
+ if not statement.endswith(";"):
174
+ statement += ";"
175
+ lines.append(line_prefix + statement)
176
+ # ... and at the end dedent
177
+ lines.append(line_prefix[:-2])
178
+
179
+ new_value = "\n".join(lines)
180
+ # restore ';;'
181
+ return new_value.replace("<>", ";;")
182
+
183
+ def is_tal_attribute(self, name):
184
+ """Check if the attribute is a tal attribute"""
185
+ if name.startswith("tal:"):
186
+ return True
187
+ try:
188
+ if not self.element.context.name.startswith("tal:"):
189
+ return False
190
+ except AttributeError:
191
+ return False
192
+ if f"tal:{name}" in self._tal_attribute_order:
193
+ return True
194
+
195
+ def maybe_escape(self, name, value):
196
+ """Escape the value if needed"""
197
+ if self.is_tal_attribute(name):
198
+ # Never escape what we have in tal attributes
199
+ return value
200
+
201
+ return escape(value, quote=False)
202
+
203
+ def can_be_valueless(self, name):
204
+ """Check if the attribute name can be without a value"""
205
+ if not self._boolean_attributes_are_allowed:
206
+ return False
207
+ if name.startswith("data-"):
208
+ return True
209
+ if name in self._known_boolean_attributes:
210
+ return True
211
+ return False
212
+
213
+ def lines(self):
214
+ """Take the attributes, sort them and prettify their values"""
215
+ attributes = self.attributes
216
+ sorted_names = sorted(attributes, key=self.sort_attributes)
217
+ lines = []
218
+ for name in sorted_names:
219
+ value = attributes[name]
220
+ if isinstance(value, list):
221
+ # Happens, e.g., for the class attribute
222
+ value = " ".join(value)
223
+ if name in self._multiline_attributes:
224
+ value = self.format_multiline(name, value)
225
+ elif name in self._tal_multiline_attributes:
226
+ value = self.format_tal_multiline(value)
227
+ if not value and self.can_be_valueless(name):
228
+ line = name
229
+ else:
230
+ if '"' in value:
231
+ quote = "'"
232
+ else:
233
+ quote = '"'
234
+
235
+ line = self._attribute_template.format(
236
+ name=name, quote=quote, value=self.maybe_escape(name, value)
237
+ )
238
+ lines.append(line)
239
+ return lines
240
+
241
+ def lstrip(self):
242
+ """This returns the attributes with the left spaces removed"""
243
+ return self().lstrip()
244
+
245
+ def __call__(self):
246
+ """Render the attributes as text
247
+
248
+ Render and an empty string if no attributes
249
+ If we have one attribute we do not indent it
250
+ If we have many we indent them
251
+ """
252
+ if len(self) == 0:
253
+ return ""
254
+ if len(self) == 1:
255
+ for line in self.lines():
256
+ return line
257
+ if self.element:
258
+ prefix = self.element.prefix + self.prefix
259
+ else:
260
+ prefix = ""
261
+ return prefix + f"\n{prefix}".join(self.lines())
zpretty/cli.py ADDED
@@ -0,0 +1,247 @@
1
+ from argparse import ArgumentParser
2
+ from os.path import splitext
3
+ from pathlib import Path
4
+ from sys import stderr
5
+ from sys import stdout
6
+ from zpretty.prettifier import ZPrettifier
7
+ from zpretty.xml import XMLPrettifier
8
+ from zpretty.zcml import ZCMLPrettifier
9
+
10
+ import re
11
+
12
+
13
+ try:
14
+ # Python >= 3.8
15
+ from importlib.metadata import version
16
+
17
+ version = version("zpretty")
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:
26
+ """A class to run zpretty from the command line"""
27
+
28
+ _default_include = r"\.(html|pt|xml|zcml)$"
29
+ _default_exclude = (
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):
35
+ self.errors = []
36
+ self.config = self.parser.parse_args()
37
+
38
+ @property
39
+ def parser(self):
40
+ """The parser we are using to parse the command line arguments"""
41
+ parser = ArgumentParser(
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(
47
+ "--encoding",
48
+ help="The file encoding (defaults to utf8)",
49
+ action="store",
50
+ dest="encoding",
51
+ default="utf8",
52
+ )
53
+ parser.add_argument(
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(
62
+ "-v",
63
+ "--version",
64
+ help="Show zpretty version number",
65
+ action="version",
66
+ version=f"zpretty {version}",
67
+ )
68
+ parser.add_argument(
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(
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(
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(
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(
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(
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(
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
144
+
145
+ def choose_prettifier(self, path):
146
+ """Choose the best prettifier given the config and the input file"""
147
+ config = self.config
148
+ if config.zcml:
149
+ return ZCMLPrettifier
150
+ if config.xml:
151
+ return XMLPrettifier
152
+ ext = splitext(path)[-1].lower()
153
+ if ext == ".xml":
154
+ return XMLPrettifier
155
+ if ext == ".zcml":
156
+ return ZCMLPrettifier
157
+ return ZPrettifier
158
+
159
+ @property
160
+ def good_paths(self):
161
+ """Return a list of good paths"""
162
+ good_paths = []
163
+
164
+ try:
165
+ exclude = re.compile(self.config.exclude)
166
+ except re.error:
167
+ exclude = re.compile(self._default_exclude)
168
+ self.errors.append(
169
+ f"Invalid regular expression for --exclude: {self.config.exclude!r}"
170
+ )
171
+
172
+ try:
173
+ extend_exclude = self.config.extend_exclude and re.compile(
174
+ self.config.extend_exclude
175
+ )
176
+ except re.error:
177
+ extend_exclude = None
178
+ self.errors.append(
179
+ f"Invalid regular expression for --extend-exclude: "
180
+ f"{self.config.extend_exclude!r}"
181
+ )
182
+
183
+ try:
184
+ include = re.compile(self.config.include)
185
+ except re.error:
186
+ include = re.compile(self._default_include)
187
+ self.errors.append(
188
+ f"Invalid regular expression for --include: {self.config.include!r}"
189
+ )
190
+
191
+ for path in self.config.paths:
192
+ # use Pathlib to check if the file exists and it is a file
193
+ if path == "-":
194
+ good_paths.append(path)
195
+ continue
196
+ if exclude.match(path) or (extend_exclude and extend_exclude.match(path)):
197
+ continue
198
+
199
+ path_instance = Path(path)
200
+ if path_instance.is_file():
201
+ good_paths.append(path)
202
+ elif path_instance.is_dir():
203
+ for file in path_instance.glob("**/*"):
204
+ if file.is_file():
205
+ if (
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))
213
+ else:
214
+ self.errors.append(f"Cannot open: {path}")
215
+
216
+ return sorted(good_paths)
217
+
218
+ def run(self):
219
+ """Prettify each filename passed in the command line"""
220
+ encoding = self.config.encoding
221
+ for path in self.good_paths:
222
+ # use Pathlib to check if the file exists and it is a file
223
+ Prettifier = self.choose_prettifier(path)
224
+ prettifier = Prettifier(path, encoding=encoding)
225
+ if self.config.check:
226
+ if not prettifier.check():
227
+ self.errors.append(f"This file would be rewritten: {path}")
228
+ continue
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:
237
+ message = "\n".join(self.errors)
238
+ stderr.write(f"{message}\n")
239
+ exit(1)
240
+
241
+
242
+ def run():
243
+ CLIRunner().run() # pragma: no cover
244
+
245
+
246
+ if __name__ == "__main__":
247
+ run() # pragma: no cover