Coverage for pyEDAA/IPXACT/__init__.py: 72%

245 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-25 17:18 +0000

1# ==================================================================================================================== # 

2# _____ ____ _ _ ___ ______ __ _ ____ _____ # 

3# _ __ _ _| ____| _ \ / \ / \ |_ _| _ \ \/ / / \ / ___|_ _| # 

4# | '_ \| | | | _| | | | |/ _ \ / _ \ | || |_) \ / / _ \| | | | # 

5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _ | || __// \ / ___ \ |___ | | # 

6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___|_| /_/\_\/_/ \_\____| |_| # 

7# |_| |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # 

15# Copyright 2016-2016 Patrick Lehmann - Dresden, Germany # 

16# # 

17# Licensed under the Apache License, Version 2.0 (the "License"); # 

18# you may not use this file except in compliance with the License. # 

19# You may obtain a copy of the License at # 

20# # 

21# http://www.apache.org/licenses/LICENSE-2.0 # 

22# # 

23# Unless required by applicable law or agreed to in writing, software # 

24# distributed under the License is distributed on an "AS IS" BASIS, # 

25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

26# See the License for the specific language governing permissions and # 

27# limitations under the License. # 

28# # 

29# SPDX-License-Identifier: Apache-2.0 # 

30# ==================================================================================================================== # 

31# 

32"""A DOM based IP-XACT implementation for Python.""" 

33from pathlib import Path 

34from textwrap import dedent 

35from typing import Union, Dict, Tuple, Optional as Nullable, ClassVar 

36 

37from lxml.etree import XMLParser, XML, XMLSchema, ElementTree, QName, _Element, _Comment 

38from pyTooling.Decorators import export, readonly 

39from pyTooling.MetaClasses import ExtendedType, abstractmethod 

40from pyTooling.Common import getFullyQualifiedName 

41from pyTooling.Versioning import SemanticVersion, CalendarVersion 

42 

43from . import Schema 

44from .Schema import * 

45 

46__author__ = "Patrick Lehmann" 

47__email__ = "Paebbels@gmail.com" 

48__copyright__ = "2016-2026, Patrick Lehmann" 

49__license__ = "Apache License, Version 2.0" 

50__version__ = "0.7.0rc1" 

51# __keywords__ = [] 

52__project_url__ = "https://github.com/edaa-org/pyEDAA.IPXACT" 

53__documentation_url__ = "https://edaa-org.github.io/pyEDAA.IPXACT" 

54__issue_tracker_url__ = "https://GitHub.com/edaa-org/pyEDAA.IPXACT/issues" 

55 

56 

57@export 

58class IPXACTException(Exception): 

59 """Base-exception for all exceptions in this package.""" 

60 

61 

62@export 

63class IPXACTSchema(metaclass=ExtendedType, slots=True): 

64 """Schema descriptor made of version, namespace prefix, URI, URL and local path.""" 

65 

66 _version: Union[SemanticVersion, CalendarVersion] #: Schema version 

67 _namespacePrefix: str #: XML namespace prefix 

68 _schemaUri: str #: Schema URI 

69 _schemaUrl: str #: Schema URL 

70 _localPath: Path #: Local path 

71 

72 def __init__( 

73 self, 

74 version: Union[str, SemanticVersion, CalendarVersion], 

75 xmlNamespacePrefix: str, 

76 schemaUri: str, 

77 schemaUrl: str, 

78 localPath: Path 

79 ) -> None: 

80 """ 

81 Initializes an IP-XACT Schema description. 

82 

83 :param version: Version of the IP-XACT Schema. 

84 :param xmlNamespacePrefix: XML namespace prefix (``<prefix:element>``) 

85 :param schemaUri: IP-XACT schema URI 

86 :param schemaUrl: URL the IP-XACT schema definition file (XSD). 

87 :param localPath: Path to the local XSD file. 

88 """ 

89 # TODO: add raises ... lines 

90 if version is None: 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true

91 raise ValueError("Parameter 'version' is None.") 

92 elif isinstance(version, str): 92 ↛ 97line 92 didn't jump to line 97 because the condition on line 92 was always true

93 if version.startswith("20"): 

94 self._version = CalendarVersion.Parse(version) 

95 else: 

96 self._version = SemanticVersion.Parse(version) 

97 elif isinstance(version, (SemanticVersion, CalendarVersion)): 

98 self._version = version 

99 else: 

100 ex = TypeError("Parameter 'version' is neither a 'SemanticVersion', a 'CalendarVersion' nor a string.") 

101 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

102 raise ex 

103 

104 if xmlNamespacePrefix is None: 104 ↛ 105line 104 didn't jump to line 105 because the condition on line 104 was never true

105 raise ValueError("Parameter 'namespacePrefix' is None.") 

106 elif not isinstance(xmlNamespacePrefix, str): 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true

107 ex = TypeError("Parameter 'namespacePrefix' is not a string.") 

108 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

109 raise ex 

110 

111 if schemaUri is None: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true

112 raise ValueError("Parameter 'schemaUri' is None.") 

113 elif not isinstance(schemaUri, str): 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true

114 ex = TypeError("Parameter 'schemaUri' is not a string.") 

115 ex.add_note(f"Got type '{getFullyQualifiedName(schemaUri)}'.") 

116 raise ex 

117 

118 if schemaUrl is None: 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true

119 raise ValueError("Parameter 'schemaUrl' is None.") 

120 elif not isinstance(schemaUrl, str): 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true

121 ex = TypeError("Parameter 'schemaUrl' is not a string.") 

122 ex.add_note(f"Got type '{getFullyQualifiedName(schemaUrl)}'.") 

123 raise ex 

124 

125 if localPath is None: 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true

126 raise ValueError("Parameter 'localPath' is None.") 

127 elif not isinstance(localPath, Path): 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true

128 ex = TypeError("Parameter 'localPath' is not a Path.") 

129 ex.add_note(f"Got type '{getFullyQualifiedName(localPath)}'.") 

130 raise ex 

131 

132 self._namespacePrefix = xmlNamespacePrefix 

133 self._schemaUri = schemaUri 

134 self._schemaUrl = schemaUrl 

135 self._localPath = localPath 

136 

137 @readonly 

138 def Version(self) -> Union[SemanticVersion, CalendarVersion]: 

139 return self._version 

140 

141 @readonly 

142 def NamespacePrefix(self) -> str: 

143 return self._namespacePrefix 

144 

145 @readonly 

146 def SchemaUri(self) -> str: 

147 return self._schemaUri 

148 

149 @readonly 

150 def SchemaUrl(self) -> str: 

151 return self._schemaUrl 

152 

153 @readonly 

154 def LocalPath(self) -> Path: 

155 return self._localPath 

156 

157 def __repr__(self) -> str: 

158 return f"<{self.__class__.__name__} IP-XACT {self._version} {self._schemaUri} - {self._localPath}>" 

159 

160 def __str__(self) -> str: 

161 return f"IP-XACT {self._version}" 

162 

163 

164# version, xmlns, URI URL, Local Path 

165_IPXACT_10 = IPXACTSchema("1.0", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.0", "", _IPXACT_10_INDEX) 

166_IPXACT_11 = IPXACTSchema("1.1", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.1", "", _IPXACT_11_INDEX) 

167_IPXACT_12 = IPXACTSchema("1.2", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.2", "", _IPXACT_12_INDEX) 

168_IPXACT_14 = IPXACTSchema("1.4", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.4", "", _IPXACT_14_INDEX) 

169_IPXACT_15 = IPXACTSchema("1.5", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.5", "", _IPXACT_15_INDEX) 

170_IPXACT_2009 = IPXACTSchema("2009", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1685-2009", "", _IPXACT_2009_INDEX) 

171_IPXACT_2014 = IPXACTSchema("2014", "ipxact", "http://www.accellera.org/XMLSchema/IPXACT/1685-2014", "http://www.accellera.org/XMLSchema/IPXACT/1685-2014/index.xsd", _IPXACT_2014_INDEX) 

172_IPXACT_2022 = IPXACTSchema("2022", "ipxact", "http://www.accellera.org/XMLSchema/IPXACT/1685-2022", "http://www.accellera.org/XMLSchema/IPXACT/1685-2022/index.xsd", _IPXACT_2022_INDEX) 

173 

174__VERSION_TABLE__: Dict[str, IPXACTSchema] = { 

175 '1.0': _IPXACT_10, 

176 '1.1': _IPXACT_11, 

177 '1.4': _IPXACT_14, 

178 '1.5': _IPXACT_15, 

179 '2009': _IPXACT_2009, 

180 '2014': _IPXACT_2014, 

181 '2022': _IPXACT_2022 

182} #: Dictionary of all IP-XACT versions mapping to :class:`IpxactSchema` instances. 

183 

184__URI_MAP__: Dict[str, IPXACTSchema] = {value.SchemaUri: value for key, value in __VERSION_TABLE__.items()} #: Mapping from schema URIs to :class:`IpxactSchema` instances. 

185 

186__DEFAULT_VERSION__ = "2022" #: IP-XACT default version 

187__DEFAULT_SCHEMA__ = __VERSION_TABLE__[__DEFAULT_VERSION__] #: IP-XACT default Schema 

188 

189 

190@export 

191class VLNV(metaclass=ExtendedType, slots=True): 

192 """VLNV data structure (Vendor, Library, Name, Version) as a unique identifier in IP-XACT.""" 

193 

194 _vendor: str #: Vendor name in a VLNV unique identifier 

195 _library: str #: Library name in a VLNV unique identifier 

196 _name: str #: Component name in a VLNV unique identifier 

197 _version: SemanticVersion #: Version in a VLNV unique identifier 

198 

199 def __init__(self, vendor: str, library: str, name: str, version: Union[str, SemanticVersion]) -> None: 

200 """ 

201 Initializes the VLNV data structure. 

202 

203 :param vendor: Vendor name in a VLNV unique identifier 

204 :param library: Library name in a VLNV unique identifier 

205 :param name: Component name in a VLNV unique identifier 

206 :param version: Version in a VLNV unique identifier 

207 """ 

208 

209 if vendor is None: 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true

210 raise ValueError("Parameter 'vendor' is None.") 

211 elif not isinstance(vendor, str): 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true

212 ex = TypeError("Parameter 'vendor' is not a string.") 

213 ex.add_note(f"Got type '{getFullyQualifiedName(vendor)}'.") 

214 raise ex 

215 

216 if library is None: 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true

217 raise ValueError("Parameter 'library' is None.") 

218 elif not isinstance(library, str): 218 ↛ 219line 218 didn't jump to line 219 because the condition on line 218 was never true

219 ex = TypeError("Parameter 'library' is not a string.") 

220 ex.add_note(f"Got type '{getFullyQualifiedName(library)}'.") 

221 raise ex 

222 

223 if name is None: 223 ↛ 224line 223 didn't jump to line 224 because the condition on line 223 was never true

224 raise ValueError("Parameter 'name' is None.") 

225 elif not isinstance(name, str): 225 ↛ 226line 225 didn't jump to line 226 because the condition on line 225 was never true

226 ex = TypeError("Parameter 'name' is not a string.") 

227 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

228 raise ex 

229 

230 if version is None: 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true

231 raise ValueError("Parameter 'version' is None.") 

232 elif isinstance(version, str): 

233 self._version = SemanticVersion.Parse(version) 

234 elif isinstance(version, SemanticVersion): 234 ↛ 237line 234 didn't jump to line 237 because the condition on line 234 was always true

235 self._version = version 

236 else: 

237 ex = TypeError("Parameter 'version' is neither a 'SemanticVersion' nor a string.") 

238 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

239 raise ex 

240 

241 self._vendor = vendor 

242 self._library = library 

243 self._name = name 

244 

245 @readonly 

246 def Vendor(self) -> str: 

247 return self._vendor 

248 

249 @readonly 

250 def Library(self) -> str: 

251 return self._library 

252 

253 @readonly 

254 def Name(self) -> str: 

255 return self._name 

256 

257 @readonly 

258 def Version(self) -> SemanticVersion: 

259 return self._version 

260 

261 def ToXml(self, indent=1, schema: IPXACTSchema = __DEFAULT_SCHEMA__, isVersionedIdentifier=False) -> str: 

262 """ 

263 Converts the object's data into XML format. 

264 

265 :param indent: Level of indentations. 

266 :param schema: XML schema. 

267 :param isVersionedIdentifier: If true, generate 4 individual tags (``<vendor>``, ``<library>``, ``<name>``, 

268 ``<version>``), otherwise a single ``<vlnv>``-tag with attributes. 

269 :returns: XML formatted string representation. 

270 """ 

271 

272 # WORKAROUND: 

273 # Python <=3.11: 

274 # {'\t' * indent} is not supported by Python before 3.12 due to a backslash within {...} 

275 indent = "\t" * indent 

276 xmlns = schema.NamespacePrefix 

277 

278 if isVersionedIdentifier: 

279 return dedent(f"""\ 

280 {indent}<{xmlns}:vendor>{self._vendor}</{xmlns}:vendor> 

281 {indent}<{xmlns}:library>{self._library}</{xmlns}:library> 

282 {indent}<{xmlns}:name>{self._name}</{xmlns}:name> 

283 {indent}<{xmlns}:version>{self._version}</{xmlns}:version> 

284 """) 

285 else: 

286 return f"""{indent}<{xmlns}:vlnv vendor="{self._vendor}" library="{self._library}" name="{self._name}" version="{self._version}"/>""" 

287 

288 

289@export 

290class Element(metaclass=ExtendedType, slots=True): 

291 """Base-class for all IP-XACT elements.""" 

292 

293 def __init__(self, vlnv: VLNV) -> None: 

294 """ 

295 Initializes the Element class. 

296 """ 

297 

298 

299@export 

300class NamedElement(Element): 

301 """Base-class for all IP-XACT elements with a VLNV.""" 

302 

303 _vlnv: VLNV #: VLNV unique identifier. 

304 

305 def __init__(self, vlnv: VLNV) -> None: 

306 """ 

307 Initializes the NameElement with an VLNV field for all derives classes. 

308 

309 :param vlnv: VLNV unique identifier. 

310 :raises TypeError: If parameter vlnv is not a VLNV. 

311 """ 

312 if not isinstance(vlnv, VLNV): 312 ↛ 313line 312 didn't jump to line 313 because the condition on line 312 was never true

313 ex = TypeError("Parameter 'vlnv' is not a VLNV.") 

314 ex.add_note(f"Got type '{getFullyQualifiedName(vlnv)}'.") 

315 raise ex 

316 

317 self._vlnv = vlnv 

318 

319 @readonly 

320 def VLNV(self) -> VLNV: 

321 return self._vlnv 

322 

323 

324@export 

325class RootElement(NamedElement): 

326 """Base-class for all IP-XACT root elements.""" 

327 

328 _file: Nullable[Path] 

329 _ROOT_TAG_NAME: ClassVar[str] = "" 

330 _xmlRoot: Nullable[_Element] 

331 _xmlSchema: Nullable[_Element] 

332 

333 _description: str 

334 

335 def __init__(self, file: Nullable[Path] = None, parse: bool = False, vlnv: Nullable[VLNV] = None, description: Nullable[str] = None) -> None: 

336 self._description = description 

337 

338 if file is None: 

339 super().__init__(vlnv) 

340 self._file = None 

341 elif isinstance(file, Path): 341 ↛ 350line 341 didn't jump to line 350 because the condition on line 341 was always true

342 self._file = file 

343 vlnv = None 

344 if parse: 344 ↛ 348line 344 didn't jump to line 348 because the condition on line 344 was always true

345 self.OpenAndValidate() 

346 vlnv, self._description = self.ParseVLNVAndDescription() 

347 

348 super().__init__(vlnv) 

349 else: 

350 ex = TypeError("Parameter 'file' is not a Path.") 

351 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") 

352 raise ex 

353 

354 def OpenAndValidate(self) -> None: 

355 if not self._file.exists(): 355 ↛ 356line 355 didn't jump to line 356 because the condition on line 355 was never true

356 raise IPXACTException(f"IPXACT file '{self._file}' not found.") from FileNotFoundError(str(self._file)) 

357 

358 try: 

359 with self._file.open("rb") as fileHandle: 

360 content = fileHandle.read() 

361 except OSError as ex: 

362 raise IPXACTException(f"Couldn't open '{self._file}'.") from ex 

363 

364 xmlParser = XMLParser(remove_blank_text=True, encoding="utf-8") 

365 self._xmlRoot = XML(content, parser=xmlParser, base_url=self._file.resolve().as_uri()) # - relative paths are not supported 

366 rootTag = QName(self._xmlRoot.tag) 

367 

368 if rootTag.localname != self._ROOT_TAG_NAME: 368 ↛ 369line 368 didn't jump to line 369 because the condition on line 368 was never true

369 raise IPXACTException(f"The input IP-XACT file is not a {self._ROOT_TAG_NAME} file.") 

370 

371 namespacePrefix = self._xmlRoot.prefix 

372 namespaceURI = self._xmlRoot.nsmap[namespacePrefix] 

373 if namespaceURI in __URI_MAP__: 373 ↛ 376line 373 didn't jump to line 376 because the condition on line 373 was always true

374 ipxactSchema = __URI_MAP__[namespaceURI] 

375 else: 

376 raise IPXACTException(f"The input IP-XACT file uses an unsupported namespace: '{namespaceURI}'.") 

377 

378 try: 

379 with ipxactSchema.LocalPath.open("rb") as fileHandle: 

380 schema = fileHandle.read() 

381 except OSError as ex: 

382 raise IPXACTException(f"Couldn't open IP-XACT schema '{ipxactSchema.LocalPath}' for {namespacePrefix} ({namespaceURI}).") from ex 

383 

384 schemaRoot = XML(schema, parser=xmlParser, base_url=ipxactSchema.LocalPath.as_uri()) 

385 schemaTree = ElementTree(schemaRoot) 

386 self._xmlSchema = XMLSchema(schemaTree) 

387 

388 try: 

389 self._xmlSchema.assertValid(self._xmlRoot) 

390 except Exception as ex: 

391 raise IPXACTException(f"The input IP-XACT file is not valid according to XML schema {namespaceURI}.") from ex 

392 

393 def ParseVLNVAndDescription(self) -> Tuple[VLNV, str]: 

394 vendor = None 

395 library = None 

396 name = None 

397 version = None 

398 description = None 

399 

400 found = 0 

401 i = iter(self._xmlRoot) 

402 for element in i: 

403 if isinstance(element, _Comment): 

404 continue 

405 

406 elementLocalname = QName(element).localname 

407 if elementLocalname == "vendor": 

408 found |= 1 

409 vendor = element.text 

410 elif elementLocalname == "library": 

411 found |= 2 

412 library = element.text 

413 elif elementLocalname == "name": 

414 found |= 4 

415 name = element.text 

416 elif elementLocalname == "version": 

417 found |= 8 

418 version = element.text 

419 elif elementLocalname == "description": 

420 found |= 16 

421 description = element.text 

422 else: 

423 self.Parse(element) 

424 

425 if found == 31: 

426 break 

427 

428 for element in i: 

429 if isinstance(element, _Comment): 

430 continue 

431 

432 self.Parse(element) 

433 

434 vlnv = VLNV(vendor=vendor, library=library, name=name, version=version) 

435 return vlnv, description 

436 

437 @abstractmethod 

438 def Parse(self, element: _Element) -> None: 

439 pass