maxml 1.0.2__py3-none-any.whl → 1.0.3__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 CHANGED
@@ -1,2 +1,4 @@
1
1
  from maxml.element import Element
2
2
  from maxml.namespace import Namespace
3
+ from maxml.enumerations import Escape
4
+ from maxml.exceptions import MaXMLError
maxml/element/__init__.py CHANGED
@@ -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
11
  from classicist import hybridmethod
7
12
 
13
+ import re
8
14
 
9
15
  logger = logger.getChild(__name__)
10
16
 
@@ -77,9 +83,9 @@ class Element(object):
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
maxml/version.txt CHANGED
@@ -1 +1 @@
1
- 1.0.2
1
+ 1.0.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxml
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: A streamlined pure Python XML serializer.
5
5
  Author: Daniel Sissman
6
6
  License-Expression: MIT
@@ -0,0 +1,12 @@
1
+ maxml/__init__.py,sha256=9wLSTIlZTXinpBr9ABp_PUWhfxubtNM89Iac2VCNet8,150
2
+ maxml/version.txt,sha256=INLLCW0atBpBQCRtEvB79rjLdD_UgSK3JTLAPUTFwUo,5
3
+ maxml/element/__init__.py,sha256=39X5ncEAT7kdFa9jz-1MVG02O5HaoxcHuFuNUNYswXY,26198
4
+ maxml/enumerations/__init__.py,sha256=N15XKtEZp2cFwqq_1GXyODqoOE--z9WukZGt_rFur2E,1153
5
+ maxml/exceptions/__init__.py,sha256=qMI3bIXJe6zDXzRnxdW3v6wbxSYfd1oK-9lnxvZzzjQ,38
6
+ maxml/logging/__init__.py,sha256=AMpvZEQH9GIBe8609sMwb2T64rovpkRRCc9rs2kHy8g,52
7
+ maxml/namespace/__init__.py,sha256=iZqHWuGi09KtMTr7wHiyTQLXB2TFljUir8wZGLi2b60,3882
8
+ maxml-1.0.3.dist-info/METADATA,sha256=ekfPSZaa565dou1yy4wwqC4fWu264JxFlwqYra-AXpo,16193
9
+ maxml-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ maxml-1.0.3.dist-info/top_level.txt,sha256=ZFK3SmCc04Dzhl9QkO_hVBAEiHwCNrwn8X_PINWE9C4,6
11
+ maxml-1.0.3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
12
+ maxml-1.0.3.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- maxml/__init__.py,sha256=QuUB-oFrPKRZ4XzPuk4Mt8r35EMx652PALyAeWvQXIo,72
2
- maxml/version.txt,sha256=v-wuNFg62n5q8stzmT-3Wj9xR6bJQ-X_X1xClPxXe5A,5
3
- maxml/element/__init__.py,sha256=8N6ePtw7nOX6VVQ0eG5mmDJkBXo8S1qwVF3k9X2OmLo,23843
4
- maxml/logging/__init__.py,sha256=AMpvZEQH9GIBe8609sMwb2T64rovpkRRCc9rs2kHy8g,52
5
- maxml/namespace/__init__.py,sha256=iZqHWuGi09KtMTr7wHiyTQLXB2TFljUir8wZGLi2b60,3882
6
- maxml-1.0.2.dist-info/METADATA,sha256=J9F0r0aKB7prWv2tALoHhPRtFYCUXm096UOdoRQW5Fc,16193
7
- maxml-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- maxml-1.0.2.dist-info/top_level.txt,sha256=ZFK3SmCc04Dzhl9QkO_hVBAEiHwCNrwn8X_PINWE9C4,6
9
- maxml-1.0.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
- maxml-1.0.2.dist-info/RECORD,,
File without changes