-
-
Notifications
You must be signed in to change notification settings - Fork 56
feat: add support for license expression details #908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
f06071d
2fd2063
fdfea5a
77fd7b7
ebe9f2e
6dda406
2fc6dac
7312022
3dc4fa6
f182193
b641efa
4208cd0
3fe0999
5afc7a3
d9967a6
c75a89f
7f88bff
8f38eb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| License related things | ||
| """ | ||
|
|
||
| from collections.abc import Iterable | ||
| from enum import Enum | ||
| from json import loads as json_loads | ||
| from typing import TYPE_CHECKING, Any, Optional, Union | ||
|
|
@@ -33,8 +34,9 @@ | |
| from .._internal.compare import ComparableTuple as _ComparableTuple | ||
| from ..exception.model import MutuallyExclusivePropertiesException | ||
| from ..exception.serialization import CycloneDxDeserializationException | ||
| from ..schema import SchemaVersion | ||
| from ..schema.schema import SchemaVersion1Dot5, SchemaVersion1Dot6, SchemaVersion1Dot7 | ||
| from . import AttachedText, XsUri | ||
| from . import AttachedText, Property, XsUri | ||
| from .bom_ref import BomRef | ||
|
|
||
|
|
||
|
|
@@ -262,6 +264,122 @@ def __repr__(self) -> str: | |
| return f'<License id={self._id!r}, name={self._name!r}>' | ||
|
|
||
|
|
||
| @serializable.serializable_class(ignore_unknown_during_deserialization=True) | ||
| class ExpressionDetails: | ||
Churro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| This is our internal representation of the `licenseExpressionDetailedType` complex type that specifies the details | ||
| and attributes related to a software license identifier within a CycloneDX BOM document. | ||
|
|
||
| .. note:: | ||
| Introduced in CycloneDX v1.7 | ||
|
|
||
|
|
||
| .. note:: | ||
| See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.7/xml/#type_licenseExpressionDetailedType | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, license_identifier: str, *, | ||
| bom_ref: Optional[Union[str, BomRef]] = None, | ||
| text: Optional[AttachedText] = None, | ||
| url: Optional[XsUri] = None, | ||
| ) -> None: | ||
| self._bom_ref = _bom_ref_from_str(bom_ref) | ||
| self.license_identifier = license_identifier | ||
| self.text = text | ||
| self.url = url | ||
|
|
||
| @property | ||
| @serializable.xml_name('license-identifier') | ||
| @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) | ||
| @serializable.xml_attribute() | ||
| def license_identifier(self) -> str: | ||
| """ | ||
| A valid SPDX license identifier. Refer to https://spdx.org/specifications for syntax requirements. | ||
| This field serves as the primary key, which uniquely identifies each record. | ||
|
|
||
| Example values: | ||
| - "Apache-2.0", | ||
| - "GPL-3.0-only WITH Classpath-exception-2.0" | ||
| - "LicenseRef-my-custom-license" | ||
|
|
||
| Returns: | ||
| `str` | ||
| """ | ||
| return self._license_identifier | ||
|
|
||
| @license_identifier.setter | ||
| def license_identifier(self, license_identifier: str) -> None: | ||
| self._license_identifier = license_identifier | ||
|
|
||
| @property | ||
| @serializable.json_name('bom-ref') | ||
| @serializable.type_mapping(BomRef) | ||
| @serializable.xml_attribute() | ||
| @serializable.xml_name('bom-ref') | ||
| def bom_ref(self) -> BomRef: | ||
| """ | ||
| An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be | ||
| unique within the BOM. | ||
|
|
||
| Returns: | ||
| `BomRef` | ||
| """ | ||
| return self._bom_ref | ||
|
|
||
| @property | ||
| @serializable.xml_sequence(1) | ||
| def text(self) -> Optional[AttachedText]: | ||
| """ | ||
| Specifies the optional full text of the attachment | ||
|
|
||
| Returns: | ||
| `AttachedText` else `None` | ||
| """ | ||
| return self._text | ||
|
|
||
| @text.setter | ||
| def text(self, text: Optional[AttachedText]) -> None: | ||
| self._text = text | ||
|
|
||
| @property | ||
| @serializable.xml_sequence(2) | ||
| def url(self) -> Optional[XsUri]: | ||
| """ | ||
| The URL to the attachment file. If the attachment is a license or BOM, an externalReference should also be | ||
| specified for completeness. | ||
|
|
||
| Returns: | ||
| `XsUri` or `None` | ||
| """ | ||
| return self._url | ||
|
|
||
| @url.setter | ||
| def url(self, url: Optional[XsUri]) -> None: | ||
| self._url = url | ||
|
|
||
| def __comparable_tuple(self) -> _ComparableTuple: | ||
| return _ComparableTuple(( | ||
| self.bom_ref.value, self.license_identifier, self.url, self.text, | ||
| )) | ||
|
|
||
| def __eq__(self, other: object) -> bool: | ||
| if isinstance(other, ExpressionDetails): | ||
| return self.__comparable_tuple() == other.__comparable_tuple() | ||
| return False | ||
|
|
||
| def __lt__(self, other: object) -> bool: | ||
| if isinstance(other, ExpressionDetails): | ||
| return self.__comparable_tuple() < other.__comparable_tuple() | ||
| return NotImplemented | ||
|
|
||
| def __hash__(self) -> int: | ||
| return hash(self.__comparable_tuple()) | ||
|
|
||
| def __repr__(self) -> str: | ||
| return f'<ExpressionDetails bom-ref={self.bom_ref!r}, license_identifier={self.license_identifier}>' | ||
|
|
||
|
|
||
| @serializable.serializable_class( | ||
| name='expression', | ||
| ignore_unknown_during_deserialization=True | ||
|
|
@@ -280,10 +398,14 @@ def __init__( | |
| self, value: str, *, | ||
| bom_ref: Optional[Union[str, BomRef]] = None, | ||
| acknowledgement: Optional[LicenseAcknowledgement] = None, | ||
| expression_details: Optional[Iterable[ExpressionDetails]] = None, | ||
| properties: Optional[Iterable[Property]] = None, | ||
| ) -> None: | ||
| self._bom_ref = _bom_ref_from_str(bom_ref) | ||
| self._value = value | ||
| self._acknowledgement = acknowledgement | ||
| self.expression_details = expression_details or [] | ||
| self.properties = properties or [] | ||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| @property | ||
| @serializable.view(SchemaVersion1Dot5) | ||
|
|
@@ -346,11 +468,60 @@ def acknowledgement(self) -> Optional[LicenseAcknowledgement]: | |
| def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None: | ||
| self._acknowledgement = acknowledgement | ||
|
|
||
| @property | ||
| @serializable.view(SchemaVersion1Dot7) | ||
| @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, child_name='details') | ||
| @serializable.xml_sequence(1) | ||
| def expression_details(self) -> 'SortedSet[ExpressionDetails]': | ||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
| Details for parts of the expression. | ||
|
|
||
| Returns: | ||
| `Iterable[ExpressionDetails]` if set else `None` | ||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
| return self._expression_details | ||
|
|
||
| @expression_details.setter | ||
| def expression_details(self, expression_details: Iterable[ExpressionDetails]) -> None: | ||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| self._expression_details = SortedSet(expression_details) | ||
|
|
||
| # @property | ||
| # @serializable.view(SchemaVersion1Dot7) | ||
| # ... | ||
| # @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, child_name='licensing') | ||
| # @serializable.xml_sequence(2) | ||
| # def licensing(self) -> ...: | ||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # ... # TODO | ||
| # | ||
|
|
||
| @property | ||
| @serializable.view(SchemaVersion1Dot7) | ||
| @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property') | ||
| @serializable.xml_sequence(3) | ||
| def properties(self) -> 'SortedSet[Property]': | ||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
| Provides the ability to document properties in a key/value store. This provides flexibility to include data not | ||
| officially supported in the standard without having to use additional namespaces or create extensions. | ||
|
|
||
| Property names of interest to the general public are encouraged to be registered in the CycloneDX Property | ||
| Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. Formal registration is OPTIONAL. | ||
|
|
||
| Return: | ||
| Set of `Property` | ||
| """ | ||
| return self._properties | ||
|
|
||
| @properties.setter | ||
| def properties(self, properties: Iterable[Property]) -> None: | ||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| self._properties = SortedSet(properties) | ||
|
|
||
| def __comparable_tuple(self) -> _ComparableTuple: | ||
| return _ComparableTuple(( | ||
| self._acknowledgement, | ||
| self._value, | ||
| self._bom_ref.value, | ||
| _ComparableTuple(self.expression_details), | ||
| _ComparableTuple(self.properties), | ||
| )) | ||
|
|
||
| def __hash__(self) -> int: | ||
|
|
@@ -415,12 +586,51 @@ class LicenseRepository(SortedSet): | |
| class _LicenseRepositorySerializationHelper(serializable.helpers.BaseHelper): | ||
| """ THIS CLASS IS NON-PUBLIC API """ | ||
|
|
||
| @staticmethod | ||
| def __supports_expression_details(view: Any) -> bool: | ||
| try: | ||
| return view is not None and view().schema_version_enum >= SchemaVersion.V1_7 | ||
| except Exception: # pragma: no cover | ||
| return False | ||
|
|
||
| @staticmethod | ||
| def __serialize_license_expression_details_xml( | ||
| license_expression: LicenseExpression, | ||
| view: Optional[type[serializable.ViewType]], | ||
| xmlns: Optional[str] | ||
| ) -> Element: | ||
| elem: Element = license_expression.as_xml( # type:ignore[attr-defined] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note to self: this looks odd - a strange back and forth of already built XML foo. |
||
| view_=view, as_string=False, element_name='expression-detailed', xmlns=xmlns) | ||
|
|
||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| expression_value = elem.text | ||
| if expression_value: | ||
| elem.set(f'{{{xmlns}}}expression' if xmlns else 'expression', expression_value) | ||
| elem.text = None | ||
|
|
||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return elem | ||
|
|
||
| @staticmethod | ||
| def __deserialize_license_expression_details_xml( | ||
| li: Element, | ||
| default_ns: Optional[str] | ||
| ) -> LicenseExpression: | ||
| expression_value = li.get('expression') | ||
| if not expression_value: | ||
| raise CycloneDxDeserializationException(f'unexpected content: {li!r}') | ||
|
|
||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| license_expression = LicenseExpression.from_xml( # type:ignore[attr-defined] | ||
| li, default_ns) | ||
| license_expression.value = expression_value | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note to self: this looks odd. the detection is not quite intuitive from a first look. need to look into the details again. |
||
|
|
||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return license_expression | ||
|
|
||
| @classmethod | ||
| def json_normalize(cls, o: LicenseRepository, *, | ||
| view: Optional[type[serializable.ViewType]], | ||
| **__: Any) -> Any: | ||
| if len(o) == 0: | ||
| return None | ||
|
|
||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| expression = next((li for li in o if isinstance(li, LicenseExpression)), None) | ||
| if expression: | ||
| # mixed license expression and license? this is an invalid constellation according to schema! | ||
|
|
@@ -461,13 +671,20 @@ def xml_normalize(cls, o: LicenseRepository, *, | |
| if len(o) == 0: | ||
| return None | ||
| elem = Element(element_name) | ||
|
|
||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| expression = next((li for li in o if isinstance(li, LicenseExpression)), None) | ||
| if expression: | ||
| # mixed license expression and license? this is an invalid constellation according to schema! | ||
| # see https://github.com/CycloneDX/specification/pull/205 | ||
| # but models need to allow it for backwards compatibility with JSON CDX < 1.5 | ||
| elem.append(expression.as_xml( # type:ignore[attr-defined] | ||
| view_=view, as_string=False, element_name='expression', xmlns=xmlns)) | ||
|
|
||
Churro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if expression.expression_details and cls.__supports_expression_details(view): | ||
|
||
| elem.append(cls.__serialize_license_expression_details_xml(expression, view, xmlns)) | ||
| else: | ||
| if expression.expression_details: | ||
| warn('LicenseExpression details are not supported in schema versions < 1.7; skipping serialization') | ||
| elem.append(expression.as_xml( # type:ignore[attr-defined] | ||
| view_=view, as_string=False, element_name='expression', xmlns=xmlns)) | ||
| else: | ||
| elem.extend( | ||
| li.as_xml( # type:ignore[attr-defined] | ||
|
|
@@ -490,6 +707,8 @@ def xml_denormalize(cls, o: Element, | |
| elif tag == 'expression': | ||
| repo.add(LicenseExpression.from_xml( # type:ignore[attr-defined] | ||
| li, default_ns)) | ||
| elif tag == 'expression-detailed': | ||
| repo.add(cls.__deserialize_license_expression_details_xml(li, default_ns)) | ||
| else: | ||
| raise CycloneDxDeserializationException(f'unexpected: {li!r}') | ||
| return repo | ||
Uh oh!
There was an error while loading. Please reload this page.