-
-
Notifications
You must be signed in to change notification settings - Fork 63
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 all commits
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 |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ | |
| 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, Property, XsUri | ||
| from .bom_ref import BomRef | ||
|
|
@@ -278,6 +279,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.
|
||
| """ | ||
| 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 | ||
|
|
@@ -296,10 +413,12 @@ def __init__( | |
| self, value: str, *, | ||
| bom_ref: Optional[Union[str, BomRef]] = None, | ||
| acknowledgement: Optional[LicenseAcknowledgement] = None, | ||
| details: Optional[Iterable[ExpressionDetails]] = None, | ||
| ) -> None: | ||
| self._bom_ref = _bom_ref_from_str(bom_ref) | ||
| self._value = value | ||
| self._acknowledgement = acknowledgement | ||
| self.details = details or [] | ||
|
|
||
| @property | ||
| @serializable.view(SchemaVersion1Dot5) | ||
|
|
@@ -362,11 +481,30 @@ def acknowledgement(self) -> Optional[LicenseAcknowledgement]: | |
| def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None: | ||
| self._acknowledgement = acknowledgement | ||
|
|
||
| @property | ||
| @serializable.json_name('expressionDetails') | ||
| @serializable.view(SchemaVersion1Dot7) | ||
| @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, child_name='details') | ||
| @serializable.xml_sequence(1) | ||
| def details(self) -> 'SortedSet[ExpressionDetails]': | ||
| """ | ||
| Details for parts of the expression. | ||
|
|
||
| Returns: | ||
| Set of `ExpressionDetails` | ||
| """ | ||
| return self._details | ||
|
|
||
| @details.setter | ||
| def details(self, details: Iterable[ExpressionDetails]) -> None: | ||
| self._details = SortedSet(details) | ||
|
|
||
| def __comparable_tuple(self) -> _ComparableTuple: | ||
| return _ComparableTuple(( | ||
| self._acknowledgement, | ||
| self._value, | ||
| self._bom_ref.value, | ||
| _ComparableTuple(self.details), | ||
| )) | ||
|
|
||
| def __hash__(self) -> int: | ||
|
|
@@ -431,6 +569,40 @@ 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) | ||
| expression_value = elem.text | ||
| if expression_value: | ||
| elem.set(f'{{{xmlns}}}expression' if xmlns else 'expression', expression_value) | ||
| elem.text = None | ||
| 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}') | ||
| license_expression: LicenseExpression = 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. |
||
| return license_expression | ||
|
|
||
| @classmethod | ||
| def json_normalize(cls, o: LicenseRepository, *, | ||
| view: Optional[type[serializable.ViewType]], | ||
|
|
@@ -482,8 +654,13 @@ def xml_normalize(cls, o: LicenseRepository, *, | |
| # 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)) | ||
| if expression.details and cls.__supports_expression_details(view): | ||
| elem.append(cls.__serialize_license_expression_details_xml(expression, view, xmlns)) | ||
| else: | ||
| if 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] | ||
|
|
@@ -506,6 +683,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.