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
« 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
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
44from . import Schema
45from .Schema import *
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"
58@export
59class IPXACTException(Exception):
60 """Base-exception for all exceptions in this package."""
63@export
64class IPXACTSchema(metaclass=ExtendedType, slots=True):
65 """Schema descriptor made of version, namespace prefix, URI, URL and local path."""
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
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.
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
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
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
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
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
138 self._namespacePrefix = xmlNamespacePrefix
139 self._schemaUri = schemaUri
140 self._schemaUrl = schemaUrl
141 self._localPath = localPath
143 @readonly
144 def Version(self) -> Union[SemanticVersion, CalendarVersion]:
145 return self._version
147 @readonly
148 def NamespacePrefix(self) -> str:
149 return self._namespacePrefix
151 @readonly
152 def SchemaUri(self) -> str:
153 return self._schemaUri
155 @readonly
156 def SchemaUrl(self) -> str:
157 return self._schemaUrl
159 @readonly
160 def LocalPath(self) -> Path:
161 return self._localPath
163 def __repr__(self) -> str:
164 return f"<{self.__class__.__name__} IP-XACT {self._version} {self._schemaUri} - {self._localPath}>"
166 def __str__(self) -> str:
167 return f"IP-XACT {self._version}"
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)
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.
190__URI_MAP__: Dict[str, IPXACTSchema] = {value.SchemaUri: value for key, value in __VERSION_TABLE__.items()} #: Mapping from schema URIs to :class:`IpxactSchema` instances.
192__DEFAULT_VERSION__ = "2022" #: IP-XACT default version
193__DEFAULT_SCHEMA__ = __VERSION_TABLE__[__DEFAULT_VERSION__] #: IP-XACT default Schema
196@export
197class VLNV(metaclass=ExtendedType, slots=True):
198 """VLNV data structure (Vendor, Library, Name, Version) as a unique identifier in IP-XACT."""
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
205 def __init__(self, vendor: str, library: str, name: str, version: Union[str, SemanticVersion]) -> None:
206 """
207 Initializes the VLNV data structure.
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 """
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
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
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
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
251 self._vendor = vendor
252 self._library = library
253 self._name = name
255 @readonly
256 def Vendor(self) -> str:
257 return self._vendor
259 @readonly
260 def Library(self) -> str:
261 return self._library
263 @readonly
264 def Name(self) -> str:
265 return self._name
267 @readonly
268 def Version(self) -> SemanticVersion:
269 return self._version
271 def ToXml(self, indent=1, schema: IPXACTSchema = __DEFAULT_SCHEMA__, isVersionedIdentifier=False) -> str:
272 """
273 Converts the object's data into XML format.
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 """
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
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}"/>"""
299@export
300class Element(metaclass=ExtendedType, slots=True):
301 """Base-class for all IP-XACT elements."""
303 def __init__(self, vlnv: VLNV) -> None:
304 """
305 Initializes the Element class.
306 """
309@export
310class NamedElement(Element):
311 """Base-class for all IP-XACT elements with a VLNV."""
313 _vlnv: VLNV #: VLNV unique identifier.
315 def __init__(self, vlnv: VLNV) -> None:
316 """
317 Initializes the NameElement with an VLNV field for all derives classes.
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
328 self._vlnv = vlnv
330 @readonly
331 def VLNV(self) -> VLNV:
332 return self._vlnv
335@export
336class RootElement(NamedElement):
337 """Base-class for all IP-XACT root elements."""
339 _file: Nullable[Path]
340 _rootTagName: ClassVar[str] = ""
341 _xmlRoot: Nullable[_Element]
342 _xmlSchema: Nullable[_Element]
344 _description: str
346 def __init__(self, file: Nullable[Path] = None, parse: bool = False, vlnv: Nullable[VLNV] = None, description: Nullable[str] = None) -> None:
347 self._description = description
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()
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
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))
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
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)
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.")
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}'.")
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
396 schemaRoot = XML(schema, parser=xmlParser, base_url=ipxactSchema.LocalPath.as_uri())
397 schemaTree = ElementTree(schemaRoot)
398 self._xmlSchema = XMLSchema(schemaTree)
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
405 def ParseVLNVAndDescription(self) -> Tuple[VLNV, str]:
406 vendor = None
407 library = None
408 name = None
409 version = None
410 description = None
412 found = 0
413 i = iter(self._xmlRoot)
414 for element in i:
415 if isinstance(element, _Comment):
416 continue
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)
437 if found == 31:
438 break
440 for element in i:
441 if isinstance(element, _Comment):
442 continue
444 self.Parse(element)
446 vlnv = VLNV(vendor=vendor, library=library, name=name, version=version)
447 return vlnv, description
449 @abstractmethod
450 def Parse(self, element: _Element) -> None:
451 pass