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
« 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
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
43from . import Schema
44from .Schema import *
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"
57@export
58class IPXACTException(Exception):
59 """Base-exception for all exceptions in this package."""
62@export
63class IPXACTSchema(metaclass=ExtendedType, slots=True):
64 """Schema descriptor made of version, namespace prefix, URI, URL and local path."""
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
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.
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
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
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
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
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
132 self._namespacePrefix = xmlNamespacePrefix
133 self._schemaUri = schemaUri
134 self._schemaUrl = schemaUrl
135 self._localPath = localPath
137 @readonly
138 def Version(self) -> Union[SemanticVersion, CalendarVersion]:
139 return self._version
141 @readonly
142 def NamespacePrefix(self) -> str:
143 return self._namespacePrefix
145 @readonly
146 def SchemaUri(self) -> str:
147 return self._schemaUri
149 @readonly
150 def SchemaUrl(self) -> str:
151 return self._schemaUrl
153 @readonly
154 def LocalPath(self) -> Path:
155 return self._localPath
157 def __repr__(self) -> str:
158 return f"<{self.__class__.__name__} IP-XACT {self._version} {self._schemaUri} - {self._localPath}>"
160 def __str__(self) -> str:
161 return f"IP-XACT {self._version}"
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)
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.
184__URI_MAP__: Dict[str, IPXACTSchema] = {value.SchemaUri: value for key, value in __VERSION_TABLE__.items()} #: Mapping from schema URIs to :class:`IpxactSchema` instances.
186__DEFAULT_VERSION__ = "2022" #: IP-XACT default version
187__DEFAULT_SCHEMA__ = __VERSION_TABLE__[__DEFAULT_VERSION__] #: IP-XACT default Schema
190@export
191class VLNV(metaclass=ExtendedType, slots=True):
192 """VLNV data structure (Vendor, Library, Name, Version) as a unique identifier in IP-XACT."""
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
199 def __init__(self, vendor: str, library: str, name: str, version: Union[str, SemanticVersion]) -> None:
200 """
201 Initializes the VLNV data structure.
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 """
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
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
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
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
241 self._vendor = vendor
242 self._library = library
243 self._name = name
245 @readonly
246 def Vendor(self) -> str:
247 return self._vendor
249 @readonly
250 def Library(self) -> str:
251 return self._library
253 @readonly
254 def Name(self) -> str:
255 return self._name
257 @readonly
258 def Version(self) -> SemanticVersion:
259 return self._version
261 def ToXml(self, indent=1, schema: IPXACTSchema = __DEFAULT_SCHEMA__, isVersionedIdentifier=False) -> str:
262 """
263 Converts the object's data into XML format.
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 """
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
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}"/>"""
289@export
290class Element(metaclass=ExtendedType, slots=True):
291 """Base-class for all IP-XACT elements."""
293 def __init__(self, vlnv: VLNV) -> None:
294 """
295 Initializes the Element class.
296 """
299@export
300class NamedElement(Element):
301 """Base-class for all IP-XACT elements with a VLNV."""
303 _vlnv: VLNV #: VLNV unique identifier.
305 def __init__(self, vlnv: VLNV) -> None:
306 """
307 Initializes the NameElement with an VLNV field for all derives classes.
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
317 self._vlnv = vlnv
319 @readonly
320 def VLNV(self) -> VLNV:
321 return self._vlnv
324@export
325class RootElement(NamedElement):
326 """Base-class for all IP-XACT root elements."""
328 _file: Nullable[Path]
329 _ROOT_TAG_NAME: ClassVar[str] = ""
330 _xmlRoot: Nullable[_Element]
331 _xmlSchema: Nullable[_Element]
333 _description: str
335 def __init__(self, file: Nullable[Path] = None, parse: bool = False, vlnv: Nullable[VLNV] = None, description: Nullable[str] = None) -> None:
336 self._description = description
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()
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
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))
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
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)
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.")
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}'.")
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
384 schemaRoot = XML(schema, parser=xmlParser, base_url=ipxactSchema.LocalPath.as_uri())
385 schemaTree = ElementTree(schemaRoot)
386 self._xmlSchema = XMLSchema(schemaTree)
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
393 def ParseVLNVAndDescription(self) -> Tuple[VLNV, str]:
394 vendor = None
395 library = None
396 name = None
397 version = None
398 description = None
400 found = 0
401 i = iter(self._xmlRoot)
402 for element in i:
403 if isinstance(element, _Comment):
404 continue
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)
425 if found == 31:
426 break
428 for element in i:
429 if isinstance(element, _Comment):
430 continue
432 self.Parse(element)
434 vlnv = VLNV(vendor=vendor, library=library, name=name, version=version)
435 return vlnv, description
437 @abstractmethod
438 def Parse(self, element: _Element) -> None:
439 pass