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

235 statements  

« prev     ^ index     » next       coverage.py v7.15.1, created at 2026-07-13 18:04 +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 sys import version_info 

35from textwrap import dedent 

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

37 

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

39from pyTooling.Decorators import export, readonly 

40from pyTooling.MetaClasses import ExtendedType, abstractmethod 

41from pyTooling.Common import getFullyQualifiedName 

42from pyTooling.Versioning import SemanticVersion, CalendarVersion 

43 

44from . import Schema 

45from .Schema import * 

46 

47__author__ = "Patrick Lehmann" 

48__email__ = "Paebbels@gmail.com" 

49__copyright__ = "2016-2026, Patrick Lehmann" 

50__license__ = "Apache License, Version 2.0" 

51__version__ = "0.6.6" 

52# __keywords__ = [] 

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

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

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

56 

57 

58@export 

59class IPXACTException(Exception): 

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

61 

62 

63@export 

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

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

66 

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

68 _namespacePrefix: str #: XML namespace prefix 

69 _schemaUri: str #: Schema URI 

70 _schemaUrl: str #: Schema URL 

71 _localPath: Path #: Local path 

72 

73 def __init__( 

74 self, 

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

76 xmlNamespacePrefix: str, 

77 schemaUri: str, 

78 schemaUrl: str, 

79 localPath: Path 

80 ) -> None: 

81 """ 

82 Initializes an IP-XACT Schema description. 

83 

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

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

86 :param schemaUri: IP-XACT schema URI 

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

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

89 """ 

90 # TODO: add raises ... lines 

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

92 raise ValueError(f"Parameter 'version' is None.") 

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

94 if version.startswith("20"): 

95 self._version = CalendarVersion.Parse(version) 

96 else: 

97 self._version = SemanticVersion.Parse(version) 

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

99 self._version = version 

100 else: 

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

102 if version_info >= (3, 11): # pragma: no cover 

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

104 raise ex 

105 

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

107 raise ValueError(f"Parameter 'namespacePrefix' is None.") 

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

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

110 if version_info >= (3, 11): # pragma: no cover 

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

112 raise ex 

113 

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

115 raise ValueError(f"Parameter 'schemaUri' is None.") 

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

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

118 if version_info >= (3, 11): # pragma: no cover 

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

120 raise ex 

121 

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

123 raise ValueError(f"Parameter 'schemaUrl' is None.") 

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

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

126 if version_info >= (3, 11): # pragma: no cover 

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

128 raise ex 

129 

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

131 raise ValueError(f"Parameter 'localPath' is None.") 

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

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

134 if version_info >= (3, 11): # pragma: no cover 

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

136 raise ex 

137 

138 self._namespacePrefix = xmlNamespacePrefix 

139 self._schemaUri = schemaUri 

140 self._schemaUrl = schemaUrl 

141 self._localPath = localPath 

142 

143 @readonly 

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

145 return self._version 

146 

147 @readonly 

148 def NamespacePrefix(self) -> str: 

149 return self._namespacePrefix 

150 

151 @readonly 

152 def SchemaUri(self) -> str: 

153 return self._schemaUri 

154 

155 @readonly 

156 def SchemaUrl(self) -> str: 

157 return self._schemaUrl 

158 

159 @readonly 

160 def LocalPath(self) -> Path: 

161 return self._localPath 

162 

163 def __repr__(self) -> str: 

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

165 

166 def __str__(self) -> str: 

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

168 

169 

170# version, xmlns, URI URL, Local Path 

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

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

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

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

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

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

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

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

179 

180__VERSION_TABLE__: Dict[str, IPXACTSchema] = { 

181 '1.0': _IPXACT_10, 

182 '1.1': _IPXACT_11, 

183 '1.4': _IPXACT_14, 

184 '1.5': _IPXACT_15, 

185 '2009': _IPXACT_2009, 

186 '2014': _IPXACT_2014, 

187 '2022': _IPXACT_2022 

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

189 

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

191 

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

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

194 

195 

196@export 

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

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

199 

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

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

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

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

204 

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

206 """ 

207 Initializes the VLNV data structure. 

208 

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

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

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

212 :param version: Version in a VLNV unique identifier 

213 """ 

214 

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

216 raise ValueError(f"Parameter 'vendor' is None.") 

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

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

219 if version_info >= (3, 11): # pragma: no cover 

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

221 raise ex 

222 

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

224 raise ValueError(f"Parameter 'library' is None.") 

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

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

227 if version_info >= (3, 11): # pragma: no cover 

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

229 raise ex 

230 

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

232 raise ValueError(f"Parameter 'name' is None.") 

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

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

235 if version_info >= (3, 11): # pragma: no cover 

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

237 raise ex 

238 

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

240 raise ValueError(f"Parameter 'version' is None.") 

241 elif isinstance(version, str): 

242 self._version = SemanticVersion.Parse(version) 

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

244 self._version = version 

245 else: 

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

247 if version_info >= (3, 11): # pragma: no cover 

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

249 raise ex 

250 

251 self._vendor = vendor 

252 self._library = library 

253 self._name = name 

254 

255 @readonly 

256 def Vendor(self) -> str: 

257 return self._vendor 

258 

259 @readonly 

260 def Library(self) -> str: 

261 return self._library 

262 

263 @readonly 

264 def Name(self) -> str: 

265 return self._name 

266 

267 @readonly 

268 def Version(self) -> SemanticVersion: 

269 return self._version 

270 

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

272 """ 

273 Converts the object's data into XML format. 

274 

275 :param indent: Level of indentations. 

276 :param schema: XML schema. 

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

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

279 :returns: XML formatted string representation. 

280 """ 

281 

282 # WORKAROUND: 

283 # Python <=3.11: 

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

285 indent = "\t" * indent 

286 xmlns = schema.NamespacePrefix 

287 

288 if isVersionedIdentifier: 

289 return dedent(f"""\ 

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

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

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

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

294 """) 

295 else: 

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

297 

298 

299@export 

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

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

302 

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

304 """ 

305 Initializes the Element class. 

306 """ 

307 

308 

309@export 

310class NamedElement(Element): 

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

312 

313 _vlnv: VLNV #: VLNV unique identifier. 

314 

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

316 """ 

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

318 

319 :param vlnv: VLNV unique identifier. 

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

321 """ 

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

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

324 if version_info >= (3, 11): # pragma: no cover 

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

326 raise ex 

327 

328 self._vlnv = vlnv 

329 

330 @readonly 

331 def VLNV(self) -> VLNV: 

332 return self._vlnv 

333 

334 

335@export 

336class RootElement(NamedElement): 

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

338 

339 _file: Nullable[Path] 

340 _rootTagName: ClassVar[str] = "" 

341 _xmlRoot: Nullable[_Element] 

342 _xmlSchema: Nullable[_Element] 

343 

344 _description: str 

345 

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

347 self._description = description 

348 

349 if file is None: 

350 super().__init__(vlnv) 

351 self._file = None 

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

353 self._file = file 

354 vlnv = None 

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

356 self.OpenAndValidate() 

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

358 

359 super().__init__(vlnv) 

360 else: 

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

362 if version_info >= (3, 11): # pragma: no cover 

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

364 raise ex 

365 

366 def OpenAndValidate(self) -> None: 

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

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

369 

370 try: 

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

372 content = fileHandle.read() 

373 except OSError as ex: 

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

375 

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

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

378 rootTag = QName(self._xmlRoot.tag) 

379 

380 if rootTag.localname != self._rootTagName: 380 ↛ 381line 380 didn't jump to line 381 because the condition on line 380 was never true

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

382 

383 namespacePrefix = self._xmlRoot.prefix 

384 namespaceURI = self._xmlRoot.nsmap[namespacePrefix] 

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

386 ipxactSchema = __URI_MAP__[namespaceURI] 

387 else: 

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

389 

390 try: 

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

392 schema = fileHandle.read() 

393 except OSError as ex: 

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

395 

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

397 schemaTree = ElementTree(schemaRoot) 

398 self._xmlSchema = XMLSchema(schemaTree) 

399 

400 try: 

401 self._xmlSchema.assertValid(self._xmlRoot) 

402 except Exception as ex: 

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

404 

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

406 vendor = None 

407 library = None 

408 name = None 

409 version = None 

410 description = None 

411 

412 found = 0 

413 i = iter(self._xmlRoot) 

414 for element in i: 

415 if isinstance(element, _Comment): 

416 continue 

417 

418 elementLocalname = QName(element).localname 

419 if elementLocalname == "vendor": 

420 found |= 1 

421 vendor = element.text 

422 elif elementLocalname == "library": 

423 found |= 2 

424 library = element.text 

425 elif elementLocalname == "name": 

426 found |= 4 

427 name = element.text 

428 elif elementLocalname == "version": 

429 found |= 8 

430 version = element.text 

431 elif elementLocalname == "description": 

432 found |= 16 

433 description = element.text 

434 else: 

435 self.Parse(element) 

436 

437 if found == 31: 

438 break 

439 

440 for element in i: 

441 if isinstance(element, _Comment): 

442 continue 

443 

444 self.Parse(element) 

445 

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

447 return vlnv, description 

448 

449 @abstractmethod 

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

451 pass