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

1"""Namespace table and XML name utilities.""" 

2 

3from typing import Optional 

4 

5import gws 

6from . import error 

7from . import namespace_c as c 

8 

9XMLNS = 'xmlns' 

10"""Prefix of namespace declarations.""" 

11 

12XML = 'xml' 

13"""Reserved ``xml`` prefix, never declared.""" 

14 

15XML_URI = 'http://www.w3.org/XML/1998/namespace' 

16"""URI of the reserved ``xml`` prefix.""" 

17 

18ADHOC = 'adhoc:' 

19"""URI scheme for undeclared prefixes: ``{adhoc:foo}bar`` is written as ``foo:bar`` and never declared.""" 

20 

21 

22def find_well_known(name: str) -> Optional[gws.XmlNamespace]: 

23 """Find a well-known namespace by its constant name. 

24 

25 Args: 

26 name: Constant name in ``namespace_c``, e.g. ``OWS_11`` or ``GML``. 

27 

28 Returns: 

29 The namespace, or ``None`` if there is no such name. 

30 """ 

31 

32 return _NAME_INDEX.get(name) 

33 

34 

35def find_by_uri(uri: str) -> Optional[gws.XmlNamespace]: 

36 """Find a well-known or registered namespace by its URI. 

37 

38 If several well-known namespaces share the URI, the first one in ``namespace_c`` is returned. 

39 

40 Args: 

41 uri: Namespace URI. 

42 

43 Returns: 

44 The namespace, or ``None`` if the URI is unknown. 

45 """ 

46 

47 return _URI_INDEX.get(uri) 

48 

49 

50def find_by_prefix(prefix: str) -> Optional[gws.XmlNamespace]: 

51 """Find a well-known or registered namespace by its prefix. 

52 

53 Registered namespaces are checked first, then the well-known ones in the order of ``namespace_c``. 

54 

55 Args: 

56 prefix: Namespace prefix. 

57 

58 Returns: 

59 The namespace, or ``None`` if the prefix is unknown. 

60 """ 

61 

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 

69 

70 

71def register(ns: gws.XmlNamespace): 

72 """Register a custom namespace. 

73 

74 Registering the same prefix and URI again does nothing. 

75 

76 Args: 

77 ns: The namespace to register. 

78 

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 """ 

83 

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}') 

89 

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}') 

93 

94 _CUSTOM.append(ns) 

95 _URI_INDEX[ns.uri] = ns 

96 

97 

98def unregister_all(): 

99 """Remove all registered custom namespaces. 

100 

101 Used in tests. 

102 """ 

103 

104 for ns in _CUSTOM: 

105 del _URI_INDEX[ns.uri] 

106 _CUSTOM.clear() 

107 

108 

109def new(prefix: str, uri: str, schemaLocation: str = '', extendsGml: bool = False) -> gws.XmlNamespace: 

110 """Create a namespace object. 

111 

112 The namespace is not registered. 

113 

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. 

119 

120 Returns: 

121 A new namespace object. 

122 """ 

123 

124 return gws.XmlNamespace( 

125 prefix=prefix, 

126 uri=uri, 

127 schemaLocation=schemaLocation, 

128 extendsGml=extendsGml, 

129 ) 

130 

131 

132def parse_name(name: str) -> tuple[str, str, str]: 

133 """Parse an XML name. 

134 

135 Args: 

136 name: A local (``foo``), a prefixed (``p:foo``) or a Clark (``{uri}foo``) name. 

137 

138 Returns: 

139 A tuple ``(uri, prefix, local name)``; ``uri`` and ``prefix`` are empty when not present. 

140 """ 

141 

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 

149 

150 

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. 

153 

154 An existing prefix or URI in ``name`` is replaced. 

155 

156 Args: 

157 name: A local, prefixed or Clark name. 

158 ns: A namespace object or a URI. 

159 

160 Returns: 

161 The Clark name, or the local name if ``ns`` is empty. 

162 """ 

163 

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 

169 

170 

171def plain_name(name: str) -> str: 

172 """Get the local part of an XML name. 

173 

174 Args: 

175 name: A local, prefixed or Clark name. 

176 

177 Returns: 

178 The local name. 

179 """ 

180 

181 return parse_name(name)[2] 

182 

183 

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. 

190 

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. 

196 

197 Returns: 

198 A dict of attribute names and values, sorted by name. 

199 """ 

200 

201 atts = [] 

202 schemas = [] 

203 prefixes = prefixes or {} 

204 

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)) 

211 

212 if with_schema_locations and ns.schemaLocation: 

213 schemas.append(ns.uri) 

214 schemas.append(ns.schemaLocation) 

215 

216 if schemas: 

217 atts.append((XMLNS + ':' + _XSI, _XSI_URL)) 

218 atts.append((_XSI + ':schemaLocation', ' '.join(schemas))) 

219 

220 return dict(sorted(atts)) 

221 

222 

223## 

224 

225_XSI = 'xsi' 

226_XSI_URL = 'http://www.w3.org/2001/XMLSchema-instance' 

227 

228 

229_CUSTOM: list[gws.XmlNamespace] = [] 

230 

231_NAME_INDEX = {} 

232_URI_INDEX = {} 

233 

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)