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

246 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-05-30 22:17 +0000

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

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

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

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

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

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

7# |_| |___/ # 

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2017-2025 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-2025, Patrick Lehmann" 

50__license__ = "Apache License, Version 2.0" 

51__version__ = "0.6.1" 

52 

53 

54@export 

55class IPXACTException(Exception): 

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

57 

58 

59@export 

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

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

62 

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

64 _namespacePrefix: str #: XML namespace prefix 

65 _schemaUri: str #: Schema URI 

66 _schemaUrl: str #: Schema URL 

67 _localPath: Path #: Local path 

68 

69 def __init__( 

70 self, 

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

72 xmlNamespacePrefix: str, 

73 schemaUri: str, 

74 schemaUrl: str, 

75 localPath: Path 

76 ) -> None: 

77 """ 

78 Initializes an IP-XACT Schema description. 

79 

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

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

82 :param schemaUri: IP-XACT schema URI 

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

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

85 """ 

86 # TODO: add raises ... lines 

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

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

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

90 if version.startswith("20"): 

91 self._version = CalendarVersion.Parse(version) 

92 else: 

93 self._version = SemanticVersion.Parse(version) 

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

95 self._version = version 

96 else: 

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

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

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

100 raise ex 

101 

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

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

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

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

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

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

108 raise ex 

109 

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

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

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

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

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

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(f"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(f"Parameter 'schemaUrl' is not a string.") 

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

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

124 raise ex 

125 

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

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

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

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

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

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

132 raise ex 

133 

134 self._namespacePrefix = xmlNamespacePrefix 

135 self._schemaUri = schemaUri 

136 self._schemaUrl = schemaUrl 

137 self._localPath = localPath 

138 

139 @readonly 

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

141 return self._version 

142 

143 @readonly 

144 def NamespacePrefix(self) -> str: 

145 return self._namespacePrefix 

146 

147 @readonly 

148 def SchemaUri(self) -> str: 

149 return self._schemaUri 

150 

151 @readonly 

152 def SchemaUrl(self) -> str: 

153 return self._schemaUrl 

154 

155 @readonly 

156 def LocalPath(self) -> Path: 

157 return self._localPath 

158 

159 def __repr__(self) -> str: 

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

161 

162 def __str__(self) -> str: 

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

164 

165 

166# version, xmlns, URI URL, Local Path 

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

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

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

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

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

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

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

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

175 

176__VERSION_TABLE__: Dict[str, IPXACTSchema] = { 

177 '1.0': _IPXACT_10, 

178 '1.1': _IPXACT_11, 

179 '1.4': _IPXACT_14, 

180 '1.5': _IPXACT_15, 

181 '2009': _IPXACT_2009, 

182 '2014': _IPXACT_2014, 

183 '2022': _IPXACT_2022 

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

185 

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

187 

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

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

190 

191 

192@export 

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

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

195 

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

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

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

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

200 

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

202 """ 

203 Initializes the VLNV data structure. 

204 

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

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

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

208 :param version: Version in a VLNV unique identifier 

209 """ 

210 

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

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

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

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

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

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

217 raise ex 

218 

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

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

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

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

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

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

225 raise ex 

226 

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

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

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

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

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

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

233 raise ex 

234 

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

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

237 elif isinstance(version, str): 

238 self._version = SemanticVersion.Parse(version) 

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

240 self._version = version 

241 else: 

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

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

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

245 raise ex 

246 

247 self._vendor = vendor 

248 self._library = library 

249 self._name = name 

250 

251 @readonly 

252 def Vendor(self) -> str: 

253 return self._vendor 

254 

255 @readonly 

256 def Library(self) -> str: 

257 return self._library 

258 

259 @readonly 

260 def Name(self) -> str: 

261 return self._name 

262 

263 @readonly 

264 def Version(self) -> SemanticVersion: 

265 return self._version 

266 

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

268 """ 

269 Converts the object's data into XML format. 

270 

271 :param indent: Level of indentations. 

272 :param schema: XML schema. 

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

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

275 :return: 

276 """ 

277 

278 # WORKAROUND: 

279 # Python <=3.11: 

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

281 indent = "\t" * indent 

282 xmlns = schema.NamespacePrefix 

283 

284 if isVersionedIdentifier: 

285 return dedent(f"""\ 

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

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

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

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

290 """) 

291 else: 

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

293 

294 

295@export 

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

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

298 

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

300 """ 

301 Initializes the Element class. 

302 """ 

303 

304 

305@export 

306class NamedElement(Element): 

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

308 

309 _vlnv: VLNV #: VLNV unique identifier. 

310 

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

312 """ 

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

314 

315 :param vlnv: VLNV unique identifier. 

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

317 """ 

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

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

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

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

322 raise ex 

323 

324 self._vlnv = vlnv 

325 

326 @readonly 

327 def VLNV(self) -> VLNV: 

328 return self._vlnv 

329 

330 

331@export 

332class RootElement(NamedElement): 

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

334 

335 _file: Nullable[Path] 

336 _rootTagName: ClassVar[str] = "" 

337 _xmlRoot: Nullable[_Element] 

338 _xmlSchema: Nullable[_Element] 

339 

340 _description: str 

341 

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

343 self._description = description 

344 

345 if file is None: 

346 super().__init__(vlnv) 

347 self._file = None 

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

349 self._file = file 

350 vlnv = None 

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

352 self.OpenAndValidate() 

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

354 

355 super().__init__(vlnv) 

356 else: 

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

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

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

360 raise ex 

361 

362 def OpenAndValidate(self) -> None: 

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

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

365 

366 try: 

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

368 content = fileHandle.read() 

369 except OSError as ex: 

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

371 

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

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

374 rootTag = QName(self._xmlRoot.tag) 

375 

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

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

378 

379 namespacePrefix = self._xmlRoot.prefix 

380 namespaceURI = self._xmlRoot.nsmap[namespacePrefix] 

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

382 ipxactSchema = __URI_MAP__[namespaceURI] 

383 else: 

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

385 

386 try: 

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

388 schema = fileHandle.read() 

389 except OSError as ex: 

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

391 

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

393 schemaTree = ElementTree(schemaRoot) 

394 self._xmlSchema = XMLSchema(schemaTree) 

395 

396 try: 

397 self._xmlSchema.assertValid(self._xmlRoot) 

398 except Exception as ex: 

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

400 

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

402 vendor = None 

403 library = None 

404 name = None 

405 version = None 

406 description = None 

407 

408 found = 0 

409 i = iter(self._xmlRoot) 

410 for element in i: 

411 if isinstance(element, _Comment): 

412 continue 

413 

414 elementLocalname = QName(element).localname 

415 if elementLocalname == "vendor": 

416 found |= 1 

417 vendor = element.text 

418 elif elementLocalname == "library": 

419 found |= 2 

420 library = element.text 

421 elif elementLocalname == "name": 

422 found |= 4 

423 name = element.text 

424 elif elementLocalname == "version": 

425 found |= 8 

426 version = element.text 

427 elif elementLocalname == "description": 

428 found |= 16 

429 description = element.text 

430 else: 

431 self.Parse(element) 

432 

433 if found == 31: 

434 break 

435 

436 for element in i: 

437 if isinstance(element, _Comment): 

438 continue 

439 

440 self.Parse(element) 

441 

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

443 return vlnv, description 

444 

445 @abstractmethod 

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

447 pass