Coverage for gws-app/gws/lib/xmlx/__init__.py: 100%

4 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""XML parsing, building and serialization. 

2 

3An XML document is a tree of ``XmlElement`` objects (the ``gws.XmlElement`` interface), which implement a subset 

4of the ``ElementTree.Element`` API with a few extensions. Trees are created by parsing (``from_string``, ``from_path``) 

5or by building (``tag``), and written with ``XmlElement.to_string``. Options for both directions are in ``gws.XmlOptions``. 

6 

7Modules 

8------- 

9 

10``parser`` 

11 Permissive expat-based parser. Comments and processing instructions are dropped, entity declarations are rejected, 

12 a DOCTYPE is allowed, undeclared prefixes are accepted, the encoding is detected (UTF-8, then the declared one, 

13 then Latin-1). Used for remote capabilities and GetFeatureInfo documents (``base/ows/client``, ``plugin/ows_client``), 

14 OWS POST bodies (``base/ows/server/service``), OGC filters (``base/search/filter``), QGIS projects (``plugin/qgis``), 

15 SVG icons (``lib/style/icon``) and GekOS records. 

16 

17``tag`` 

18 ``tag(name, *args, **kwargs)`` builds an element from positional arguments: strings, numbers, booleans, dates 

19 and datetimes become text, dicts and ``gws.Data`` objects become attributes, elements become children, 

20 other iterables are spread, ``None`` is skipped. Keyword arguments become attributes as well. 

21 ``name`` can be a slash-separated path (``'a/b/c'`` creates nested elements, the arguments apply to the innermost one); 

22 slashes inside the ``{uri}`` part of a Clark name do not separate. This is how OWS server templates 

23 (``plugin/ows_server/*/templates``, ``base/ows/server/templatelib``), the GML writer (``lib/gml``) and the SVG 

24 renderer (``lib/svg``) produce XML. 

25 

26``element`` 

27 The ``XmlElement`` implementation: ElementTree-style navigation (``find``, ``findall``, ``iter``...) plus 

28 ``textof``, ``textlist``, ``textdict``, ``findfirst``, ``require``, ``isa``, ``add``, ``declare``, ``to_dict``. 

29 

30``serializer`` 

31 Writes a tree to a string: XML declaration, DOCTYPE, whitespace compaction, escaping, name validation, 

32 namespace prefixes and declarations. Invoked through ``XmlElement.to_string(opts)``. 

33 

34``namespace`` 

35 Namespace objects (``gws.XmlNamespace``: ``prefix``, ``uri``, ``schemaLocation``), the namespace table, 

36 registration of custom namespaces (``plugin/xml_helper``), and name utilities (``parse_name``, ``full_name``, 

37 ``plain_name``). 

38 

39``namespace_c`` 

40 The well-known namespaces as module-level constants with uppercase names, accessible as ``namespace.c.WFS``, 

41 ``namespace.c.GML`` and so on. 

42 

43``validator`` 

44 Test-only schema validation with lxml against the ``xsi:schemaLocation`` of a document. Schemas are downloaded 

45 and cached under ``gws.c.CACHE_DIR``. Not imported by this package. 

46 

47``util`` 

48 Value-to-string conversion and escaping. 

49 

50``error`` 

51 The exception classes. 

52 

53Names and namespaces 

54-------------------- 

55 

56Element and attribute names in a tree are either local (``Point``) or Clark names (``{http://www.opengis.net/gml/3.2}Point``). 

57A prefixed name never occurs in a tree: prefixes are a serialization detail. ``XmlElement.name`` is always the local name. 

58 

59*Namespace-less handling* (``XmlOptions(removeNamespaces=True)``): the parser strips all prefixes and declarations, 

60so names are local and paths are plain (``el.find('Capability/Request')``). This is what every OWS reader in the app uses; 

61the document cannot be reconstructed from the tree. 

62 

63*Namespace-aware handling* (the default): the parser resolves prefixes against the document's own ``xmlns`` declarations 

64into Clark names; the default namespace applies to elements, not to attributes; an undeclared prefix ``p`` becomes the 

65synthetic URI ``adhoc:p``. Declarations are kept in ``XmlElement.namespaces`` of the declaring element, so a parsed 

66document round-trips through ``to_string`` with its original prefixes (QGIS project patching relies on this). 

67 

68*Building*: in ``tag('OWS_11:Title')``, a prefix in ``tag()`` names and attribute keys is the *name* of a well-known 

69namespace (``namespace_c``), resolved to a Clark name at build time; an unknown name raises ``BuildError``. 

70Elements of configured (custom) namespaces are built with ``namespace.full_name(name, ns)``. 

71 

72*Serializing*: the prefix of a Clark name comes from the nearest enclosing ``XmlElement.namespaces`` declaration, 

73else from the namespace table, renamed via ``XmlOptions.customNamespacePrefixes`` (WFS ``NAMESPACES``). An element 

74in the default namespace (``XmlOptions.defaultNamespace`` or an enclosing ``xmlns=`` declaration) is written unprefixed. 

75With ``withNamespaceDeclarations``, every namespace used in the tree is declared on the root, with 

76``xsi:schemaLocation`` if ``withSchemaLocations`` is set; ``XmlElement.declare()`` adds declarations to a specific 

77element (inline GML for QGIS, ``xlink`` in DTD-based WMS 1.1 output, QName values such as ``gml:Envelope``). 

78 

79*The namespace table* contains the well-known namespaces from ``namespace_c``, keyed by their uppercase name 

80(``OWS_11``, ``GML``), and custom namespaces registered at configuration time (``namespace.register``), which have no name. 

81A prefix can occur several times (e.g. ``gml`` for GML 2 and GML 3.2). A URI can occur several times with different 

82schema locations (``GML_2``, ``GML_3_1``); ``namespace.find_by_uri`` returns the first one, a document that needs 

83another one declares it explicitly (``XmlElement.namespaces``). 

84 

85Errors 

86------ 

87 

88``ParseError`` (malformed input, entity declarations, undecodable bytes), ``BuildError`` (invalid ``tag()`` arguments), 

89``WriteError`` (invalid or prefixed names in a tree), ``NamespaceError`` (unknown or conflicting namespaces). 

90All derive from ``xmlx.Error``. 

91 

92Examples 

93-------- 

94 

95Read a capabilities document without namespaces:: 

96 

97 import gws.lib.xmlx as xmlx 

98 

99 root = xmlx.from_string(text, gws.XmlOptions(removeNamespaces=True)) 

100 title = root.textof('Service/Title') 

101 names = [el.textof('Name') for el in root.findall('Capability/Layer/Layer')] 

102 

103Parse a document with namespaces, names are Clark names:: 

104 

105 root = xmlx.from_string('<a xmlns:p="u:p"><p:b x="1">t</p:b></a>') 

106 root.findall('{u:p}b') # one element 

107 root.to_string() # '<a xmlns:p="u:p"><p:b x="1">t</p:b></a>' 

108 

109Build and serialize an element:: 

110 

111 el = xmlx.tag( 

112 'geometry/GML:Point', 

113 {'GML:id': 'xy'}, 

114 xmlx.tag('GML:coordinates', '12.345,56.789'), 

115 srsName=3857, 

116 ) 

117 el.to_string(gws.XmlOptions(withNamespaceDeclarations=True)) 

118 

119which returns (line breaks added):: 

120 

121 <geometry xmlns:gml="http://www.opengis.net/gml/3.2"> 

122 <gml:Point gml:id="xy" srsName="3857"> 

123 <gml:coordinates>12.345,56.789</gml:coordinates> 

124 </gml:Point> 

125 </geometry> 

126""" 

127 

128from .parser import from_path, from_string 

129from .tag import tag 

130from .error import Error, ParseError, WriteError, NamespaceError, BuildError 

131from . import namespace, util