maxml 1.0.1__tar.gz → 1.0.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxml
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: A streamlined pure Python XML serializer.
5
5
  Author: Daniel Sissman
6
6
  License-Expression: MIT
@@ -18,7 +18,8 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
- Requires-Dist: hybridmethod==1.0.*
21
+ Requires-Dist: classicist==1.0.*
22
+ Requires-Dist: enumerific==1.0.*
22
23
  Provides-Extra: development
23
24
  Requires-Dist: black==24.10.*; extra == "development"
24
25
  Requires-Dist: pytest==8.3.*; extra == "development"
@@ -48,7 +49,7 @@ using `pip` via the `pip install` command by entering the following into your sh
48
49
 
49
50
  ### Example Usage
50
51
 
51
- To use the MaXML library, import the library's and begin creating your XML document:
52
+ To use the MaXML library, import the library and begin creating your XML document:
52
53
 
53
54
  ```python
54
55
  import maxml
@@ -18,7 +18,7 @@ using `pip` via the `pip install` command by entering the following into your sh
18
18
 
19
19
  ### Example Usage
20
20
 
21
- To use the MaXML library, import the library's and begin creating your XML document:
21
+ To use the MaXML library, import the library and begin creating your XML document:
22
22
 
23
23
  ```python
24
24
  import maxml
@@ -1,2 +1,3 @@
1
1
  # MaXML Library: Runtime Dependencies
2
- hybridmethod==1.0.*
2
+ classicist==1.0.*
3
+ enumerific==1.0.*
@@ -0,0 +1,4 @@
1
+ from maxml.element import Element
2
+ from maxml.namespace import Namespace
3
+ from maxml.enumerations import Escape
4
+ from maxml.exceptions import MaXMLError
@@ -2,9 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  from maxml.namespace import Namespace
4
4
  from maxml.logging import logger
5
+ from maxml.enumerations import (
6
+ Context,
7
+ Escape,
8
+ )
9
+ from maxml.exceptions import MaXMLError
5
10
 
6
- from hybridmethod import hybridmethod
11
+ from classicist import hybridmethod
7
12
 
13
+ import re
8
14
 
9
15
  logger = logger.getChild(__name__)
10
16
 
@@ -71,15 +77,15 @@ class Element(object):
71
77
  for namespace in self._namespaces:
72
78
  if namespace.prefix == prefix:
73
79
  if namespace.uri == uri:
74
- logger.warning(
80
+ logger.info(
75
81
  " >>> The '%s' namespace has already been registered..."
76
82
  % (prefix)
77
83
  )
78
84
  break
79
85
  else:
80
- raise ValueError(
81
- "The '%s' namespace has already been registered with a different URI!"
82
- % (prefix)
86
+ raise MaXMLError(
87
+ "The '%s' namespace, %s, has already been registered with a different URI: %s!"
88
+ % (prefix, uri, namespace.uri)
83
89
  )
84
90
  else:
85
91
  if namespace := Namespace(prefix=prefix, uri=uri):
@@ -170,7 +176,7 @@ class Element(object):
170
176
 
171
177
  break
172
178
  else:
173
- raise ValueError(
179
+ raise MaXMLError(
174
180
  f"No namespace has been registered for the '{prefix}' prefix associated with the '{name}' element!"
175
181
  )
176
182
 
@@ -557,6 +563,7 @@ class Element(object):
557
563
  pretty: bool = False,
558
564
  indent: str | int = None,
559
565
  encoding: str = None,
566
+ escape: Escape = Escape.All,
560
567
  **kwargs,
561
568
  ) -> str | bytes:
562
569
  """Supports serializing the current Element tree to a string or to a bytes array
@@ -565,6 +572,11 @@ class Element(object):
565
572
  if not isinstance(pretty, bool):
566
573
  raise TypeError("The 'pretty' argument must have a boolean value!")
567
574
 
575
+ if not isinstance(escape, Escape):
576
+ raise TypeError(
577
+ "The 'escape' argument must reference an Escape enumeration option!"
578
+ )
579
+
568
580
  if indent is None:
569
581
  indent = 2
570
582
 
@@ -585,10 +597,54 @@ class Element(object):
585
597
  elif not isinstance(encoding, str):
586
598
  raise TypeError("The 'encoding' argument must have a string value!")
587
599
 
600
+ def escaper(value: str, context: Context, escape: Escape) -> str:
601
+ """Helper method to escape special characters in XML attributes and text."""
602
+
603
+ if isinstance(value, str):
604
+ pass
605
+ elif hasattr(value, "__str__"):
606
+ value = str(value)
607
+ else:
608
+ raise TypeError(
609
+ "The 'value' argument must have a string value or a value that can be cast to a string!"
610
+ )
611
+
612
+ replacements: dict[str, str] = {
613
+ "&": "&",
614
+ "<": "&lt;",
615
+ ">": "&gt;",
616
+ '"': "&quot;",
617
+ "'": "&apos;",
618
+ }
619
+
620
+ if context is Context.Attribute:
621
+ # Required replacements for element attribute values
622
+ required: list[str] = ["&", "<", '"']
623
+ elif context is Context.Text:
624
+ # Required replacements for element text
625
+ required: list[str] = ["&", "<"]
626
+ else:
627
+ # Require all replacements for other contexts
628
+ required: list[str] = [key for key in replacements]
629
+
630
+ for search, replacement in replacements.items():
631
+ if (escape is Escape.All) or (search in required):
632
+ if search == "&":
633
+ # Ensure only standalone "&" characters are replaced, ignoring
634
+ # any that are part of an XML special character escape sequence
635
+ value = re.sub(
636
+ r"&(?!(([a-z]+|#x?[0-9a-fA-F]+);))", replacement, value
637
+ )
638
+ else:
639
+ value = value.replace(search, replacement)
640
+
641
+ return value
642
+
588
643
  def stringify(
589
644
  element: Element,
590
645
  depth: int,
591
646
  pretty: bool = False,
647
+ escape: Escape = Escape.All,
592
648
  indent: str = None,
593
649
  **kwargs,
594
650
  ) -> str:
@@ -624,6 +680,8 @@ class Element(object):
624
680
  string += f"\n{indent * (depth + 2)}"
625
681
  newline = True
626
682
 
683
+ value = escaper(value, context=Context.Attribute, escape=escape)
684
+
627
685
  string += f' {key}="{value}"'
628
686
 
629
687
  # Add any non-promoted namespaces (those which can follow any attributes)
@@ -656,7 +714,7 @@ class Element(object):
656
714
 
657
715
  # Include the element's text content, if any
658
716
  if element.text and (element.mixed or not element.children):
659
- string += element.text
717
+ string += escaper(element.text, context=Context.Text, escape=escape)
660
718
 
661
719
  # Include the element's children, if any
662
720
  if element.children and (element.mixed or not element.text):
@@ -669,6 +727,7 @@ class Element(object):
669
727
  depth=(depth + 1),
670
728
  pretty=pretty,
671
729
  indent=indent,
730
+ escape=escape,
672
731
  **kwargs,
673
732
  )
674
733
 
@@ -684,6 +743,7 @@ class Element(object):
684
743
  depth=0,
685
744
  pretty=pretty,
686
745
  indent=indent,
746
+ escape=escape,
687
747
  **kwargs,
688
748
  )
689
749
 
@@ -0,0 +1,25 @@
1
+ from enumerific import Enumeration, auto
2
+
3
+
4
+ class Context(Enumeration):
5
+ """List of XML contexts, to denote the XML context currently being processed."""
6
+
7
+ Unknown = auto()
8
+ Root = auto(description="Document root node")
9
+ Prolog = auto(description="Document declaration (prolog)")
10
+ Instruction = auto(description="Document processing instruction")
11
+ DocType = auto(description="Document type")
12
+ Element = auto(description="Element comprising opening/closing or self-closing tag")
13
+ TagOpen = auto(description="Opening tag of an element (maybe self-closing)")
14
+ TagClose = auto(description="Closing tag of an element")
15
+ Attribute = auto(description="Attribute held by an element's tag")
16
+ Text = auto(description="Text held between an element's opening and closing tags")
17
+ Data = auto(description="Data held in a CDATA section")
18
+ Comment = auto(description="Comment held in a comment section")
19
+
20
+
21
+ class Escape(Enumeration):
22
+ """List of XML special character escape modes."""
23
+
24
+ All = auto(description="Escape all XML special characters")
25
+ Required = auto(description="Escpae only required XML special characters")
@@ -0,0 +1,2 @@
1
+ class MaXMLError(Exception):
2
+ pass
@@ -0,0 +1 @@
1
+ 1.0.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxml
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: A streamlined pure Python XML serializer.
5
5
  Author: Daniel Sissman
6
6
  License-Expression: MIT
@@ -18,7 +18,8 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
- Requires-Dist: hybridmethod==1.0.*
21
+ Requires-Dist: classicist==1.0.*
22
+ Requires-Dist: enumerific==1.0.*
22
23
  Provides-Extra: development
23
24
  Requires-Dist: black==24.10.*; extra == "development"
24
25
  Requires-Dist: pytest==8.3.*; extra == "development"
@@ -48,7 +49,7 @@ using `pip` via the `pip install` command by entering the following into your sh
48
49
 
49
50
  ### Example Usage
50
51
 
51
- To use the MaXML library, import the library's and begin creating your XML document:
52
+ To use the MaXML library, import the library and begin creating your XML document:
52
53
 
53
54
  ```python
54
55
  import maxml
@@ -12,6 +12,8 @@ source/maxml.egg-info/requires.txt
12
12
  source/maxml.egg-info/top_level.txt
13
13
  source/maxml.egg-info/zip-safe
14
14
  source/maxml/element/__init__.py
15
+ source/maxml/enumerations/__init__.py
16
+ source/maxml/exceptions/__init__.py
15
17
  source/maxml/logging/__init__.py
16
18
  source/maxml/namespace/__init__.py
17
19
  tests/test_element.py
@@ -1,4 +1,5 @@
1
- hybridmethod==1.0.*
1
+ classicist==1.0.*
2
+ enumerific==1.0.*
2
3
 
3
4
  [development]
4
5
  black==24.10.*
@@ -391,3 +391,73 @@ def test_maxml_fragment_tostring(element: maxml.Element, data: callable):
391
391
  assert isinstance(compare, str)
392
392
 
393
393
  assert string == compare
394
+
395
+
396
+ def test_maxml_special_tostring(data: callable):
397
+ """Check serialization functionality works as expected with special characters"""
398
+
399
+ element = maxml.Element(name="my:test", namespace="http://namespace.example.org/my")
400
+
401
+ # Ensure that the element object's type is as expected
402
+ assert isinstance(element, maxml.Element)
403
+
404
+ sub = element.subelement("my:sub")
405
+
406
+ # Ensure that the subelement's type is as expected
407
+ assert isinstance(sub, maxml.Element)
408
+
409
+ # Ensure that the subelement's name prefix was parsed correctly
410
+ assert sub.prefix == "my"
411
+
412
+ # Ensure that the subelement's name was parsed correctly
413
+ assert sub.name == "sub"
414
+
415
+ # Ensure that the element's fullname was parsed correctly
416
+ assert sub.fullname == "my:sub"
417
+
418
+ # Ensure that the element's namespace prefix was registered correctly
419
+ assert sub.namespace.prefix == "my"
420
+
421
+ # Ensure that the element's namespace URI was registered correctly
422
+ assert sub.namespace.uri == "http://namespace.example.org/my"
423
+
424
+ # Ensure that the root has the expected depth
425
+ assert sub.depth == 1
426
+
427
+ sub.set("another", 'Test\'s all of the special characters "2 > 3 < 1" & more!')
428
+
429
+ values = [
430
+ "This & That",
431
+ "1 < 2",
432
+ "3 > 2",
433
+ "That's looking good!",
434
+ 'Yes, I\'d agree, it is "looking good"!',
435
+ "This &gt; is already encoded &#129;!",
436
+ ]
437
+
438
+ sequence = sub.subelement("my:seq")
439
+
440
+ for index, value in enumerate(values, start=1):
441
+ item = sequence.subelement("my:li")
442
+ item.set("my:index", str(index))
443
+ item.text = value
444
+
445
+ string: str = element.tostring(pretty=True)
446
+
447
+ assert isinstance(string, str)
448
+
449
+ compare: str = data("examples/example03special-all.xml")
450
+
451
+ assert isinstance(compare, str)
452
+
453
+ assert string == compare
454
+
455
+ string: str = element.tostring(pretty=True, escape=maxml.Escape.Required)
456
+
457
+ assert isinstance(string, str)
458
+
459
+ compare: str = data("examples/example03special-required.xml")
460
+
461
+ assert isinstance(compare, str)
462
+
463
+ assert string == compare
@@ -1,2 +0,0 @@
1
- from maxml.element import Element
2
- from maxml.namespace import Namespace
@@ -1 +0,0 @@
1
- 1.0.1
File without changes
File without changes
File without changes