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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""XML parsing, building and serialization.
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``.
7Modules
8-------
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.
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.
26``element``
27 The ``XmlElement`` implementation: ElementTree-style navigation (``find``, ``findall``, ``iter``...) plus
28 ``textof``, ``textlist``, ``textdict``, ``findfirst``, ``require``, ``isa``, ``add``, ``declare``, ``to_dict``.
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)``.
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``).
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.
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.
47``util``
48 Value-to-string conversion and escaping.
50``error``
51 The exception classes.
53Names and namespaces
54--------------------
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.
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.
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).
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)``.
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``).
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``).
85Errors
86------
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``.
92Examples
93--------
95Read a capabilities document without namespaces::
97 import gws.lib.xmlx as xmlx
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')]
103Parse a document with namespaces, names are Clark names::
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>'
109Build and serialize an element::
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))
119which returns (line breaks added)::
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"""
128from .parser import from_path, from_string
129from .tag import tag
130from .error import Error, ParseError, WriteError, NamespaceError, BuildError
131from . import namespace, util