Coverage for gws-app/gws/lib/xmlx/namespace.py: 86%
121 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
1"""XML namespace manager.
3Maintains a registry of XML namespaces (well-known and custom).
4"""
6from typing import Optional
7import re
8import os
10import gws
11from . import error
13XMLNS = 'xmlns'
16def from_args(**kwargs) -> gws.XmlNamespace:
17 """Create a Namespace from keyword arguments."""
19 ns = gws.XmlNamespace(**kwargs)
20 return ns
23def register(ns: gws.XmlNamespace):
24 """Add a Namespace to an internal registry."""
26 if ns.uid not in _INDEX.uid:
27 _ALL.append(ns)
28 _build_index()
31def get(uid: str) -> Optional[gws.XmlNamespace]:
32 """Locate the Namespace by a uid."""
34 return _INDEX.uid.get(uid)
37def require(uid: str) -> gws.XmlNamespace:
38 """Locate the Namespace by a uid."""
40 ns = get(uid)
41 if not ns:
42 raise error.NamespaceError(f'unknown namespace {uid!r}')
43 return ns
46def find_by_xmlns(xmlns: str) -> Optional[gws.XmlNamespace]:
47 """Locate the Namespace by an xmlns prefix."""
49 return _INDEX.xmlns.get(xmlns)
52def find_by_uri(uri: str) -> Optional[gws.XmlNamespace]:
53 """Locate the Namespace by an Uri."""
55 return _INDEX.uri.get(uri)
58def split_name(name: str) -> tuple[str, str, str]:
59 """Parse an XML name in a xmlns: or Clark notation.
61 Args:
62 name: XML name.
64 Returns:
65 A tuple ``(xmlns-prefix, uri, proper name)``.
66 """
68 if not name:
69 return '', '', name
71 if name[0] == '{':
72 s = name.split('}')
73 return '', s[0][1:], s[1]
75 if ':' in name:
76 s = name.split(':')
77 return s[0], '', s[1]
79 return '', '', name
82def extract(name: str) -> tuple[Optional[gws.XmlNamespace], str]:
83 """Extract a Namespace object from a qualified name.
85 Args:
86 name: XML name.
88 Returns:
89 A tuple ``(XmlNamespace, proper name)``
90 """
92 xmlns, uri, pname = split_name(name)
94 if xmlns:
95 ns = find_by_xmlns(xmlns)
96 if not ns:
97 raise error.NamespaceError(f'unknown namespace {xmlns!r}')
98 return ns, pname
100 if uri:
101 ns = find_by_uri(uri)
102 if not ns:
103 raise error.NamespaceError(f'unknown namespace uri {uri!r}')
104 return ns, pname
106 return None, pname
109def qualify_name(name: str, ns: Optional[gws.XmlNamespace] = None, replace: bool = False) -> str:
110 """Qualify an XML name.
112 Args:
113 name: An XML name.
114 ns: A namespace.
115 replace: If true, replace the existing namespace.
117 Returns:
118 A qualified name.
119 """
121 exist_ns, uri, pname = split_name(name)
122 if (exist_ns or uri) and not replace:
123 return name
124 if ns:
125 return ns.xmlns + ':' + pname
126 return pname
129def unqualify_name(name: str) -> str:
130 """Returns an unqualified XML name."""
132 _, _, pname = split_name(name)
133 return pname
136def declarations(
137 namespaces: dict[str, gws.XmlNamespace],
138 with_schema_locations: bool = False,
139) -> dict:
140 """Returns an xmlns declaration block as dictionary of attributes.
142 Args:
143 namespaces: Mapping from prefixes to namespaces.
144 with_schema_locations: Add the "schemaLocation" attribute.
146 Returns:
147 A dict of attributes.
148 """
150 atts = []
151 schemas = []
153 for xmlns, ns in namespaces.items():
154 if xmlns == '':
155 atts.append((XMLNS, ns.uri))
156 else:
157 atts.append((XMLNS + ':' + xmlns, ns.uri))
159 if with_schema_locations and ns.schemaLocation:
160 schemas.append(ns.uri)
161 schemas.append(ns.schemaLocation)
163 if schemas:
164 atts.append((XMLNS + ':' + _XSI, _XSI_URL))
165 atts.append((_XSI + ':schemaLocation', ' '.join(schemas)))
167 return dict(sorted(atts))
170##
173def _collect_namespaces(el: gws.XmlElement, ns_map):
174 ns, _ = extract(el.tag)
175 if ns:
176 ns_map[ns.uid] = ns
178 for key in el.attrib:
179 ns, _ = extract(key)
180 if ns and ns.xmlns != XMLNS:
181 ns_map[ns.uid] = ns
183 for sub in el:
184 _collect_namespaces(sub, ns_map)
187def _parse_versioned_uri(uri: str) -> tuple[str, str]:
188 m = re.match(r'(.+?)/([\d.]+)$', uri)
189 if m:
190 return m.group(1), m.group(2)
191 return '', uri
194_XSI = 'xsi'
195_XSI_URL = 'http://www.w3.org/2001/XMLSchema-instance'
198_ALL: list[gws.XmlNamespace] = []
201class _Index:
202 uid = {}
203 xmlns = {}
204 uri = {}
207_INDEX = _Index()
210# fake namespace for 'xmlns:'
211_ALL.append(
212 gws.XmlNamespace(
213 uid=XMLNS,
214 xmlns=XMLNS,
215 uri='',
216 schemaLocation='',
217 version='',
218 isDefault=True,
219 )
220)
223def _load_known():
224 def http(u):
225 return 'http://' + u if not u.startswith('http') else u
227 with open(os.path.dirname(__file__) + '/namespaces.md') as fp:
228 for ln in fp:
229 ln = ln.strip()
230 if not ln.startswith('|'):
231 continue
232 p = [x.strip() for x in ln.strip('|').split('|')]
233 if p[0].startswith('#') or p[0].startswith('-'):
234 continue
235 uid, xmlns, dflt, version, uri, schema = p
236 _ALL.append(
237 gws.XmlNamespace(
238 uid=uid,
239 xmlns=xmlns or uid,
240 uri=http(uri),
241 schemaLocation=http(schema) if schema else '',
242 version=version,
243 isDefault=dflt != 'N',
244 )
245 )
248def _build_index():
249 _INDEX.uid = {}
250 _INDEX.xmlns = {}
251 _INDEX.uri = {}
253 for ns in _ALL:
254 _INDEX.uid[ns.uid] = ns
256 if ns.xmlns not in _INDEX.xmlns or ns.isDefault:
257 _INDEX.xmlns[ns.xmlns] = ns
259 _INDEX.uri[ns.uri] = ns
260 if ns.version and not ns.uri.endswith('/' + ns.version):
261 _INDEX.uri[ns.uri + '/' + ns.version] = ns
264_load_known()
265_build_index()