maxml 1.0.0__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.
- maxml/__init__.py +2 -0
- maxml/element/__init__.py +663 -0
- maxml/logging/__init__.py +3 -0
- maxml/namespace/__init__.py +126 -0
- maxml/version.txt +1 -0
- maxml-1.0.0.dist-info/METADATA +330 -0
- maxml-1.0.0.dist-info/RECORD +10 -0
- maxml-1.0.0.dist-info/WHEEL +5 -0
- maxml-1.0.0.dist-info/top_level.txt +1 -0
- maxml-1.0.0.dist-info/zip-safe +1 -0
maxml/__init__.py
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from maxml.namespace import Namespace
|
|
4
|
+
from maxml.logging import logger
|
|
5
|
+
|
|
6
|
+
from hybridmethod import hybridmethod
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logger.getChild(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Element(object):
|
|
13
|
+
"""The Element class represents an XML element"""
|
|
14
|
+
|
|
15
|
+
_name: str = None
|
|
16
|
+
_prefix: str = None
|
|
17
|
+
_namespaces: set[Namespace] = set()
|
|
18
|
+
_attributes: dict[str, str] = None
|
|
19
|
+
_text: str = None
|
|
20
|
+
_parent: Element = None
|
|
21
|
+
_children: list[Element] = None
|
|
22
|
+
_mixed: bool = False
|
|
23
|
+
|
|
24
|
+
@hybridmethod
|
|
25
|
+
def register_namespace(self, prefix: str, uri: str):
|
|
26
|
+
"""Supports registering namespaces globally for the module or per instance
|
|
27
|
+
depending on whether the method is called on the class directly or whether it is
|
|
28
|
+
called on a specific instance of the class.
|
|
29
|
+
|
|
30
|
+
If a namespace is registered globally for the module, the registered namespaces
|
|
31
|
+
become available for use by any instance of the class created within the program
|
|
32
|
+
after that point. This is especially useful for widely used XML namespaces which
|
|
33
|
+
obviates the need to re-register these widely used namespaces for each instance.
|
|
34
|
+
|
|
35
|
+
If there are namespaces which are specific to a document that is being created
|
|
36
|
+
and that won't be used elsewhere in the program, then those namespaces can be
|
|
37
|
+
registered on the specific class instance within which they will be used without
|
|
38
|
+
affecting the global list of registered namespaces.
|
|
39
|
+
|
|
40
|
+
Each namespace consists of a prefix which can be used to prefix element names
|
|
41
|
+
and the URI associated with that namespace prefix.
|
|
42
|
+
|
|
43
|
+
For example, the 'rdf' prefix is associated with the following canonical URI:
|
|
44
|
+
"http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
45
|
+
|
|
46
|
+
This would be registered globally by calling:
|
|
47
|
+
|
|
48
|
+
Element.register_namespace(
|
|
49
|
+
prefix="rdf",
|
|
50
|
+
uri="http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
Or this would be registered on a specific instance of the class by calling:
|
|
54
|
+
|
|
55
|
+
instance.register_namespace(
|
|
56
|
+
prefix="rdf",
|
|
57
|
+
uri="http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
Where 'instance' was the variable referencing the desired instance of the class.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
logger.debug("%s.register_namespace(prefix: %s, uri: %s)", self, prefix, uri)
|
|
64
|
+
|
|
65
|
+
if not isinstance(prefix, str):
|
|
66
|
+
raise TypeError("The 'prefix' argument must have a string value!")
|
|
67
|
+
|
|
68
|
+
if not isinstance(uri, str):
|
|
69
|
+
raise TypeError("The 'uri' argument must have a string value!")
|
|
70
|
+
|
|
71
|
+
for namespace in self._namespaces:
|
|
72
|
+
if namespace.prefix == prefix:
|
|
73
|
+
if namespace.uri == uri:
|
|
74
|
+
logger.warning(
|
|
75
|
+
" >>> The '%s' namespace has already been registered..."
|
|
76
|
+
% (prefix)
|
|
77
|
+
)
|
|
78
|
+
break
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"The '%s' namespace has already been registered with a different URI!"
|
|
82
|
+
% (prefix)
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
if namespace := Namespace(prefix=prefix, uri=uri):
|
|
86
|
+
self._namespaces.add(namespace)
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
name: str,
|
|
91
|
+
text: str = None,
|
|
92
|
+
namespace: Namespace | str = None,
|
|
93
|
+
mixed: bool = True,
|
|
94
|
+
parent: Element = None,
|
|
95
|
+
**attributes,
|
|
96
|
+
):
|
|
97
|
+
"""Support initializing a new XML element with a required element name, optional
|
|
98
|
+
text content, an optional parent element reference (which must be set for child
|
|
99
|
+
nodes) and an optional namespace definition provided either as a Namespace class
|
|
100
|
+
instance containing the matching prefix and associated namespace URI or as a URI
|
|
101
|
+
given as a string which corresponds to the prefix used in the element's name."""
|
|
102
|
+
|
|
103
|
+
if not isinstance(name, str):
|
|
104
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
105
|
+
elif len(name := name.strip()) == 0:
|
|
106
|
+
raise ValueError("The 'name' argument must have a non-empty string value!")
|
|
107
|
+
|
|
108
|
+
if not ":" in name:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"The 'name' argument must contain a ':' separator character between the namespace and tag name!"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
prefix: str = None
|
|
114
|
+
|
|
115
|
+
if ":" in name:
|
|
116
|
+
(prefix, basename) = name.split(":", maxsplit=1)
|
|
117
|
+
|
|
118
|
+
self._prefix: str = prefix
|
|
119
|
+
|
|
120
|
+
self._name: str = basename
|
|
121
|
+
|
|
122
|
+
if parent is None:
|
|
123
|
+
pass
|
|
124
|
+
elif isinstance(parent, Element):
|
|
125
|
+
self._parent: Element = parent
|
|
126
|
+
else:
|
|
127
|
+
raise TypeError(
|
|
128
|
+
"The 'parent' argument must reference an Element class instance!"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if text is None:
|
|
132
|
+
pass
|
|
133
|
+
elif isinstance(text, str):
|
|
134
|
+
self._text: str = text
|
|
135
|
+
else:
|
|
136
|
+
raise TypeError(
|
|
137
|
+
"The 'text' argument, if specified, must have a string value!"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self._namespaces: set[Namespace] = set()
|
|
141
|
+
|
|
142
|
+
if namespace is None:
|
|
143
|
+
for namespace in self.__class__._namespaces:
|
|
144
|
+
if namespace.prefix == prefix:
|
|
145
|
+
break
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"No namespace has been registered for the '{prefix}' prefix associated with the '{name}' element!"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
self._namespaces.add(namespace.copy().promote())
|
|
152
|
+
elif isinstance(uri := namespace, str):
|
|
153
|
+
for namespace in self.__class__._namespaces:
|
|
154
|
+
if namespace.uri == uri:
|
|
155
|
+
break
|
|
156
|
+
else:
|
|
157
|
+
namespace = Namespace(prefix=prefix, uri=uri)
|
|
158
|
+
|
|
159
|
+
self.__class__._namespaces.add(namespace)
|
|
160
|
+
|
|
161
|
+
self._namespaces.add(namespace.copy().promote())
|
|
162
|
+
elif isinstance(namespace, Namespace):
|
|
163
|
+
self.__class__._namespaces.add(namespace)
|
|
164
|
+
|
|
165
|
+
self._namespaces.add(namespace.copy().promote())
|
|
166
|
+
else:
|
|
167
|
+
raise TypeError(
|
|
168
|
+
"The 'namespace' argument does not contain a valid namespace!"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self._attributes: dict[str, str] = attributes or {}
|
|
172
|
+
|
|
173
|
+
self._children: list[Element] = []
|
|
174
|
+
|
|
175
|
+
if isinstance(mixed, bool):
|
|
176
|
+
self._mixed = mixed
|
|
177
|
+
else:
|
|
178
|
+
raise TypeError("The 'mixed' argument must have a boolean value!")
|
|
179
|
+
|
|
180
|
+
logger.debug(
|
|
181
|
+
"%s.__init__(name: %s, text: %s, parent: %s, namespace: %s, kwargs: %s) => depth %d",
|
|
182
|
+
self.__class__.__name__,
|
|
183
|
+
name,
|
|
184
|
+
text,
|
|
185
|
+
parent,
|
|
186
|
+
namespace,
|
|
187
|
+
attributes,
|
|
188
|
+
self.depth,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def depth(self) -> int:
|
|
193
|
+
"""Return the depth of the current Element where the root Element has a depth of
|
|
194
|
+
zero (0) and all nested Elements have a depth that increases by one (1) for each
|
|
195
|
+
level of nesting between the root node and the current Element node."""
|
|
196
|
+
|
|
197
|
+
depth: int = 0
|
|
198
|
+
|
|
199
|
+
if self.parent:
|
|
200
|
+
depth = 1 + self.parent.depth
|
|
201
|
+
else:
|
|
202
|
+
depth = 0
|
|
203
|
+
|
|
204
|
+
return depth
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def prefix(self) -> str | None:
|
|
208
|
+
"""Return the current Element node's namespace prefix."""
|
|
209
|
+
|
|
210
|
+
return self._prefix
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def name(self) -> str:
|
|
214
|
+
"""Return the current Element node's name without its namespace prefix."""
|
|
215
|
+
|
|
216
|
+
return self._name
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def fullname(self) -> str:
|
|
220
|
+
"""Return the current Element node's full name, combining the elements namespace
|
|
221
|
+
prefix and name."""
|
|
222
|
+
|
|
223
|
+
if self._prefix:
|
|
224
|
+
return f"{self._prefix}:{self._name}"
|
|
225
|
+
else:
|
|
226
|
+
return self._name
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def namespace(self) -> Namespace | None:
|
|
230
|
+
"""Return the current Element node's namespace, if the Element has a prefix and
|
|
231
|
+
if a matching namespace has been registered prior to or at the time the Element
|
|
232
|
+
was created. If no matching namespace can be found, None will be returned."""
|
|
233
|
+
|
|
234
|
+
namespace: Namespace = None
|
|
235
|
+
|
|
236
|
+
if self._prefix:
|
|
237
|
+
for namespace in self.__class__._namespaces:
|
|
238
|
+
if namespace.prefix == self._prefix:
|
|
239
|
+
break
|
|
240
|
+
else:
|
|
241
|
+
namespace = None
|
|
242
|
+
|
|
243
|
+
logger.debug(
|
|
244
|
+
"%s.namespace(%s) => %s",
|
|
245
|
+
self.__class__.__name__,
|
|
246
|
+
self.fullname,
|
|
247
|
+
namespace.prefix if namespace else "?",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return namespace
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def namespaces(self) -> set[Namespace]:
|
|
254
|
+
"""Return the namespaces associated with the current Element and its parent"""
|
|
255
|
+
|
|
256
|
+
namespaces: set[Namespace] = set(self._namespaces)
|
|
257
|
+
|
|
258
|
+
logger.debug(
|
|
259
|
+
"%s.namespaces(%s) => %s",
|
|
260
|
+
self.__class__.__name__,
|
|
261
|
+
self.fullname,
|
|
262
|
+
[n.prefix for n in namespaces],
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if self.parent:
|
|
266
|
+
for namespace in self.parent.namespaces:
|
|
267
|
+
namespaces.add(namespace)
|
|
268
|
+
|
|
269
|
+
if namespace := self.namespace:
|
|
270
|
+
namespaces.add(namespace)
|
|
271
|
+
|
|
272
|
+
logger.debug(
|
|
273
|
+
"%s.namespaces(%s) => %s",
|
|
274
|
+
self.__class__.__name__,
|
|
275
|
+
self.fullname,
|
|
276
|
+
[n.prefix for n in namespaces],
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return namespaces
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def namespaced(self) -> set[Namespace]:
|
|
283
|
+
"""Return the namespaces associated with the current Element and its children"""
|
|
284
|
+
|
|
285
|
+
namespaces: list[Namespace] = set()
|
|
286
|
+
|
|
287
|
+
inherited: set[Namespace] = self.parent.namespaces if self.parent else set()
|
|
288
|
+
|
|
289
|
+
# logger.debug("%s.namespaced(%s) => inherited => %s", self.__class__.__name__, self.fullname, [n.prefix for n in inherited])
|
|
290
|
+
|
|
291
|
+
if namespace := self.namespace:
|
|
292
|
+
if self.parent is None:
|
|
293
|
+
namespaces.add(namespace)
|
|
294
|
+
elif not namespace in inherited:
|
|
295
|
+
namespaces.add(namespace)
|
|
296
|
+
|
|
297
|
+
if self.depth > 0:
|
|
298
|
+
for child in self.children:
|
|
299
|
+
if namespace := child.namespace:
|
|
300
|
+
if self.parent is None or not namespace in inherited:
|
|
301
|
+
namespaces.add(namespace)
|
|
302
|
+
|
|
303
|
+
# logger.debug("%s.namespaced(%s) => current => %s", self.__class__.__name__, self.fullname, [n.prefix for n in namespaces])
|
|
304
|
+
|
|
305
|
+
return namespaces
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def parent(self) -> Element | None:
|
|
309
|
+
"""Return current Element node's parent Element or None if its the root node."""
|
|
310
|
+
|
|
311
|
+
return self._parent
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def root(self) -> Element:
|
|
315
|
+
"""Return the root Element of the tree, callable from any other Element node."""
|
|
316
|
+
|
|
317
|
+
if self.parent is None:
|
|
318
|
+
return self
|
|
319
|
+
|
|
320
|
+
return self.parent.root
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def children(self) -> list[Element]:
|
|
324
|
+
"""Return a copy of the list of children associated with the current Element."""
|
|
325
|
+
|
|
326
|
+
return list(self._children)
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def empty(self) -> bool:
|
|
330
|
+
"""Determine if the current Element is considered empty or not, as determined by
|
|
331
|
+
the absence of any children, returning True for Elements without children."""
|
|
332
|
+
|
|
333
|
+
return len(self._children) == 0
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def attributes(self) -> dict[str, str]:
|
|
337
|
+
"""Return a copy of the dictionary of attributes held by the current Element."""
|
|
338
|
+
|
|
339
|
+
return dict(self._attributes)
|
|
340
|
+
|
|
341
|
+
def set(self, name: str, value: object) -> Element:
|
|
342
|
+
"""Supports setting a named attribute value on the current Element; if the named
|
|
343
|
+
attribute already exists, its value will be overwritten."""
|
|
344
|
+
|
|
345
|
+
if not isinstance(name, str):
|
|
346
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
347
|
+
|
|
348
|
+
if not (isinstance(value, object) and hasattr(value, "__str__")):
|
|
349
|
+
raise TypeError(
|
|
350
|
+
"The 'value' argument must have a value that can be cast to a string!"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
self._attributes[name] = value
|
|
354
|
+
|
|
355
|
+
return self
|
|
356
|
+
|
|
357
|
+
def get(self, name: str, default: object = None) -> object | None:
|
|
358
|
+
"""Supports getting the value of a named attribute on the Element if the named
|
|
359
|
+
attribute exists, returning the optional default value if the named attributes
|
|
360
|
+
does not exist or returning None otherwise."""
|
|
361
|
+
|
|
362
|
+
if not isinstance(name, str):
|
|
363
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
364
|
+
|
|
365
|
+
if name in self._attributes:
|
|
366
|
+
return self._attributes[name]
|
|
367
|
+
|
|
368
|
+
return default
|
|
369
|
+
|
|
370
|
+
def unset(self, name: str) -> Element:
|
|
371
|
+
"""Supports unsetting a named attribute on the Element."""
|
|
372
|
+
|
|
373
|
+
if not isinstance(name, str):
|
|
374
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
375
|
+
|
|
376
|
+
if name in self._attributes:
|
|
377
|
+
del self._attributes[name]
|
|
378
|
+
|
|
379
|
+
return self
|
|
380
|
+
|
|
381
|
+
def subelement(self, name: str, **kwargs) -> Element:
|
|
382
|
+
"""Create a child Element under the current Element."""
|
|
383
|
+
|
|
384
|
+
if not isinstance(name, str):
|
|
385
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
386
|
+
|
|
387
|
+
element = Element(name=name, parent=self, **kwargs)
|
|
388
|
+
|
|
389
|
+
self._children.append(element)
|
|
390
|
+
|
|
391
|
+
if self.parent:
|
|
392
|
+
if namespace := element.namespace:
|
|
393
|
+
self._namespaces.add(namespace)
|
|
394
|
+
|
|
395
|
+
return element
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def text(self) -> str | None:
|
|
399
|
+
"""Return the text value of the current Element if present or None otherwise."""
|
|
400
|
+
|
|
401
|
+
return self._text
|
|
402
|
+
|
|
403
|
+
@text.setter
|
|
404
|
+
def text(self, text: str):
|
|
405
|
+
"""Supports setting the text property of the current Element by assigning a
|
|
406
|
+
string value, or nullifying the text value if needed by assinging None."""
|
|
407
|
+
|
|
408
|
+
if text is None:
|
|
409
|
+
self._text = None
|
|
410
|
+
elif isinstance(text, str):
|
|
411
|
+
self._text = text
|
|
412
|
+
else:
|
|
413
|
+
raise TypeError(
|
|
414
|
+
"The 'text' property must be assigned to a string value or None!"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def mixed(self) -> bool:
|
|
419
|
+
"""Return whether mixed content mode is enabled or not; by default it is."""
|
|
420
|
+
|
|
421
|
+
return self._mixed
|
|
422
|
+
|
|
423
|
+
def _parse_path(self, path: str) -> list[str]:
|
|
424
|
+
"""Supports parsing the search path into its consistuent parts"""
|
|
425
|
+
|
|
426
|
+
if not isinstance(path, str):
|
|
427
|
+
raise TypeError("The 'path' argument must have a string value!")
|
|
428
|
+
|
|
429
|
+
# The root node can be referenced via '$', '.' or '//' and to allow removal of
|
|
430
|
+
# a potentially trailing '/' we first need to replace '//' with '.'
|
|
431
|
+
if path == "//":
|
|
432
|
+
path = "."
|
|
433
|
+
|
|
434
|
+
# Remove any potentially trailing '/' from the search path so that it doesn't
|
|
435
|
+
# result in an additional empty search path component when splitting below
|
|
436
|
+
path = path.rstrip("/")
|
|
437
|
+
|
|
438
|
+
# The '//' prefix can also be used to indicate the root node
|
|
439
|
+
if path.startswith("//"):
|
|
440
|
+
path = "." + path[2:]
|
|
441
|
+
|
|
442
|
+
# The '$' prefix can also be used to indicate the root node
|
|
443
|
+
elif path.startswith("$"):
|
|
444
|
+
path = "." + path[1:]
|
|
445
|
+
|
|
446
|
+
return path.split("/")
|
|
447
|
+
|
|
448
|
+
def find(self, path: str) -> Element | None:
|
|
449
|
+
"""Supports finding the matching element nested within the current Element as
|
|
450
|
+
specified by the path which consists of one or more prefixed names and optional
|
|
451
|
+
wildcard characters."""
|
|
452
|
+
|
|
453
|
+
if not isinstance(path, str):
|
|
454
|
+
raise TypeError("The 'path' argument must have a string value!")
|
|
455
|
+
|
|
456
|
+
current: Element = self
|
|
457
|
+
|
|
458
|
+
if (count := len(parts := self._parse_path(path))) > 0:
|
|
459
|
+
root: Element = self.root
|
|
460
|
+
|
|
461
|
+
found: bool = False
|
|
462
|
+
|
|
463
|
+
for index, part in enumerate(parts, start=1):
|
|
464
|
+
# print(index, part)
|
|
465
|
+
|
|
466
|
+
if part == ".":
|
|
467
|
+
# print(f" >>> matched root ({part}) => {root.fullname}")
|
|
468
|
+
current = root
|
|
469
|
+
|
|
470
|
+
if count == index:
|
|
471
|
+
found = True
|
|
472
|
+
break
|
|
473
|
+
else:
|
|
474
|
+
# print(f" >>> checking children for ({part})")
|
|
475
|
+
|
|
476
|
+
for child in current.children:
|
|
477
|
+
# print(f" >>> checking child ({child.fullname})")
|
|
478
|
+
|
|
479
|
+
if part == "*" or child.fullname == part:
|
|
480
|
+
# print(f" >>> matched child ({part})")
|
|
481
|
+
|
|
482
|
+
current = child
|
|
483
|
+
|
|
484
|
+
if count == index:
|
|
485
|
+
found = True
|
|
486
|
+
break
|
|
487
|
+
|
|
488
|
+
if found is False:
|
|
489
|
+
current = None
|
|
490
|
+
|
|
491
|
+
return current
|
|
492
|
+
|
|
493
|
+
def findall(self, path: str) -> list[Element]:
|
|
494
|
+
"""Supports finding the matching elements nested within the current Element as
|
|
495
|
+
specified by the path which consists of one or more prefixed names and optional
|
|
496
|
+
wildcard characters."""
|
|
497
|
+
|
|
498
|
+
if not isinstance(path, str):
|
|
499
|
+
raise TypeError("The 'path' argument must have a string value!")
|
|
500
|
+
|
|
501
|
+
found: list[Element] = []
|
|
502
|
+
|
|
503
|
+
current: Element = self
|
|
504
|
+
|
|
505
|
+
if (count := len(parts := self._parse_path(path))) > 0:
|
|
506
|
+
root: Element = self.root
|
|
507
|
+
|
|
508
|
+
for index, part in enumerate(parts, start=1):
|
|
509
|
+
# print(index, part)
|
|
510
|
+
|
|
511
|
+
if part == ".":
|
|
512
|
+
current = root
|
|
513
|
+
|
|
514
|
+
if index == count:
|
|
515
|
+
found.append(child)
|
|
516
|
+
else:
|
|
517
|
+
for child in current.children:
|
|
518
|
+
if part == "*" or child.fullname == part:
|
|
519
|
+
current = child
|
|
520
|
+
|
|
521
|
+
if index == count:
|
|
522
|
+
found.append(child)
|
|
523
|
+
|
|
524
|
+
return found
|
|
525
|
+
|
|
526
|
+
def tostring(
|
|
527
|
+
self,
|
|
528
|
+
pretty: bool = False,
|
|
529
|
+
indent: str | int = None,
|
|
530
|
+
encoding: str = None,
|
|
531
|
+
**kwargs,
|
|
532
|
+
) -> str | bytes:
|
|
533
|
+
"""Supports serializing the current Element tree to a string or to a bytes array
|
|
534
|
+
if a string encoding, such as 'UTF-8', is specified."""
|
|
535
|
+
|
|
536
|
+
if not isinstance(pretty, bool):
|
|
537
|
+
raise TypeError("The 'pretty' argument must have a boolean value!")
|
|
538
|
+
|
|
539
|
+
if indent is None:
|
|
540
|
+
indent = 2
|
|
541
|
+
|
|
542
|
+
if isinstance(indent, (int, str)):
|
|
543
|
+
if isinstance(indent, int) and indent > 0:
|
|
544
|
+
indent = " " * indent
|
|
545
|
+
elif isinstance(indent, str) and len(indent) > 0:
|
|
546
|
+
pass
|
|
547
|
+
else:
|
|
548
|
+
indent = ""
|
|
549
|
+
else:
|
|
550
|
+
raise TypeError(
|
|
551
|
+
"The 'indent' argument, if specified, must have an integer or string value!"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if encoding is None:
|
|
555
|
+
pass
|
|
556
|
+
elif not isinstance(encoding, str):
|
|
557
|
+
raise TypeError("The 'encoding' argument must have a string value!")
|
|
558
|
+
|
|
559
|
+
def stringify(
|
|
560
|
+
element: Element,
|
|
561
|
+
depth: int,
|
|
562
|
+
pretty: bool = False,
|
|
563
|
+
indent: str = None,
|
|
564
|
+
**kwargs,
|
|
565
|
+
) -> str:
|
|
566
|
+
"""Convert the current XML element to a serialized XML string, recursively
|
|
567
|
+
iterating downward through the tree and all of its children."""
|
|
568
|
+
|
|
569
|
+
if indent is None:
|
|
570
|
+
indent = ""
|
|
571
|
+
|
|
572
|
+
newline: bool = False
|
|
573
|
+
|
|
574
|
+
# Begin the element tag
|
|
575
|
+
string: str = f"<{element.fullname}"
|
|
576
|
+
|
|
577
|
+
# Add any promoted namespaces (those which should proceed any attributes)
|
|
578
|
+
count = len(element.namespaced)
|
|
579
|
+
for index, namespace in enumerate(element.namespaced, start=1):
|
|
580
|
+
if not namespace.promoted:
|
|
581
|
+
continue
|
|
582
|
+
|
|
583
|
+
if pretty and count > 1 and (newline or (index > 1 and index <= count)):
|
|
584
|
+
string += f"\n{indent * (depth + 2)}"
|
|
585
|
+
|
|
586
|
+
string += f' xmlns:{namespace.prefix}="{namespace.uri}"'
|
|
587
|
+
|
|
588
|
+
# Add any attributes
|
|
589
|
+
count = len(element.attributes)
|
|
590
|
+
for index, (key, value) in enumerate(element.attributes.items(), start=1):
|
|
591
|
+
if pretty and count > 1 and index >= 1 and index <= count:
|
|
592
|
+
string += f"\n{indent * (depth + 2)}"
|
|
593
|
+
newline = True
|
|
594
|
+
|
|
595
|
+
string += f' {key}="{value}"'
|
|
596
|
+
|
|
597
|
+
# Add any non-promoted namespaces (those which can follow any attributes)
|
|
598
|
+
count = len(element.namespaced)
|
|
599
|
+
|
|
600
|
+
if count > 2:
|
|
601
|
+
newline = True
|
|
602
|
+
|
|
603
|
+
for index, namespace in enumerate(element.namespaced, start=1):
|
|
604
|
+
if namespace.promoted:
|
|
605
|
+
continue
|
|
606
|
+
|
|
607
|
+
if pretty and count > 1 and (newline or (index > 1 and index <= count)):
|
|
608
|
+
string += f"\n{indent * (depth + 2)}"
|
|
609
|
+
|
|
610
|
+
string += f' xmlns:{namespace.prefix}="{namespace.uri}"'
|
|
611
|
+
|
|
612
|
+
# Close an empty element tag if the element lacks text content and children
|
|
613
|
+
if element.text is None and element.empty is True:
|
|
614
|
+
string += f"/>"
|
|
615
|
+
|
|
616
|
+
# Otherwise include the element's optional text and children
|
|
617
|
+
else:
|
|
618
|
+
string += f">"
|
|
619
|
+
|
|
620
|
+
if not element.mixed and (element.text and element.children):
|
|
621
|
+
raise ValueError(
|
|
622
|
+
"The current XML element cannot be serialized as mixed content mode is not enabled, but both node text and children have been specified; please either enable mixed mode or adjust the element's node contents."
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Include the element's text content, if any
|
|
626
|
+
if element.text and (element.mixed or not element.children):
|
|
627
|
+
string += element.text
|
|
628
|
+
|
|
629
|
+
# Include the element's children, if any
|
|
630
|
+
if element.children and (element.mixed or not element.text):
|
|
631
|
+
for child in element.children:
|
|
632
|
+
if pretty:
|
|
633
|
+
string += f"\n{indent * (depth + 1)}"
|
|
634
|
+
|
|
635
|
+
string += stringify(
|
|
636
|
+
element=child,
|
|
637
|
+
depth=(depth + 1),
|
|
638
|
+
pretty=pretty,
|
|
639
|
+
indent=indent,
|
|
640
|
+
**kwargs,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
if pretty:
|
|
644
|
+
string += f"\n{indent * (depth)}"
|
|
645
|
+
|
|
646
|
+
string += f"</{element.fullname}>"
|
|
647
|
+
|
|
648
|
+
return string
|
|
649
|
+
|
|
650
|
+
string = stringify(
|
|
651
|
+
element=self,
|
|
652
|
+
depth=0,
|
|
653
|
+
pretty=pretty,
|
|
654
|
+
indent=indent,
|
|
655
|
+
**kwargs,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
string = string.strip()
|
|
659
|
+
|
|
660
|
+
if encoding:
|
|
661
|
+
string = string.encode(encoding)
|
|
662
|
+
|
|
663
|
+
return string
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import zlib
|
|
4
|
+
|
|
5
|
+
from maxml.logging import logger
|
|
6
|
+
|
|
7
|
+
logger = logger.getChild(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Namespace(object):
|
|
11
|
+
"""The Namespace class represents an XML namespace"""
|
|
12
|
+
|
|
13
|
+
_prefix: str = None
|
|
14
|
+
_uri: str = None
|
|
15
|
+
_promoted: bool = False
|
|
16
|
+
|
|
17
|
+
def __init__(self, prefix: str, uri: str):
|
|
18
|
+
"""Initialize the Namespace class"""
|
|
19
|
+
|
|
20
|
+
if not isinstance(prefix, str):
|
|
21
|
+
raise TypeError("The 'prefix' argument must have a string value!")
|
|
22
|
+
|
|
23
|
+
self._prefix = prefix
|
|
24
|
+
|
|
25
|
+
if not isinstance(uri, str):
|
|
26
|
+
raise TypeError("The 'uri' argument must have a string value!")
|
|
27
|
+
|
|
28
|
+
self._uri = uri
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
"""Return a string representation of the class for debugging purposes."""
|
|
32
|
+
|
|
33
|
+
return f"<{self.__class__.__name__}({self._prefix}:{self._uri})>"
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
"""Return a more detailed representation of the class for debugging purposes."""
|
|
37
|
+
|
|
38
|
+
return f"<{__name__}.{self.__class__.__name__}({self._prefix}:{self._uri}) object at 0x{hex(id(self))}>"
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def stableid(value: str):
|
|
42
|
+
"""Generate a stable id for a given string value; this is used with __hash__."""
|
|
43
|
+
|
|
44
|
+
if not isinstance(value, str):
|
|
45
|
+
raise TypeError("The 'value' argument must have a string value!")
|
|
46
|
+
|
|
47
|
+
return zlib.crc32(value.encode()) & 0xFFFFFFFF
|
|
48
|
+
|
|
49
|
+
def __hash__(self) -> int:
|
|
50
|
+
"""Generate a stable hash value for a Namespace with a given prefix and URI."""
|
|
51
|
+
|
|
52
|
+
return self.stableid(self._prefix) + self.stableid(self._uri)
|
|
53
|
+
|
|
54
|
+
def __eq__(self, other: Namespace) -> bool:
|
|
55
|
+
"""Support comparing Namespace instances by determining if the prefix and URI
|
|
56
|
+
match, allowing copies of Namespace instances to compare as equal even if they
|
|
57
|
+
are distinctly different copies in memory."""
|
|
58
|
+
|
|
59
|
+
logger.debug(
|
|
60
|
+
"%s.__eq__(self: %s, other: %s)",
|
|
61
|
+
self.__class__.__name__,
|
|
62
|
+
self,
|
|
63
|
+
other,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not isinstance(other, Namespace):
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
if self.prefix != other.prefix:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
if self.uri != other.uri:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def prefix(self) -> str:
|
|
79
|
+
"""Return the prefix held by the Namespace."""
|
|
80
|
+
|
|
81
|
+
return self._prefix
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def uri(self) -> str:
|
|
85
|
+
"""Return the URI held by the Namespace."""
|
|
86
|
+
|
|
87
|
+
return self._uri
|
|
88
|
+
|
|
89
|
+
def copy(self) -> Namespace:
|
|
90
|
+
"""Create an independent copy of the current Namespace instance."""
|
|
91
|
+
|
|
92
|
+
return Namespace(prefix=self.prefix, uri=self.uri)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def promoted(self) -> bool:
|
|
96
|
+
"""Return the promoted status of the Namespace instance as set or unset through
|
|
97
|
+
the 'promote' and 'unpromote' helper methods."""
|
|
98
|
+
|
|
99
|
+
return self._promoted
|
|
100
|
+
|
|
101
|
+
@promoted.setter
|
|
102
|
+
def promoted(self, promoted: bool):
|
|
103
|
+
"""Support setting the promoted value via the property accessor."""
|
|
104
|
+
|
|
105
|
+
if not isinstance(promoted, bool):
|
|
106
|
+
raise TypeError("The 'promoted' argument must have a boolean value!")
|
|
107
|
+
|
|
108
|
+
self._promoted = promoted
|
|
109
|
+
|
|
110
|
+
def promote(self) -> Namespace:
|
|
111
|
+
"""Mark the current Namespace instance as having been 'promoted' which allows
|
|
112
|
+
it to be listed before any attributes on the Element it is associated with; as
|
|
113
|
+
this method returns a reference to 'self' it may be chained with other calls."""
|
|
114
|
+
|
|
115
|
+
self._promoted = True
|
|
116
|
+
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def unpromote(self) -> Namespace:
|
|
120
|
+
"""Mark the current Namespace instance as having been 'unpromoted' preventing it
|
|
121
|
+
from being listed before any attributes on the Element it is associated with; as
|
|
122
|
+
this method returns a reference to 'self' it may be chained with other calls."""
|
|
123
|
+
|
|
124
|
+
self._promoted = False
|
|
125
|
+
|
|
126
|
+
return self
|
maxml/version.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maxml
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A streamlined pure Python XML serializer.
|
|
5
|
+
Author: Daniel Sissman
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: documentation, https://github.com/bluebinary/maxml/blob/main/README.md
|
|
8
|
+
Project-URL: changelog, https://github.com/bluebinary/maxml/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: repository, https://github.com/bluebinary/maxml
|
|
10
|
+
Project-URL: issues, https://github.com/bluebinary/maxml/issues
|
|
11
|
+
Project-URL: homepage, https://github.com/bluebinary/maxml
|
|
12
|
+
Keywords: xml,serialization,xml serialization,xml writer
|
|
13
|
+
Platform: any
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: hybridmethod==1.0.*
|
|
22
|
+
Provides-Extra: development
|
|
23
|
+
Requires-Dist: black==24.10.*; extra == "development"
|
|
24
|
+
Requires-Dist: pytest==8.3.*; extra == "development"
|
|
25
|
+
Requires-Dist: pytest-codeblocks==0.17.0; extra == "development"
|
|
26
|
+
Provides-Extra: distribution
|
|
27
|
+
Requires-Dist: build; extra == "distribution"
|
|
28
|
+
Requires-Dist: twine; extra == "distribution"
|
|
29
|
+
Requires-Dist: wheel; extra == "distribution"
|
|
30
|
+
|
|
31
|
+
# MaXML: A Pure Python XML Serializer
|
|
32
|
+
|
|
33
|
+
The MaXML library provides a streamlined pure Python XML serializer.
|
|
34
|
+
|
|
35
|
+
### Requirements
|
|
36
|
+
|
|
37
|
+
The MaXML library has been tested with Python 3.10, 3.11, 3.12 and 3.13. The library is
|
|
38
|
+
not compatible with Python 3.9 or earlier.
|
|
39
|
+
|
|
40
|
+
### Installation
|
|
41
|
+
|
|
42
|
+
The MaXML library is available from PyPI, so may be added to a project's dependencies
|
|
43
|
+
via its `requirements.txt` file or similar by referencing the MaXML library's name,
|
|
44
|
+
`maxml`, or the library may be installed directly into your local runtime environment
|
|
45
|
+
using `pip` via the `pip install` command by entering the following into your shell:
|
|
46
|
+
|
|
47
|
+
$ pip install maxml
|
|
48
|
+
|
|
49
|
+
### Example Usage
|
|
50
|
+
|
|
51
|
+
To use the MaXML library, import the library's and begin creating your XML document:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import maxml
|
|
55
|
+
|
|
56
|
+
root = maxml.Element(name="my:node", namespace="http://namespace.example.org/my")
|
|
57
|
+
|
|
58
|
+
child = root.subelement(name="my:child-node")
|
|
59
|
+
|
|
60
|
+
child.set("my-attribute", "my-attribute-value")
|
|
61
|
+
|
|
62
|
+
child.text = "testing"
|
|
63
|
+
|
|
64
|
+
root.tostring(pretty=True)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The above example will result in the following XML:
|
|
68
|
+
|
|
69
|
+
```xml
|
|
70
|
+
<my:node xmlns:my="http://namespace.example.org/my">
|
|
71
|
+
<my:child-node my-attribute="my-attribute-value">testing</my:child-node>
|
|
72
|
+
</my:node>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Methods & Properties
|
|
76
|
+
|
|
77
|
+
The MaXML library provides two main classes for use in creating and serializing XML, the
|
|
78
|
+
`Elements` class that is used to represent nodes in the XML document tree along with any
|
|
79
|
+
attributes those nodes have, their associated namespaces, any text content and children,
|
|
80
|
+
and the `Namespace` class for holding information about namespaces.
|
|
81
|
+
|
|
82
|
+
The classes and their methods and properties are listed below:
|
|
83
|
+
|
|
84
|
+
#### Element Class
|
|
85
|
+
|
|
86
|
+
The `Element` class constructor `Element(...)` takes the following arguments:
|
|
87
|
+
|
|
88
|
+
* `name` (`str`) – The required `name` argument sets the prefixed name of the element.
|
|
89
|
+
|
|
90
|
+
* `text` (`str`) – The optional `text` argument can be used to specify the text content
|
|
91
|
+
of the element; alternatively it can be set later via the `text` property.
|
|
92
|
+
|
|
93
|
+
* `namespace` (`Namespace` | `str`) – The optional `namespace` argument can be used to
|
|
94
|
+
specify the namespace for the element while it is being created; the namespace can
|
|
95
|
+
either be specified as the URI that corresponds with the prefix specified as part of
|
|
96
|
+
the element's name, or can be a reference to a `Namespace` class instance that holds
|
|
97
|
+
the corresponding `prefix` and matching `URI`. If the matching namespace has already
|
|
98
|
+
been registered before the element is created via the class' `register_element()`
|
|
99
|
+
method, then it is not necessary to specify a `namespace` argument when an element
|
|
100
|
+
that references that namespace (via its `name` prefix) is created.
|
|
101
|
+
|
|
102
|
+
* `mixed` (`bool`) – The optional `mixed` argument can be used to override the default
|
|
103
|
+
mixed-content mode of the element; by default each element allows mixed-content
|
|
104
|
+
which means that the element can contain both text content and children; if an
|
|
105
|
+
element should only be allowed to contain text content or children, then
|
|
106
|
+
`mixed` can be set to `False` during the construction of the element, which will
|
|
107
|
+
then prevent both content types from being serialized and will instead result in an
|
|
108
|
+
error being raised.
|
|
109
|
+
|
|
110
|
+
* `parent` (`Element`) – The optional `parent` property is used internally by the
|
|
111
|
+
library when sub-elements are created to set the appropriate parent reference; this
|
|
112
|
+
property should not be set manually unless one is conformable with the possible
|
|
113
|
+
side-effects that may occur.
|
|
114
|
+
|
|
115
|
+
The `Element` class provides the following methods:
|
|
116
|
+
|
|
117
|
+
* `register_namespace(prefix: str, uri: str)` – The `register_namespace()` method
|
|
118
|
+
supports registering namespaces globally for the module or per instance depending
|
|
119
|
+
on whether the method is called on the class directly or whether it is called on a
|
|
120
|
+
specific instance of the class.
|
|
121
|
+
|
|
122
|
+
If a namespace is registered globally for the module, the registered namespaces
|
|
123
|
+
become available for use by any instance of the class created within the program
|
|
124
|
+
after that point. This is especially useful for widely used XML namespaces which
|
|
125
|
+
obviates the need to re-register these widely used namespaces for each instance.
|
|
126
|
+
|
|
127
|
+
If there are namespaces which are specific to a document that is being created
|
|
128
|
+
and that won't be used elsewhere in the program, then those namespaces can be
|
|
129
|
+
registered on the specific class instance within which they will be used without
|
|
130
|
+
affecting the global list of registered namespaces.
|
|
131
|
+
|
|
132
|
+
Each namespace consists of a prefix which can be used to prefix element names
|
|
133
|
+
and the URI associated with that namespace prefix.
|
|
134
|
+
|
|
135
|
+
For example, the 'rdf' prefix is associated with the following canonical URI:
|
|
136
|
+
"http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
137
|
+
|
|
138
|
+
This would be registered globally by calling:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from maxml import Element
|
|
142
|
+
|
|
143
|
+
Element.register_namespace(
|
|
144
|
+
prefix="rdf",
|
|
145
|
+
uri="http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or this would be registered locally on an instance of the class before its use by a
|
|
150
|
+
sub-element by calling:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from maxml import Element
|
|
154
|
+
|
|
155
|
+
element = Element(name="my:test", namespace="http://namespace.example.org/my")
|
|
156
|
+
|
|
157
|
+
element.register_namespace(
|
|
158
|
+
prefix="rdf",
|
|
159
|
+
uri="http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Where `instance` was the variable referencing the desired instance of the class.
|
|
164
|
+
|
|
165
|
+
* `set(name: str, value: object)` (`Element`) – The `set()` supports setting a named
|
|
166
|
+
attribute value on the current element; if the named attribute already exists, its
|
|
167
|
+
value is overwritten. The `set()` method returns a reference to `self` so it may be
|
|
168
|
+
chained with other calls on the element.
|
|
169
|
+
|
|
170
|
+
* `get(name: str, default: object = None)` (`object` | `None`) – The `get()` supports
|
|
171
|
+
getting the value of a named attribute on the Element if the named attribute exists,
|
|
172
|
+
returning the optional `default` value if the named attributes does not exist or if
|
|
173
|
+
the `default` value has not been specified, returning `None` otherwise.
|
|
174
|
+
|
|
175
|
+
* `unset(name: str)` (`Element`) – The `set()` supports unsetting a named attribute on
|
|
176
|
+
the element. The `unset()` method returns a reference to `self` so it may be chained
|
|
177
|
+
with other calls on the element.
|
|
178
|
+
|
|
179
|
+
* `subelement(name: str, **kwargs)` (`Element`) – The `subelement()` method creates a
|
|
180
|
+
child element nested under the current element. It requires a `name` and optionally
|
|
181
|
+
accepts all of the other arguments that the `Element` class constructor accepts as
|
|
182
|
+
documented above. When a child element is created, its parent is automatically set
|
|
183
|
+
to the element it is nested under. The `subelement()` method returns a reference to
|
|
184
|
+
the newly created element, thus other calls on that element may be chained.
|
|
185
|
+
|
|
186
|
+
* `find(path: str)` (`Element` | `None`) – The `find()` method can be used to find the
|
|
187
|
+
matching element nested within the current element as specified by the element path
|
|
188
|
+
which consists of one or more prefixed names and optional wildcard characters.
|
|
189
|
+
|
|
190
|
+
The path to the element we wish to find can be specified from the root node of the
|
|
191
|
+
tree by starting the path with the "//" marker which indicates the root node, or the
|
|
192
|
+
path can be specified as a relative path by omitting this, which will result in the
|
|
193
|
+
search starting at the current element node. The path should be specified with the
|
|
194
|
+
name of the element at each level of the nesting that should be matched against to
|
|
195
|
+
reach the desired node; each element node name should be separated by a single "/"
|
|
196
|
+
character, and if any node name could be matched as part of the search the wildcard
|
|
197
|
+
character "*" can be used in place of an element node name.
|
|
198
|
+
|
|
199
|
+
* `findall(path: str)` (`list[Element]`) – The `findall()` method can be used to find
|
|
200
|
+
the matching elements nested within the current element as specified by the element
|
|
201
|
+
path which consists of one or more prefixed names and optional wildcard characters.
|
|
202
|
+
|
|
203
|
+
The `findall()` method uses the same search path format as the `find()` method
|
|
204
|
+
described above, the only difference being that if multiple elements are found at
|
|
205
|
+
the end of the search, all matching elements will be returned instead of the first
|
|
206
|
+
match found as is the case with the `find` method.
|
|
207
|
+
|
|
208
|
+
* `tostring(pretty: bool = False, indent: str | int = None, encoding: str = None)` –
|
|
209
|
+
The `tostring()` method supports serializing the current element tree to a string,
|
|
210
|
+
or to a bytes array if a string encoding, such as 'UTF-8' is specified.
|
|
211
|
+
|
|
212
|
+
To create a 'pretty' printed string, set the `pretty` argument to `True` and set an
|
|
213
|
+
optional indent, which by default is set to two spaces per level of indentation. To
|
|
214
|
+
set the indent level to 1 or more spaces, set `indent` to a positive integer value
|
|
215
|
+
of the number of spaces that should be used for the indentation per level of nesting
|
|
216
|
+
or to use a different whitespace character, such as a tab, set the `indent` value to
|
|
217
|
+
a tab character using the escape sequence for a tab, `"\t"`.
|
|
218
|
+
|
|
219
|
+
To have the method return an encoded `bytes` sequence instead of a unicode string,
|
|
220
|
+
set the optional `encoding` argument to a valid string encoding, such as `"UTF-8"`.
|
|
221
|
+
|
|
222
|
+
The `Element` class provides the following properties:
|
|
223
|
+
|
|
224
|
+
* `prefix` (`str`) – The `prefix` property returns the prefix portion of the element's
|
|
225
|
+
tag full name, for example `my` from `my:test`.
|
|
226
|
+
|
|
227
|
+
* `name` (`str`) – The `name` property getter returns the name portion of the element's
|
|
228
|
+
tag full name, for example `test` from `my:test`.
|
|
229
|
+
|
|
230
|
+
* `fullname` (`str`) – The `fullname` property returns the full name of the element's
|
|
231
|
+
tag, for example `my:test`.
|
|
232
|
+
|
|
233
|
+
* `namespace` (`Namespace`) – The `namespace` property returns the namespace associated
|
|
234
|
+
with the element, as either registered before the element was created, or created by
|
|
235
|
+
the process of creating the element if the optional `namespace` property was used.
|
|
236
|
+
|
|
237
|
+
* `depth` (`int`) – The `depth` property returns depth of the element in the tree.
|
|
238
|
+
|
|
239
|
+
* `parent` (`Element` | `None`) – The `parent` property returns the parent, if any, of
|
|
240
|
+
the element; the root node of the tree will not have a parent, while all other
|
|
241
|
+
elements will have an assigned parent, set automatically when a sub-element is made.
|
|
242
|
+
|
|
243
|
+
* `children` (`list[Element]`) – The `children` property returns the list of children
|
|
244
|
+
elements associated with the element, if any have been assigned.
|
|
245
|
+
|
|
246
|
+
* `attributes` (`dict[str, str]`) – The `attributes` property returns a dictionary of
|
|
247
|
+
the attributes associated with the element, if any have been assigned, where the key
|
|
248
|
+
of each entry is the name of the attribute and the value is its value.
|
|
249
|
+
|
|
250
|
+
* `text` (`str` | `None`) – The `text` property returns the text of the element if any
|
|
251
|
+
has been assigned or `None` otherwise.
|
|
252
|
+
|
|
253
|
+
* `mixed` (`bool`) – The `mixed` property returns the mixed-content status of the
|
|
254
|
+
element which determines whether the element can have both text content and children
|
|
255
|
+
or if at most it can only have one or the other. By default each element allows both
|
|
256
|
+
content types, but by setting the `mixed` argument on the `Element` constructor to
|
|
257
|
+
`False`, mixed-content mode will be turned-off.
|
|
258
|
+
|
|
259
|
+
* `root` (`Element`) – The `root` property returns the root element of the tree from
|
|
260
|
+
anywhere else within the tree.
|
|
261
|
+
|
|
262
|
+
* `namespaces` (`set[Namespace]`) – The `namespaces` property returns the full `set` of
|
|
263
|
+
namespaces associated with the element including any inherited from its parents; the
|
|
264
|
+
set can be used to inspect the associated namespaces, but its primary use is to help
|
|
265
|
+
facilitate the serialization of the document.
|
|
266
|
+
|
|
267
|
+
* `namespaced` (`set[Namespace]`) – The `namespaced` property returns the unique `set`
|
|
268
|
+
of namespaces associated with the element that have not already been referenced by
|
|
269
|
+
a parent node, thus ensuring only the newly referenced namespaces are introduced in
|
|
270
|
+
the serialized document rather than potentially repeated references to namespaces
|
|
271
|
+
which have already been referenced previously; the set can be used to inspect the
|
|
272
|
+
associated namespaces, but its primary use is to help facilitate the serialization.
|
|
273
|
+
|
|
274
|
+
#### Namespace Class
|
|
275
|
+
|
|
276
|
+
The `Namespace` class constructor `Namespace(...)` takes the following arguments:
|
|
277
|
+
|
|
278
|
+
* `prefix` (`str`) – The required `prefix` argument sets the namespace prefix.
|
|
279
|
+
* `uri` (`str`) – The required `uri` argument sets the namespace URI.
|
|
280
|
+
|
|
281
|
+
The `Namespace` class provides the following methods:
|
|
282
|
+
|
|
283
|
+
* `copy()` (`Namespace`) – The `copy()` method creates an independent copy of the
|
|
284
|
+
current Namespace instance and returns it.
|
|
285
|
+
|
|
286
|
+
* `promote()` (`Namespace`) – The `promote()` marks the current Namespace instance as
|
|
287
|
+
having been 'promoted' which allows it to be listed before any attributes on the
|
|
288
|
+
Element it is associated with; as this method returns a reference to `self` it may
|
|
289
|
+
be chained with other calls.
|
|
290
|
+
|
|
291
|
+
* `unpromote()` (`Namespace`) – The `unpromote()` marks the current Namespace instance
|
|
292
|
+
as having been 'un-promoted' preventing it from being listed before any attributes
|
|
293
|
+
on the Element it is associated with; as this method returns a reference to `self`
|
|
294
|
+
it may be chained with other calls.
|
|
295
|
+
|
|
296
|
+
The `Namespace` class provides the following properties:
|
|
297
|
+
|
|
298
|
+
* `prefix` (`str`) – The `prefix` property returns the prefix held by the namespace.
|
|
299
|
+
* `uri` (`str`) – The `uri` property returns the URI held by the namespace.
|
|
300
|
+
* `promoted` (`bool`) – The `promoted` property getter returns the promoted state of the
|
|
301
|
+
Namespace instance as set or unset through the `promote()` and `unpromote()` helper
|
|
302
|
+
methods or via the `promoted` property setter.
|
|
303
|
+
* `promoted` (`bool`) – The `promoted` property setter supports setting the `promoted`
|
|
304
|
+
property value via the property accessor.
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
### Unit Tests
|
|
308
|
+
|
|
309
|
+
The MaXML library includes a suite of comprehensive unit tests which ensure that the
|
|
310
|
+
library functionality operates as expected. The unit tests were developed with and are
|
|
311
|
+
run via `pytest`.
|
|
312
|
+
|
|
313
|
+
To ensure that the unit tests are run within a predictable runtime environment where all of the necessary dependencies are available, a [Docker](https://www.docker.com) image is created within which the tests are run. To run the unit tests, ensure Docker and Docker Compose is [installed](https://docs.docker.com/engine/install/), and perform the following commands, which will build the Docker image via `docker compose build` and then run the tests via `docker compose run` – the output of running the tests will be displayed:
|
|
314
|
+
|
|
315
|
+
```shell
|
|
316
|
+
$ docker compose build
|
|
317
|
+
$ docker compose run tests
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
To run the unit tests with optional command line arguments being passed to `pytest`, append the relevant arguments to the `docker compose run tests` command, as follows, for example passing `-vv` to enable verbose output:
|
|
321
|
+
|
|
322
|
+
```shell
|
|
323
|
+
$ docker compose run tests -vv
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
See the documentation for [PyTest](https://docs.pytest.org/en/latest/) regarding available optional command line arguments.
|
|
327
|
+
|
|
328
|
+
### Copyright & License Information
|
|
329
|
+
|
|
330
|
+
Copyright © 2025 Daniel Sissman; licensed under the MIT License.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
maxml/__init__.py,sha256=QuUB-oFrPKRZ4XzPuk4Mt8r35EMx652PALyAeWvQXIo,72
|
|
2
|
+
maxml/version.txt,sha256=klIfw8vZZL3J9YSpkbif3apXVO0cyW1tQkRTOGacEwU,5
|
|
3
|
+
maxml/element/__init__.py,sha256=jCrnThqnN6hACfmbsFZcY8MMV4czBCmoYOOpVpRjzDQ,22673
|
|
4
|
+
maxml/logging/__init__.py,sha256=AMpvZEQH9GIBe8609sMwb2T64rovpkRRCc9rs2kHy8g,52
|
|
5
|
+
maxml/namespace/__init__.py,sha256=iZqHWuGi09KtMTr7wHiyTQLXB2TFljUir8wZGLi2b60,3882
|
|
6
|
+
maxml-1.0.0.dist-info/METADATA,sha256=LW_yxkgZyiuT3fOmtnkiWKoEra0hHpvY7M-igpXXt6g,16164
|
|
7
|
+
maxml-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
maxml-1.0.0.dist-info/top_level.txt,sha256=ZFK3SmCc04Dzhl9QkO_hVBAEiHwCNrwn8X_PINWE9C4,6
|
|
9
|
+
maxml-1.0.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
10
|
+
maxml-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
maxml
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|