Coverage for gws-app/gws/lib/xmlx/namespace.py: 100%
82 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"""Namespace table and XML name utilities."""
3from typing import Optional
5import gws
6from . import error
7from . import namespace_c as c
9XMLNS = 'xmlns'
10"""Prefix of namespace declarations."""
12XML = 'xml'
13"""Reserved ``xml`` prefix, never declared."""
15XML_URI = 'http://www.w3.org/XML/1998/namespace'
16"""URI of the reserved ``xml`` prefix."""
18ADHOC = 'adhoc:'
19"""URI scheme for undeclared prefixes: ``{adhoc:foo}bar`` is written as ``foo:bar`` and never declared."""
22def find_well_known(name: str) -> Optional[gws.XmlNamespace]:
23 """Find a well-known namespace by its constant name.
25 Args:
26 name: Constant name in ``namespace_c``, e.g. ``OWS_11`` or ``GML``.
28 Returns:
29 The namespace, or ``None`` if there is no such name.
30 """
32 return _NAME_INDEX.get(name)
35def find_by_uri(uri: str) -> Optional[gws.XmlNamespace]:
36 """Find a well-known or registered namespace by its URI.
38 If several well-known namespaces share the URI, the first one in ``namespace_c`` is returned.
40 Args:
41 uri: Namespace URI.
43 Returns:
44 The namespace, or ``None`` if the URI is unknown.
45 """
47 return _URI_INDEX.get(uri)
50def find_by_prefix(prefix: str) -> Optional[gws.XmlNamespace]:
51 """Find a well-known or registered namespace by its prefix.
53 Registered namespaces are checked first, then the well-known ones in the order of ``namespace_c``.
55 Args:
56 prefix: Namespace prefix.
58 Returns:
59 The namespace, or ``None`` if the prefix is unknown.
60 """
62 for ns in _CUSTOM:
63 if ns.prefix == prefix:
64 return ns
65 for ns in _NAME_INDEX.values():
66 if ns.prefix == prefix:
67 return ns
68 return None
71def register(ns: gws.XmlNamespace):
72 """Register a custom namespace.
74 Registering the same prefix and URI again does nothing.
76 Args:
77 ns: The namespace to register.
79 Raises:
80 NamespaceError: If the URI is already known under a different prefix,
81 or the prefix is already used for a different URI.
82 """
84 old = _URI_INDEX.get(ns.uri)
85 if old:
86 if old.prefix == ns.prefix:
87 return
88 raise error.NamespaceError(f'namespace {ns.uri!r} is already registered as {old.prefix!r}')
90 old = find_by_prefix(ns.prefix)
91 if old:
92 raise error.NamespaceError(f'namespace prefix {ns.prefix!r} is already registered for {old.uri!r}')
94 _CUSTOM.append(ns)
95 _URI_INDEX[ns.uri] = ns
98def unregister_all():
99 """Remove all registered custom namespaces.
101 Used in tests.
102 """
104 for ns in _CUSTOM:
105 del _URI_INDEX[ns.uri]
106 _CUSTOM.clear()
109def new(prefix: str, uri: str, schemaLocation: str = '', extendsGml: bool = False) -> gws.XmlNamespace:
110 """Create a namespace object.
112 The namespace is not registered.
114 Args:
115 prefix: Default prefix, empty for a default namespace declaration.
116 uri: Namespace URI.
117 schemaLocation: Schema location URL.
118 extendsGml: Whether the namespace schema extends the GML schema.
120 Returns:
121 A new namespace object.
122 """
124 return gws.XmlNamespace(
125 prefix=prefix,
126 uri=uri,
127 schemaLocation=schemaLocation,
128 extendsGml=extendsGml,
129 )
132def parse_name(name: str) -> tuple[str, str, str]:
133 """Parse an XML name.
135 Args:
136 name: A local (``foo``), a prefixed (``p:foo``) or a Clark (``{uri}foo``) name.
138 Returns:
139 A tuple ``(uri, prefix, local name)``; ``uri`` and ``prefix`` are empty when not present.
140 """
142 if name.startswith('{'):
143 uri, _, pname = name[1:].partition('}')
144 return uri, '', pname
145 if ':' in name:
146 prefix, _, pname = name.partition(':')
147 return '', prefix, pname
148 return '', '', name
151def full_name(name: str, ns: Optional[gws.XmlNamespace | str]) -> str:
152 """Create a Clark name (``{uri}name``) from a name and a namespace or a URI.
154 An existing prefix or URI in ``name`` is replaced.
156 Args:
157 name: A local, prefixed or Clark name.
158 ns: A namespace object or a URI.
160 Returns:
161 The Clark name, or the local name if ``ns`` is empty.
162 """
164 pname = plain_name(name)
165 if not ns:
166 return pname
167 uri = ns if isinstance(ns, str) else ns.uri
168 return '{' + uri + '}' + pname
171def plain_name(name: str) -> str:
172 """Get the local part of an XML name.
174 Args:
175 name: A local, prefixed or Clark name.
177 Returns:
178 The local name.
179 """
181 return parse_name(name)[2]
184def declarations(
185 namespaces: list[gws.XmlNamespace],
186 prefixes: Optional[dict[str, str]] = None,
187 with_schema_locations: bool = False,
188) -> dict:
189 """Create ``xmlns`` declarations as attributes.
191 Args:
192 namespaces: Namespaces to declare (``prefix == ''`` is the default namespace).
193 prefixes: Mapping from URIs to prefixes that replace the default ones.
194 with_schema_locations: Add an ``xsi:schemaLocation`` attribute (and the ``xsi`` declaration)
195 for namespaces that have a schema location.
197 Returns:
198 A dict of attribute names and values, sorted by name.
199 """
201 atts = []
202 schemas = []
203 prefixes = prefixes or {}
205 for ns in namespaces:
206 prefix = prefixes.get(ns.uri, ns.prefix)
207 if prefix == '':
208 atts.append((XMLNS, ns.uri))
209 else:
210 atts.append((XMLNS + ':' + prefix, ns.uri))
212 if with_schema_locations and ns.schemaLocation:
213 schemas.append(ns.uri)
214 schemas.append(ns.schemaLocation)
216 if schemas:
217 atts.append((XMLNS + ':' + _XSI, _XSI_URL))
218 atts.append((_XSI + ':schemaLocation', ' '.join(schemas)))
220 return dict(sorted(atts))
223##
225_XSI = 'xsi'
226_XSI_URL = 'http://www.w3.org/2001/XMLSchema-instance'
229_CUSTOM: list[gws.XmlNamespace] = []
231_NAME_INDEX = {}
232_URI_INDEX = {}
234for _name, _ns in vars(c).items():
235 if isinstance(_ns, gws.XmlNamespace):
236 _NAME_INDEX.setdefault(_name, _ns)
237 _URI_INDEX.setdefault(_ns.uri, _ns)