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

1"""XML namespace manager. 

2 

3Maintains a registry of XML namespaces (well-known and custom). 

4""" 

5 

6from typing import Optional 

7import re 

8import os 

9 

10import gws 

11from . import error 

12 

13XMLNS = 'xmlns' 

14 

15 

16def from_args(**kwargs) -> gws.XmlNamespace: 

17 """Create a Namespace from keyword arguments.""" 

18 

19 ns = gws.XmlNamespace(**kwargs) 

20 return ns 

21 

22 

23def register(ns: gws.XmlNamespace): 

24 """Add a Namespace to an internal registry.""" 

25 

26 if ns.uid not in _INDEX.uid: 

27 _ALL.append(ns) 

28 _build_index() 

29 

30 

31def get(uid: str) -> Optional[gws.XmlNamespace]: 

32 """Locate the Namespace by a uid.""" 

33 

34 return _INDEX.uid.get(uid) 

35 

36 

37def require(uid: str) -> gws.XmlNamespace: 

38 """Locate the Namespace by a uid.""" 

39 

40 ns = get(uid) 

41 if not ns: 

42 raise error.NamespaceError(f'unknown namespace {uid!r}') 

43 return ns 

44 

45 

46def find_by_xmlns(xmlns: str) -> Optional[gws.XmlNamespace]: 

47 """Locate the Namespace by an xmlns prefix.""" 

48 

49 return _INDEX.xmlns.get(xmlns) 

50 

51 

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

53 """Locate the Namespace by an Uri.""" 

54 

55 return _INDEX.uri.get(uri) 

56 

57 

58def split_name(name: str) -> tuple[str, str, str]: 

59 """Parse an XML name in a xmlns: or Clark notation. 

60 

61 Args: 

62 name: XML name. 

63 

64 Returns: 

65 A tuple ``(xmlns-prefix, uri, proper name)``. 

66 """ 

67 

68 if not name: 

69 return '', '', name 

70 

71 if name[0] == '{': 

72 s = name.split('}') 

73 return '', s[0][1:], s[1] 

74 

75 if ':' in name: 

76 s = name.split(':') 

77 return s[0], '', s[1] 

78 

79 return '', '', name 

80 

81 

82def extract(name: str) -> tuple[Optional[gws.XmlNamespace], str]: 

83 """Extract a Namespace object from a qualified name. 

84 

85 Args: 

86 name: XML name. 

87 

88 Returns: 

89 A tuple ``(XmlNamespace, proper name)`` 

90 """ 

91 

92 xmlns, uri, pname = split_name(name) 

93 

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 

99 

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 

105 

106 return None, pname 

107 

108 

109def qualify_name(name: str, ns: Optional[gws.XmlNamespace] = None, replace: bool = False) -> str: 

110 """Qualify an XML name. 

111 

112 Args: 

113 name: An XML name. 

114 ns: A namespace. 

115 replace: If true, replace the existing namespace. 

116 

117 Returns: 

118 A qualified name. 

119 """ 

120 

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 

127 

128 

129def unqualify_name(name: str) -> str: 

130 """Returns an unqualified XML name.""" 

131 

132 _, _, pname = split_name(name) 

133 return pname 

134 

135 

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. 

141 

142 Args: 

143 namespaces: Mapping from prefixes to namespaces. 

144 with_schema_locations: Add the "schemaLocation" attribute. 

145 

146 Returns: 

147 A dict of attributes. 

148 """ 

149 

150 atts = [] 

151 schemas = [] 

152 

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

158 

159 if with_schema_locations and ns.schemaLocation: 

160 schemas.append(ns.uri) 

161 schemas.append(ns.schemaLocation) 

162 

163 if schemas: 

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

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

166 

167 return dict(sorted(atts)) 

168 

169 

170## 

171 

172 

173def _collect_namespaces(el: gws.XmlElement, ns_map): 

174 ns, _ = extract(el.tag) 

175 if ns: 

176 ns_map[ns.uid] = ns 

177 

178 for key in el.attrib: 

179 ns, _ = extract(key) 

180 if ns and ns.xmlns != XMLNS: 

181 ns_map[ns.uid] = ns 

182 

183 for sub in el: 

184 _collect_namespaces(sub, ns_map) 

185 

186 

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 

192 

193 

194_XSI = 'xsi' 

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

196 

197 

198_ALL: list[gws.XmlNamespace] = [] 

199 

200 

201class _Index: 

202 uid = {} 

203 xmlns = {} 

204 uri = {} 

205 

206 

207_INDEX = _Index() 

208 

209 

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) 

221 

222 

223def _load_known(): 

224 def http(u): 

225 return 'http://' + u if not u.startswith('http') else u 

226 

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 ) 

246 

247 

248def _build_index(): 

249 _INDEX.uid = {} 

250 _INDEX.xmlns = {} 

251 _INDEX.uri = {} 

252 

253 for ns in _ALL: 

254 _INDEX.uid[ns.uid] = ns 

255 

256 if ns.xmlns not in _INDEX.xmlns or ns.isDefault: 

257 _INDEX.xmlns[ns.xmlns] = ns 

258 

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 

262 

263 

264_load_known() 

265_build_index()