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
« 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
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-2025, Patrick Lehmann"
50__license__ = "Apache License, Version 2.0"
51__version__ = "0.6.1"
54@export
55class IPXACTException(Exception):
56 """Base-exception for all exceptions in this package."""
59@export
60class IPXACTSchema(metaclass=ExtendedType, slots=True):
61 """Schema descriptor made of version, namespace prefix, URI, URL and local path."""
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
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.
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
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
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
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
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
134 self._namespacePrefix = xmlNamespacePrefix
135 self._schemaUri = schemaUri
136 self._schemaUrl = schemaUrl
137 self._localPath = localPath
139 @readonly
140 def Version(self) -> Union[SemanticVersion, CalendarVersion]:
141 return self._version
143 @readonly
144 def NamespacePrefix(self) -> str:
145 return self._namespacePrefix
147 @readonly
148 def SchemaUri(self) -> str:
149 return self._schemaUri
151 @readonly
152 def SchemaUrl(self) -> str:
153 return self._schemaUrl
155 @readonly
156 def LocalPath(self) -> Path:
157 return self._localPath
159 def __repr__(self) -> str:
160 return f"<{self.__class__.__name__} IP-XACT {self._version} {self._schemaUri} - {self._localPath}>"
162 def __str__(self) -> str:
163 return f"IP-XACT {self._version}"
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)
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.
186__URI_MAP__: Dict[str, IPXACTSchema] = {value.SchemaUri: value for key, value in __VERSION_TABLE__.items()} #: Mapping from schema URIs to :class:`IpxactSchema` instances.
188__DEFAULT_VERSION__ = "2022" #: IP-XACT default version
189__DEFAULT_SCHEMA__ = __VERSION_TABLE__[__DEFAULT_VERSION__] #: IP-XACT default Schema
192@export
193class VLNV(metaclass=ExtendedType, slots=True):
194 """VLNV data structure (Vendor, Library, Name, Version) as a unique identifier in IP-XACT."""
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
201 def __init__(self, vendor: str, library: str, name: str, version: Union[str, SemanticVersion]) -> None:
202 """
203 Initializes the VLNV data structure.
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 """
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
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
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
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
247 self._vendor = vendor
248 self._library = library
249 self._name = name
251 @readonly
252 def Vendor(self) -> str:
253 return self._vendor
255 @readonly
256 def Library(self) -> str:
257 return self._library
259 @readonly
260 def Name(self) -> str:
261 return self._name
263 @readonly
264 def Version(self) -> SemanticVersion:
265 return self._version
267 def ToXml(self, indent=1, schema: IPXACTSchema = __DEFAULT_SCHEMA__, isVersionedIdentifier=False) -> str:
268 """
269 Converts the object's data into XML format.
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 """
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
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}"/>"""
295@export
296class Element(metaclass=ExtendedType, slots=True):
297 """Base-class for all IP-XACT elements."""
299 def __init__(self, vlnv: VLNV) -> None:
300 """
301 Initializes the Element class.
302 """
305@export
306class NamedElement(Element):
307 """Base-class for all IP-XACT elements with a VLNV."""
309 _vlnv: VLNV #: VLNV unique identifier.
311 def __init__(self, vlnv: VLNV) -> None:
312 """
313 Initializes the NameElement with an VLNV field for all derives classes.
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
324 self._vlnv = vlnv
326 @readonly
327 def VLNV(self) -> VLNV:
328 return self._vlnv
331@export
332class RootElement(NamedElement):
333 """Base-class for all IP-XACT root elements."""
335 _file: Nullable[Path]
336 _rootTagName: ClassVar[str] = ""
337 _xmlRoot: Nullable[_Element]
338 _xmlSchema: Nullable[_Element]
340 _description: str
342 def __init__(self, file: Nullable[Path] = None, parse: bool = False, vlnv: Nullable[VLNV] = None, description: Nullable[str] = None) -> None:
343 self._description = description
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()
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
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))
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
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)
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.")
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}'.")
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
392 schemaRoot = XML(schema, parser=xmlParser, base_url=ipxactSchema.LocalPath.as_uri())
393 schemaTree = ElementTree(schemaRoot)
394 self._xmlSchema = XMLSchema(schemaTree)
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
401 def ParseVLNVAndDescription(self) -> Tuple[VLNV, str]:
402 vendor = None
403 library = None
404 name = None
405 version = None
406 description = None
408 found = 0
409 i = iter(self._xmlRoot)
410 for element in i:
411 if isinstance(element, _Comment):
412 continue
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)
433 if found == 31:
434 break
436 for element in i:
437 if isinstance(element, _Comment):
438 continue
440 self.Parse(element)
442 vlnv = VLNV(vendor=vendor, library=library, name=name, version=version)
443 return vlnv, description
445 @abstractmethod
446 def Parse(self, element: _Element) -> None:
447 pass