Source code for pyEDAA.IPXACT

# ==================================================================================================================== #
#              _____ ____    _        _      ___ ______  __    _    ____ _____                                         #
#  _ __  _   _| ____|  _ \  / \      / \    |_ _|  _ \ \/ /   / \  / ___|_   _|                                        #
# | '_ \| | | |  _| | | | |/ _ \    / _ \    | || |_) \  /   / _ \| |     | |                                          #
# | |_) | |_| | |___| |_| / ___ \  / ___ \ _ | ||  __//  \  / ___ \ |___  | |                                          #
# | .__/ \__, |_____|____/_/   \_\/_/   \_(_)___|_|  /_/\_\/_/   \_\____| |_|                                          #
# |_|    |___/                                                                                                         #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2017-2025 Patrick Lehmann - Bötzingen, Germany                                                             #
# Copyright 2016-2016 Patrick Lehmann - Dresden, Germany                                                               #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""A DOM based IP-XACT implementation for Python."""
from pathlib  import Path
from sys      import version_info
from textwrap import dedent
from typing   import Union, Dict, Tuple, Optional as Nullable, ClassVar

from lxml.etree            import XMLParser, XML, XMLSchema, ElementTree, QName, _Element, _Comment
from pyTooling.Decorators  import export, readonly
from pyTooling.MetaClasses import ExtendedType, abstractmethod
from pyTooling.Common      import getFullyQualifiedName
from pyTooling.Versioning  import SemanticVersion, CalendarVersion

from .       import Schema
from .Schema import *

__author__ =    "Patrick Lehmann"
__email__ =     "Paebbels@gmail.com"
__copyright__ = "2016-2025, Patrick Lehmann"
__license__ =   "Apache License, Version 2.0"
__version__ =   "0.6.1"


[docs] @export class IPXACTException(Exception): """Base-exception for all exceptions in this package."""
[docs] @export class IPXACTSchema(metaclass=ExtendedType, slots=True): """Schema descriptor made of version, namespace prefix, URI, URL and local path.""" _version: Union[SemanticVersion, CalendarVersion] #: Schema version _namespacePrefix: str #: XML namespace prefix _schemaUri: str #: Schema URI _schemaUrl: str #: Schema URL _localPath: Path #: Local path
[docs] def __init__( self, version: Union[str, SemanticVersion, CalendarVersion], xmlNamespacePrefix: str, schemaUri: str, schemaUrl: str, localPath: Path ) -> None: """ Initializes an IP-XACT Schema description. :param version: Version of the IP-XACT Schema. :param xmlNamespacePrefix: XML namespace prefix (``<prefix:element>``) :param schemaUri: IP-XACT schema URI :param schemaUrl: URL the IP-XACT schema definition file (XSD). :param localPath: Path to the local XSD file. """ # TODO: add raises ... lines if version is None: raise ValueError(f"Parameter 'version' is None.") elif isinstance(version, str): if version.startswith("20"): self._version = CalendarVersion.Parse(version) else: self._version = SemanticVersion.Parse(version) elif isinstance(version, (SemanticVersion, CalendarVersion)): self._version = version else: ex = TypeError(f"Parameter 'version' is neither a 'SemanticVersion', a 'CalendarVersion' nor a string.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") raise ex if xmlNamespacePrefix is None: raise ValueError(f"Parameter 'namespacePrefix' is None.") elif not isinstance(xmlNamespacePrefix, str): ex = TypeError(f"Parameter 'namespacePrefix' is not a string.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") raise ex if schemaUri is None: raise ValueError(f"Parameter 'schemaUri' is None.") elif not isinstance(schemaUri, str): ex = TypeError(f"Parameter 'schemaUri' is not a string.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(schemaUri)}'.") raise ex if schemaUrl is None: raise ValueError(f"Parameter 'schemaUrl' is None.") elif not isinstance(schemaUrl, str): ex = TypeError(f"Parameter 'schemaUrl' is not a string.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(schemaUrl)}'.") raise ex if localPath is None: raise ValueError(f"Parameter 'localPath' is None.") elif not isinstance(localPath, Path): ex = TypeError(f"Parameter 'localPath' is not a Path.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(localPath)}'.") raise ex self._namespacePrefix = xmlNamespacePrefix self._schemaUri = schemaUri self._schemaUrl = schemaUrl self._localPath = localPath
@readonly def Version(self) -> Union[SemanticVersion, CalendarVersion]: return self._version @readonly def NamespacePrefix(self) -> str: return self._namespacePrefix @readonly def SchemaUri(self) -> str: return self._schemaUri @readonly def SchemaUrl(self) -> str: return self._schemaUrl @readonly def LocalPath(self) -> Path: return self._localPath
[docs] def __repr__(self) -> str: return f"<{self.__class__.__name__} IP-XACT {self._version} {self._schemaUri} - {self._localPath}>"
[docs] def __str__(self) -> str: return f"IP-XACT {self._version}"
# version, xmlns, URI URL, Local Path _IPXACT_10 = IPXACTSchema("1.0", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.0", "", _IPXACT_10_INDEX) _IPXACT_11 = IPXACTSchema("1.1", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.1", "", _IPXACT_11_INDEX) _IPXACT_12 = IPXACTSchema("1.2", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.2", "", _IPXACT_12_INDEX) _IPXACT_14 = IPXACTSchema("1.4", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.4", "", _IPXACT_14_INDEX) _IPXACT_15 = IPXACTSchema("1.5", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.5", "", _IPXACT_15_INDEX) _IPXACT_2009 = IPXACTSchema("2009", "spirit", "http://www.spiritconsortium.org/XMLSchema/SPIRIT/1685-2009", "", _IPXACT_2009_INDEX) _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) _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) __VERSION_TABLE__: Dict[str, IPXACTSchema] = { '1.0': _IPXACT_10, '1.1': _IPXACT_11, '1.4': _IPXACT_14, '1.5': _IPXACT_15, '2009': _IPXACT_2009, '2014': _IPXACT_2014, '2022': _IPXACT_2022 } #: Dictionary of all IP-XACT versions mapping to :class:`IpxactSchema` instances. __URI_MAP__: Dict[str, IPXACTSchema] = {value.SchemaUri: value for key, value in __VERSION_TABLE__.items()} #: Mapping from schema URIs to :class:`IpxactSchema` instances. __DEFAULT_VERSION__ = "2022" #: IP-XACT default version __DEFAULT_SCHEMA__ = __VERSION_TABLE__[__DEFAULT_VERSION__] #: IP-XACT default Schema
[docs] @export class VLNV(metaclass=ExtendedType, slots=True): """VLNV data structure (Vendor, Library, Name, Version) as a unique identifier in IP-XACT.""" _vendor: str #: Vendor name in a VLNV unique identifier _library: str #: Library name in a VLNV unique identifier _name: str #: Component name in a VLNV unique identifier _version: SemanticVersion #: Version in a VLNV unique identifier
[docs] def __init__(self, vendor: str, library: str, name: str, version: Union[str, SemanticVersion]) -> None: """ Initializes the VLNV data structure. :param vendor: Vendor name in a VLNV unique identifier :param library: Library name in a VLNV unique identifier :param name: Component name in a VLNV unique identifier :param version: Version in a VLNV unique identifier """ if vendor is None: raise ValueError(f"Parameter 'vendor' is None.") elif not isinstance(vendor, str): ex = TypeError(f"Parameter 'vendor' is not a string.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(vendor)}'.") raise ex if library is None: raise ValueError(f"Parameter 'library' is None.") elif not isinstance(library, str): ex = TypeError(f"Parameter 'library' is not a string.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(library)}'.") raise ex if name is None: raise ValueError(f"Parameter 'name' is None.") elif not isinstance(name, str): ex = TypeError(f"Parameter 'name' is not a string.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") raise ex if version is None: raise ValueError(f"Parameter 'version' is None.") elif isinstance(version, str): self._version = SemanticVersion.Parse(version) elif isinstance(version, SemanticVersion): self._version = version else: ex = TypeError(f"Parameter 'version' is neither a 'SemanticVersion' nor a string.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") raise ex self._vendor = vendor self._library = library self._name = name
@readonly def Vendor(self) -> str: return self._vendor @readonly def Library(self) -> str: return self._library @readonly def Name(self) -> str: return self._name @readonly def Version(self) -> SemanticVersion: return self._version
[docs] def ToXml(self, indent=1, schema: IPXACTSchema = __DEFAULT_SCHEMA__, isVersionedIdentifier=False) -> str: """ Converts the object's data into XML format. :param indent: Level of indentations. :param schema: XML schema. :param isVersionedIdentifier: If true, generate 4 individual tags (``<vendor>``, ``<library>``, ``<name>``, ``<version>``), otherwise a single ``<vlnv>``-tag with attributes. :return: """ # WORKAROUND: # Python <=3.11: # {'\t' * indent} is not supported by Python before 3.12 due to a backslash within {...} indent = "\t" * indent xmlns = schema.NamespacePrefix if isVersionedIdentifier: return dedent(f"""\ {indent}<{xmlns}:vendor>{self._vendor}</{xmlns}:vendor> {indent}<{xmlns}:library>{self._library}</{xmlns}:library> {indent}<{xmlns}:name>{self._name}</{xmlns}:name> {indent}<{xmlns}:version>{self._version}</{xmlns}:version> """) else: return f"""{indent}<{xmlns}:vlnv vendor="{self._vendor}" library="{self._library}" name="{self._name}" version="{self._version}"/>"""
[docs] @export class Element(metaclass=ExtendedType, slots=True): """Base-class for all IP-XACT elements."""
[docs] def __init__(self, vlnv: VLNV) -> None: """ Initializes the Element class. """
[docs] @export class NamedElement(Element): """Base-class for all IP-XACT elements with a VLNV.""" _vlnv: VLNV #: VLNV unique identifier.
[docs] def __init__(self, vlnv: VLNV) -> None: """ Initializes the NameElement with an VLNV field for all derives classes. :param vlnv: VLNV unique identifier. :raises TypeError: If parameter vlnv is not a VLNV. """ if not isinstance(vlnv, VLNV): ex = TypeError(f"Parameter 'vlnv' is not a VLNV.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(vlnv)}'.") raise ex self._vlnv = vlnv
@readonly def VLNV(self) -> VLNV: return self._vlnv
[docs] @export class RootElement(NamedElement): """Base-class for all IP-XACT root elements.""" _file: Nullable[Path] _rootTagName: ClassVar[str] = "" _xmlRoot: Nullable[_Element] _xmlSchema: Nullable[_Element] _description: str
[docs] def __init__(self, file: Nullable[Path] = None, parse: bool = False, vlnv: Nullable[VLNV] = None, description: Nullable[str] = None) -> None: self._description = description if file is None: super().__init__(vlnv) self._file = None elif isinstance(file, Path): self._file = file vlnv = None if parse: self.OpenAndValidate() vlnv, self._description = self.ParseVLNVAndDescription() super().__init__(vlnv) else: ex = TypeError(f"Parameter 'file' is not a Path.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") raise ex
def OpenAndValidate(self) -> None: if not self._file.exists(): raise IPXACTException(f"IPXACT file '{self._file}' not found.") from FileNotFoundError(str(self._file)) try: with self._file.open("rb") as fileHandle: content = fileHandle.read() except OSError as ex: raise IPXACTException(f"Couldn't open '{self._file}'.") from ex xmlParser = XMLParser(remove_blank_text=True, encoding="utf-8") self._xmlRoot = XML(content, parser=xmlParser, base_url=self._file.resolve().as_uri()) # - relative paths are not supported rootTag = QName(self._xmlRoot.tag) if rootTag.localname != self._rootTagName: raise IPXACTException(f"The input IP-XACT file is not a {self._rootTagName} file.") namespacePrefix = self._xmlRoot.prefix namespaceURI = self._xmlRoot.nsmap[namespacePrefix] if namespaceURI in __URI_MAP__: ipxactSchema = __URI_MAP__[namespaceURI] else: raise IPXACTException(f"The input IP-XACT file uses an unsupported namespace: '{namespaceURI}'.") try: with ipxactSchema.LocalPath.open("rb") as fileHandle: schema = fileHandle.read() except OSError as ex: raise IPXACTException(f"Couldn't open IP-XACT schema '{ipxactSchema.LocalPath}' for {namespacePrefix} ({namespaceURI}).") from ex schemaRoot = XML(schema, parser=xmlParser, base_url=ipxactSchema.LocalPath.as_uri()) schemaTree = ElementTree(schemaRoot) self._xmlSchema = XMLSchema(schemaTree) try: self._xmlSchema.assertValid(self._xmlRoot) except Exception as ex: raise IPXACTException(f"The input IP-XACT file is not valid according to XML schema {namespaceURI}.") from ex def ParseVLNVAndDescription(self) -> Tuple[VLNV, str]: vendor = None library = None name = None version = None description = None found = 0 i = iter(self._xmlRoot) for element in i: if isinstance(element, _Comment): continue elementLocalname = QName(element).localname if elementLocalname == "vendor": found |= 1 vendor = element.text elif elementLocalname == "library": found |= 2 library = element.text elif elementLocalname == "name": found |= 4 name = element.text elif elementLocalname == "version": found |= 8 version = element.text elif elementLocalname == "description": found |= 16 description = element.text else: self.Parse(element) if found == 31: break for element in i: if isinstance(element, _Comment): continue self.Parse(element) vlnv = VLNV(vendor=vendor, library=library, name=name, version=version) return vlnv, description @abstractmethod def Parse(self, element: _Element) -> None: pass