Skip to content

Commit 6dda406

Browse files
committed
integrate LicenseExpressionDetailed into LicenseExpression
Signed-off-by: Johannes Feichtner <johannes@web-wack.at>
1 parent ebe9f2e commit 6dda406

5 files changed

Lines changed: 87 additions & 216 deletions

File tree

cyclonedx/model/license.py

Lines changed: 59 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,6 @@ def __eq__(self, other: object) -> bool:
253253
def __lt__(self, other: Any) -> bool:
254254
if isinstance(other, DisjunctiveLicense):
255255
return self.__comparable_tuple() < other.__comparable_tuple()
256-
if isinstance(other, LicenseExpressionDetailed):
257-
return False # self after any LicenseExpressionDetailed
258256
if isinstance(other, LicenseExpression):
259257
return False # self after any LicenseExpression
260258
return NotImplemented
@@ -266,118 +264,6 @@ def __repr__(self) -> str:
266264
return f'<License id={self._id!r}, name={self._name!r}>'
267265

268266

269-
@serializable.serializable_class(
270-
name='expression',
271-
ignore_unknown_during_deserialization=True
272-
)
273-
class LicenseExpression:
274-
"""
275-
This is our internal representation of `licenseType`'s expression type that can be used in multiple places within
276-
a CycloneDX BOM document.
277-
278-
.. note::
279-
See the CycloneDX Schema definition:
280-
https://cyclonedx.org/docs/1.7/json/#components_items_licenses_items_expression
281-
"""
282-
283-
def __init__(
284-
self, value: str, *,
285-
bom_ref: Optional[Union[str, BomRef]] = None,
286-
acknowledgement: Optional[LicenseAcknowledgement] = None,
287-
) -> None:
288-
self._bom_ref = _bom_ref_from_str(bom_ref)
289-
self._value = value
290-
self._acknowledgement = acknowledgement
291-
292-
@property
293-
@serializable.view(SchemaVersion1Dot5)
294-
@serializable.view(SchemaVersion1Dot6)
295-
@serializable.view(SchemaVersion1Dot7)
296-
@serializable.type_mapping(BomRef)
297-
@serializable.xml_attribute()
298-
@serializable.xml_name('bom-ref')
299-
@serializable.json_name('bom-ref')
300-
def bom_ref(self) -> BomRef:
301-
"""
302-
An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be
303-
unique within the BOM.
304-
305-
Returns:
306-
`BomRef`
307-
"""
308-
return self._bom_ref
309-
310-
@property
311-
@serializable.xml_name('.')
312-
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
313-
@serializable.json_name('expression')
314-
def value(self) -> str:
315-
"""
316-
Value of this LicenseExpression.
317-
318-
Returns:
319-
`str`
320-
"""
321-
return self._value
322-
323-
@value.setter
324-
def value(self, value: str) -> None:
325-
self._value = value
326-
327-
@property
328-
@serializable.view(SchemaVersion1Dot6)
329-
@serializable.view(SchemaVersion1Dot7)
330-
@serializable.xml_attribute()
331-
def acknowledgement(self) -> Optional[LicenseAcknowledgement]:
332-
"""
333-
Declared licenses and concluded licenses represent two different stages in the licensing process within
334-
software development.
335-
336-
Declared licenses refer to the initial intention of the software authors regarding the
337-
licensing terms under which their code is released. On the other hand, concluded licenses are the result of a
338-
comprehensive analysis of the project's codebase to identify and confirm the actual licenses of the components
339-
used, which may differ from the initially declared licenses. While declared licenses provide an upfront
340-
indication of the licensing intentions, concluded licenses offer a more thorough understanding of the actual
341-
licensing within a project, facilitating proper compliance and risk management. Observed licenses are defined
342-
in evidence.licenses. Observed licenses form the evidence necessary to substantiate a concluded license.
343-
344-
Returns:
345-
`LicenseAcknowledgement` or `None`
346-
"""
347-
return self._acknowledgement
348-
349-
@acknowledgement.setter
350-
def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None:
351-
self._acknowledgement = acknowledgement
352-
353-
def __comparable_tuple(self) -> _ComparableTuple:
354-
return _ComparableTuple((
355-
self._acknowledgement,
356-
self._value,
357-
self._bom_ref.value,
358-
))
359-
360-
def __hash__(self) -> int:
361-
return hash(self.__comparable_tuple())
362-
363-
def __eq__(self, other: object) -> bool:
364-
if isinstance(other, LicenseExpression):
365-
return self.__comparable_tuple() == other.__comparable_tuple()
366-
return False
367-
368-
def __lt__(self, other: Any) -> bool:
369-
if isinstance(other, LicenseExpression):
370-
return self.__comparable_tuple() < other.__comparable_tuple()
371-
if isinstance(other, LicenseExpressionDetailed):
372-
return False # self after any LicenseExpressionDetailed
373-
if isinstance(other, DisjunctiveLicense):
374-
return True # self before any DisjunctiveLicense
375-
return NotImplemented
376-
377-
def __repr__(self) -> str:
378-
return f'<LicenseExpression value={self._value!r}>'
379-
380-
381267
@serializable.serializable_class(ignore_unknown_during_deserialization=True)
382268
class ExpressionDetails:
383269
"""
@@ -494,27 +380,30 @@ def __repr__(self) -> str:
494380
return f'<ExpressionDetails bom-ref={self.bom_ref!r}, license_identifier={self.license_identifier}>'
495381

496382

497-
@serializable.serializable_class(ignore_unknown_during_deserialization=True)
498-
class LicenseExpressionDetailed:
383+
@serializable.serializable_class(
384+
name='expression',
385+
ignore_unknown_during_deserialization=True
386+
)
387+
class LicenseExpression:
499388
"""
500-
Specifies the details and attributes related to a software license.
501-
It must be a valid SPDX license expression, along with additional properties such as license acknowledgment.
389+
This is our internal representation of `licenseType`'s expression type that can be used in multiple places within
390+
a CycloneDX BOM document.
502391
503392
.. note::
504393
See the CycloneDX Schema definition:
505-
https://cyclonedx.org/docs/1.7/json/#components_items_licenses_items_oneOf_i1_expressionDetails
394+
https://cyclonedx.org/docs/1.7/json/#components_items_licenses_items_expression
506395
"""
507396

508397
def __init__(
509-
self, expression: str, *,
510-
expression_details: Optional[Iterable[ExpressionDetails]] = None,
398+
self, value: str, *,
511399
bom_ref: Optional[Union[str, BomRef]] = None,
512400
acknowledgement: Optional[LicenseAcknowledgement] = None,
401+
expression_details: Optional[Iterable[ExpressionDetails]] = None,
513402
properties: Optional[Iterable[Property]] = None,
514403
) -> None:
515404
self._bom_ref = _bom_ref_from_str(bom_ref)
516-
self.expression = expression
517-
self.acknowledgement = acknowledgement
405+
self._value = value
406+
self._acknowledgement = acknowledgement
518407
self.expression_details = expression_details or []
519408
self.properties = properties or []
520409

@@ -537,21 +426,21 @@ def bom_ref(self) -> BomRef:
537426
return self._bom_ref
538427

539428
@property
429+
@serializable.xml_name('.')
540430
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
541-
@serializable.xml_attribute()
542-
def expression(self) -> str:
431+
@serializable.json_name('expression')
432+
def value(self) -> str:
543433
"""
544-
A valid SPDX license expression.
545-
Refer to https://spdx.org/specifications for syntax requirements.
434+
Value of this LicenseExpression.
546435
547436
Returns:
548437
`str`
549438
"""
550-
return self._expression
439+
return self._value
551440

552-
@expression.setter
553-
def expression(self, expression: str) -> None:
554-
self._expression = expression
441+
@value.setter
442+
def value(self, value: str) -> None:
443+
self._value = value
555444

556445
@property
557446
@serializable.view(SchemaVersion1Dot6)
@@ -629,7 +518,7 @@ def properties(self, properties: Iterable[Property]) -> None:
629518
def __comparable_tuple(self) -> _ComparableTuple:
630519
return _ComparableTuple((
631520
self._acknowledgement,
632-
self._expression,
521+
self._value,
633522
self._bom_ref.value,
634523
_ComparableTuple(self.expression_details),
635524
_ComparableTuple(self.properties),
@@ -639,28 +528,25 @@ def __hash__(self) -> int:
639528
return hash(self.__comparable_tuple())
640529

641530
def __eq__(self, other: object) -> bool:
642-
if isinstance(other, LicenseExpressionDetailed):
531+
if isinstance(other, LicenseExpression):
643532
return self.__comparable_tuple() == other.__comparable_tuple()
644533
return False
645534

646535
def __lt__(self, other: Any) -> bool:
647-
if isinstance(other, LicenseExpressionDetailed):
648-
return self.__comparable_tuple() < other.__comparable_tuple()
649536
if isinstance(other, LicenseExpression):
650-
return True # self before any LicenseExpression
537+
return self.__comparable_tuple() < other.__comparable_tuple()
651538
if isinstance(other, DisjunctiveLicense):
652-
return False # self after any DisjunctiveLicense
539+
return True # self before any DisjunctiveLicense
653540
return NotImplemented
654541

655542
def __repr__(self) -> str:
656-
return f'<LicenseExpressionDetailed expression={self._expression!r}>'
543+
return f'<LicenseExpression value={self._value!r}>'
657544

658545

659-
License = Union[LicenseExpression, LicenseExpressionDetailed, DisjunctiveLicense]
546+
License = Union[LicenseExpression, DisjunctiveLicense]
660547
"""TypeAlias for a union of supported license models.
661548
662549
- :class:`LicenseExpression`
663-
- :class:`LicenseExpressionDetailed`
664550
- :class:`DisjunctiveLicense`
665551
"""
666552

@@ -708,25 +594,35 @@ def __supports_expression_details(view: Any) -> bool:
708594
return False
709595

710596
@staticmethod
711-
def __transpile_license_expression_details_xml(
712-
expression_detailed: LicenseExpressionDetailed,
597+
def __serialize_license_expression_details_xml(
598+
license_expression: LicenseExpression,
713599
view: Optional[type[serializable.ViewType]],
714600
xmlns: Optional[str]
715601
) -> Element:
716-
normalized: Element = expression_detailed.as_xml( # type:ignore[attr-defined]
717-
view_=view, as_string=False, element_name='expression', xmlns=xmlns)
602+
elem: Element = license_expression.as_xml( # type:ignore[attr-defined]
603+
view_=view, as_string=False, element_name='expression-detailed', xmlns=xmlns)
718604

719-
ns_prefix = f'{{{xmlns}}}' if xmlns else ''
720-
details = normalized.findall(f'./{ns_prefix}details')
721-
for details_elem in details:
722-
normalized.remove(details_elem)
723-
724-
expression_value = normalized.get(f'{ns_prefix}expression')
605+
expression_value = elem.text
725606
if expression_value:
726-
normalized.text = expression_value
727-
del normalized.attrib[f'{ns_prefix}expression']
607+
elem.set(f'{{{xmlns}}}expression' if xmlns else 'expression', expression_value)
608+
elem.text = None
609+
610+
return elem
611+
612+
@staticmethod
613+
def __deserialize_license_expression_details_xml(
614+
li: Element,
615+
default_ns: Optional[str]
616+
) -> LicenseExpression:
617+
expression_value = li.get('expression')
618+
if not expression_value:
619+
raise CycloneDxDeserializationException(f'unexpected content: {li!r}')
728620

729-
return normalized
621+
license_expression = LicenseExpression.from_xml( # type:ignore[attr-defined]
622+
li, default_ns)
623+
license_expression.value = expression_value
624+
625+
return license_expression
730626

731627
@classmethod
732628
def json_normalize(cls, o: LicenseRepository, *,
@@ -735,12 +631,6 @@ def json_normalize(cls, o: LicenseRepository, *,
735631
if len(o) == 0:
736632
return None
737633

738-
expression_detailed = next((li for li in o if isinstance(li, LicenseExpressionDetailed)), None)
739-
if expression_detailed:
740-
if not cls.__supports_expression_details(view):
741-
warn('LicenseExpressionDetailed is not supported in schema versions < 1.7; ignoring expressionDetails')
742-
return [json_loads(expression_detailed.as_json(view_=view))] # type:ignore[attr-defined]
743-
744634
expression = next((li for li in o if isinstance(li, LicenseExpression)), None)
745635
if expression:
746636
# mixed license expression and license? this is an invalid constellation according to schema!
@@ -764,10 +654,6 @@ def json_denormalize(cls, o: list[dict[str, Any]],
764654
if 'license' in li:
765655
repo.add(DisjunctiveLicense.from_json( # type:ignore[attr-defined]
766656
li['license']))
767-
elif 'expressionDetails' in li:
768-
repo.add(LicenseExpressionDetailed.from_json( # type:ignore[attr-defined]
769-
li
770-
))
771657
elif 'expression' in li:
772658
repo.add(LicenseExpression.from_json( # type:ignore[attr-defined]
773659
li
@@ -786,22 +672,19 @@ def xml_normalize(cls, o: LicenseRepository, *,
786672
return None
787673
elem = Element(element_name)
788674

789-
expression_detailed = next((li for li in o if isinstance(li, LicenseExpressionDetailed)), None)
790-
if expression_detailed:
791-
if cls.__supports_expression_details(view):
792-
elem.append(expression_detailed.as_xml( # type:ignore[attr-defined]
793-
view_=view, as_string=False, element_name='expression-detailed', xmlns=xmlns))
794-
else:
795-
warn('LicenseExpressionDetailed is not supported in schema versions < 1.7; ignoring details')
796-
elem.append(cls.__transpile_license_expression_details_xml(expression_detailed, view, xmlns))
797-
798675
expression = next((li for li in o if isinstance(li, LicenseExpression)), None)
799676
if expression:
800677
# mixed license expression and license? this is an invalid constellation according to schema!
801678
# see https://github.com/CycloneDX/specification/pull/205
802679
# but models need to allow it for backwards compatibility with JSON CDX < 1.5
803-
elem.append(expression.as_xml( # type:ignore[attr-defined]
804-
view_=view, as_string=False, element_name='expression', xmlns=xmlns))
680+
681+
if expression.expression_details and cls.__supports_expression_details(view):
682+
elem.append(cls.__serialize_license_expression_details_xml(expression, view, xmlns))
683+
else:
684+
if expression.expression_details:
685+
warn('LicenseExpression details are not supported in schema versions < 1.7; skipping serialization')
686+
elem.append(expression.as_xml( # type:ignore[attr-defined]
687+
view_=view, as_string=False, element_name='expression', xmlns=xmlns))
805688
else:
806689
elem.extend(
807690
li.as_xml( # type:ignore[attr-defined]
@@ -821,12 +704,11 @@ def xml_denormalize(cls, o: Element,
821704
if tag == 'license':
822705
repo.add(DisjunctiveLicense.from_xml( # type:ignore[attr-defined]
823706
li, default_ns))
824-
elif tag == 'expression-detailed':
825-
repo.add(LicenseExpressionDetailed.from_xml( # type:ignore[attr-defined]
826-
li, default_ns))
827707
elif tag == 'expression':
828708
repo.add(LicenseExpression.from_xml( # type:ignore[attr-defined]
829709
li, default_ns))
710+
elif tag == 'expression-detailed':
711+
repo.add(cls.__deserialize_license_expression_details_xml(li, default_ns))
830712
else:
831713
raise CycloneDxDeserializationException(f'unexpected: {li!r}')
832714
return repo

tests/_data/models.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@
103103
License,
104104
LicenseAcknowledgement,
105105
LicenseExpression,
106-
LicenseExpressionDetailed,
107106
)
108107
from cyclonedx.model.lifecycle import LifecyclePhase, NamedLifecycle, PredefinedLifecycle
109108
from cyclonedx.model.release_note import ReleaseNotes
@@ -1099,10 +1098,10 @@ def get_bom_with_licenses() -> Bom:
10991098
text=AttachedText(content='this is additional license text')),
11001099
]),
11011100
Component(name='c-with-expression-details', type=ComponentType.LIBRARY, bom_ref='C4',
1102-
licenses=[LicenseExpressionDetailed(expression='GPL-3.0-or-later OR GPL-2.0',
1103-
expression_details=expression_details,
1104-
acknowledgement=LicenseAcknowledgement.DECLARED
1105-
)]),
1101+
licenses=[LicenseExpression(value='GPL-3.0-or-later OR GPL-2.0',
1102+
expression_details=expression_details,
1103+
acknowledgement=LicenseAcknowledgement.DECLARED
1104+
)]),
11061105
],
11071106
services=[
11081107
Service(name='s-with-expression', bom_ref='S1',

0 commit comments

Comments
 (0)