Skip to content

Commit 6770786

Browse files
authored
fix: multiple licenses issue #365 (#466)
breaking changes ------------------ * Reworked license related models and collections * API * Removed class `factory.license.LicenseChoiceFactory` The old functionality was integrated into `factory.license.LicenseFactory`. * Method `factory.license.LicenseFactory.make_from_string()`'s parameter `name_or_spdx` was renamed to `value` * Method `factory.license.LicenseFactory.make_from_string()`'s return value can also be a `LicenseExpression` The behavior imitates the old `factory.license.LicenseChoiceFactory.make_from_string()` * Renamed class `module.License` to `module.license.DisjunctliveLicense` * Removed class `module.LicenseChoice` Use dedicated classes `module.license.DisjunctliveLicense` and `module.license.LicenseExpression` instead * All occurrences of `models.LicenseChoice` were replaced by `models.licenses.License` * All occurrences of `SortedSet[LicenseChoice]` were specialized to `models.license.LicenseRepository` fixes ------------------ * serialization of multy-licenses #365 added ------------------ * API * Method `factory.license.LicenseFactory.make_with_expression()` * Class `model.license.DisjunctiveLicense` * Class `model.license.LicenseExpression` * Class `model.license.LicenseRepository` * Class `serialization.LicenseRepositoryHelper` tests ------------------ * added regression test for bug #365 misc ------------------ * raised dependency `py-serializable@^9.15` ---- fixes #365 ~~BLOCKED by a feature request to serializer: <https://github.com/madpah/serializable/pull/32>~~ --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 1e963bd commit 6770786

File tree

121 files changed

+2504
-594
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+2504
-594
lines changed

cyclonedx/factory/license.py

Lines changed: 37 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,76 +13,59 @@
1313
# SPDX-License-Identifier: Apache-2.0
1414
# Copyright (c) OWASP Foundation. All Rights Reserved.
1515

16-
from typing import Optional
16+
from typing import TYPE_CHECKING, Optional
1717

1818
from ..exception.factory import InvalidLicenseExpressionException, InvalidSpdxLicenseException
19-
from ..model import AttachedText, License, LicenseChoice, XsUri
19+
from ..model.license import DisjunctiveLicense, LicenseExpression
2020
from ..spdx import fixup_id as spdx_fixup, is_compound_expression as is_spdx_compound_expression
2121

22+
if TYPE_CHECKING: # pragma: no cover
23+
from ..model import AttachedText, XsUri
24+
from ..model.license import License
2225

23-
class LicenseFactory:
24-
"""Factory for :class:`cyclonedx.model.License`."""
25-
26-
def make_from_string(self, name_or_spdx: str, *,
27-
license_text: Optional[AttachedText] = None,
28-
license_url: Optional[XsUri] = None) -> License:
29-
"""Make a :class:`cyclonedx.model.License` from a string."""
30-
try:
31-
return self.make_with_id(name_or_spdx, text=license_text, url=license_url)
32-
except InvalidSpdxLicenseException:
33-
return self.make_with_name(name_or_spdx, text=license_text, url=license_url)
34-
35-
def make_with_id(self, spdx_id: str, *, text: Optional[AttachedText] = None,
36-
url: Optional[XsUri] = None) -> License:
37-
"""Make a :class:`cyclonedx.model.License` from an SPDX-ID.
38-
39-
:raises InvalidSpdxLicenseException: if `spdx_id` was not known/supported SPDX-ID
40-
"""
41-
spdx_license_id = spdx_fixup(spdx_id)
42-
if spdx_license_id is None:
43-
raise InvalidSpdxLicenseException(spdx_id)
44-
return License(id=spdx_license_id, text=text, url=url)
45-
46-
def make_with_name(self, name: str, *, text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> License:
47-
"""Make a :class:`cyclonedx.model.License` with a name."""
48-
return License(name=name, text=text, url=url)
4926

27+
class LicenseFactory:
28+
"""Factory for :class:`cyclonedx.model.license.License`."""
5029

51-
class LicenseChoiceFactory:
52-
"""Factory for :class:`cyclonedx.model.LicenseChoice`."""
53-
54-
def __init__(self, *, license_factory: LicenseFactory) -> None:
55-
self.license_factory = license_factory
56-
57-
def make_from_string(self, expression_or_name_or_spdx: str) -> LicenseChoice:
58-
"""Make a :class:`cyclonedx.model.LicenseChoice` from a string.
59-
60-
Priority: SPDX license ID, SPDX license expression, named license
61-
"""
30+
def make_from_string(self, value: str, *,
31+
license_text: Optional['AttachedText'] = None,
32+
license_url: Optional['XsUri'] = None) -> 'License':
33+
"""Make a :class:`cyclonedx.model.license.License` from a string."""
6234
try:
63-
return LicenseChoice(license=self.license_factory.make_with_id(expression_or_name_or_spdx))
35+
return self.make_with_id(value, text=license_text, url=license_url)
6436
except InvalidSpdxLicenseException:
6537
pass
6638
try:
67-
return self.make_with_compound_expression(expression_or_name_or_spdx)
39+
return self.make_with_expression(value)
6840
except InvalidLicenseExpressionException:
6941
pass
70-
return LicenseChoice(license=self.license_factory.make_with_name(expression_or_name_or_spdx))
42+
return self.make_with_name(value, text=license_text, url=license_url)
7143

72-
def make_with_compound_expression(self, compound_expression: str) -> LicenseChoice:
73-
"""Make a :class:`cyclonedx.model.LicenseChoice` with a compound expression.
44+
def make_with_expression(self, expression: str) -> LicenseExpression:
45+
"""Make a :class:`cyclonedx.model.license.LicenseExpression` with a compound expression.
7446
7547
Utilizes :func:`cyclonedx.spdx.is_compound_expression`.
7648
77-
:raises InvalidLicenseExpressionException: if `expression` is not known/supported license expression
49+
:raises InvalidLicenseExpressionException: if param `value` is not known/supported license expression
7850
"""
79-
if is_spdx_compound_expression(compound_expression):
80-
return LicenseChoice(expression=compound_expression)
81-
raise InvalidLicenseExpressionException(compound_expression)
51+
if is_spdx_compound_expression(expression):
52+
return LicenseExpression(expression)
53+
raise InvalidLicenseExpressionException(expression)
54+
55+
def make_with_id(self, spdx_id: str, *,
56+
text: Optional['AttachedText'] = None,
57+
url: Optional['XsUri'] = None) -> DisjunctiveLicense:
58+
"""Make a :class:`cyclonedx.model.license.DisjunctiveLicense` from an SPDX-ID.
59+
60+
:raises InvalidSpdxLicenseException: if param `spdx_id` was not known/supported SPDX-ID
61+
"""
62+
spdx_license_id = spdx_fixup(spdx_id)
63+
if spdx_license_id is None:
64+
raise InvalidSpdxLicenseException(spdx_id)
65+
return DisjunctiveLicense(id=spdx_license_id, text=text, url=url)
8266

83-
def make_with_license(self, name_or_spdx: str, *,
84-
license_text: Optional[AttachedText] = None,
85-
license_url: Optional[XsUri] = None) -> LicenseChoice:
86-
"""Make a :class:`cyclonedx.model.LicenseChoice` with a license (name or SPDX-ID)."""
87-
return LicenseChoice(license=self.license_factory.make_from_string(
88-
name_or_spdx, license_text=license_text, license_url=license_url))
67+
def make_with_name(self, name: str, *,
68+
text: Optional['AttachedText'] = None,
69+
url: Optional['XsUri'] = None) -> DisjunctiveLicense:
70+
"""Make a :class:`cyclonedx.model.license.DisjunctiveLicense` with a name."""
71+
return DisjunctiveLicense(name=name, text=text, url=url)

cyclonedx/model/__init__.py

Lines changed: 3 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import hashlib
1717
import re
18-
import warnings
1918
from datetime import datetime, timezone
2019
from enum import Enum
2120
from itertools import zip_longest
@@ -28,7 +27,6 @@
2827
from ..exception.model import (
2928
InvalidLocaleTypeException,
3029
InvalidUriException,
31-
MutuallyExclusivePropertiesException,
3230
NoPropertiesProvidedException,
3331
UnknownHashTypeException,
3432
)
@@ -444,20 +442,19 @@ def uri(self) -> str:
444442
return self._uri
445443

446444
@classmethod
447-
def serialize(cls, o: object) -> str:
445+
def serialize(cls, o: Any) -> str:
448446
if isinstance(o, XsUri):
449447
return str(o)
450-
451448
raise ValueError(f'Attempt to serialize a non-XsUri: {o.__class__}')
452449

453450
@classmethod
454-
def deserialize(cls, o: object) -> 'XsUri':
451+
def deserialize(cls, o: Any) -> 'XsUri':
455452
try:
456453
return XsUri(uri=str(o))
457454
except ValueError:
458455
raise ValueError(f'XsUri string supplied ({o}) does not parse!')
459456

460-
def __eq__(self, other: object) -> bool:
457+
def __eq__(self, other: Any) -> bool:
461458
if isinstance(other, XsUri):
462459
return hash(other) == hash(self)
463460
return False
@@ -579,181 +576,6 @@ def __repr__(self) -> str:
579576
return f'<ExternalReference {self.type.name}, {self.url}>'
580577

581578

582-
@serializable.serializable_class
583-
class License:
584-
"""
585-
This is our internal representation of `licenseType` complex type that can be used in multiple places within
586-
a CycloneDX BOM document.
587-
588-
.. note::
589-
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseType
590-
"""
591-
592-
def __init__(self, *, id: Optional[str] = None, name: Optional[str] = None,
593-
text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> None:
594-
if not id and not name:
595-
raise MutuallyExclusivePropertiesException('Either `id` or `name` MUST be supplied')
596-
if id and name:
597-
warnings.warn(
598-
'Both `id` and `name` have been supplied - `name` will be ignored!',
599-
RuntimeWarning
600-
)
601-
self.id = id
602-
if not id:
603-
self.name = name
604-
else:
605-
self.name = None
606-
self.text = text
607-
self.url = url
608-
609-
@property
610-
def id(self) -> Optional[str]:
611-
"""
612-
A valid SPDX license ID
613-
614-
Returns:
615-
`str` or `None`
616-
"""
617-
return self._id
618-
619-
@id.setter
620-
def id(self, id: Optional[str]) -> None:
621-
self._id = id
622-
623-
@property
624-
def name(self) -> Optional[str]:
625-
"""
626-
If SPDX does not define the license used, this field may be used to provide the license name.
627-
628-
Returns:
629-
`str` or `None`
630-
"""
631-
return self._name
632-
633-
@name.setter
634-
def name(self, name: Optional[str]) -> None:
635-
self._name = name
636-
637-
@property
638-
def text(self) -> Optional[AttachedText]:
639-
"""
640-
Specifies the optional full text of the attachment
641-
642-
Returns:
643-
`AttachedText` else `None`
644-
"""
645-
return self._text
646-
647-
@text.setter
648-
def text(self, text: Optional[AttachedText]) -> None:
649-
self._text = text
650-
651-
@property
652-
def url(self) -> Optional[XsUri]:
653-
"""
654-
The URL to the attachment file. If the attachment is a license or BOM, an externalReference should also be
655-
specified for completeness.
656-
657-
Returns:
658-
`XsUri` or `None`
659-
"""
660-
return self._url
661-
662-
@url.setter
663-
def url(self, url: Optional[XsUri]) -> None:
664-
self._url = url
665-
666-
def __eq__(self, other: object) -> bool:
667-
if isinstance(other, License):
668-
return hash(other) == hash(self)
669-
return False
670-
671-
def __lt__(self, other: Any) -> bool:
672-
if isinstance(other, License):
673-
return ComparableTuple((self.id, self.name)) < ComparableTuple((other.id, other.name))
674-
return NotImplemented
675-
676-
def __hash__(self) -> int:
677-
return hash((self.id, self.name, self.text, self.url))
678-
679-
def __repr__(self) -> str:
680-
return f'<License id={self.id}, name={self.name}>'
681-
682-
683-
@serializable.serializable_class
684-
class LicenseChoice:
685-
"""
686-
This is our internal representation of `licenseChoiceType` complex type that can be used in multiple places within
687-
a CycloneDX BOM document.
688-
689-
.. note::
690-
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseChoiceType
691-
"""
692-
693-
def __init__(self, *, license: Optional[License] = None, expression: Optional[str] = None) -> None:
694-
if not license and not expression:
695-
raise NoPropertiesProvidedException(
696-
'One of `license` or `expression` must be supplied - neither supplied'
697-
)
698-
if license and expression:
699-
warnings.warn(
700-
'Both `license` and `expression` have been supplied - `license` will take precedence',
701-
RuntimeWarning
702-
)
703-
self.license = license
704-
if not license:
705-
self.expression = expression
706-
else:
707-
self.expression = None
708-
709-
@property
710-
def license(self) -> Optional[License]:
711-
"""
712-
License definition
713-
714-
Returns:
715-
`License` or `None`
716-
"""
717-
return self._license
718-
719-
@license.setter
720-
def license(self, license: Optional[License]) -> None:
721-
self._license = license
722-
723-
@property
724-
def expression(self) -> Optional[str]:
725-
"""
726-
A valid SPDX license expression (not enforced).
727-
728-
Refer to https://spdx.org/specifications for syntax requirements.
729-
730-
Returns:
731-
`str` or `None`
732-
"""
733-
return self._expression
734-
735-
@expression.setter
736-
def expression(self, expression: Optional[str]) -> None:
737-
self._expression = expression
738-
739-
def __eq__(self, other: object) -> bool:
740-
if isinstance(other, LicenseChoice):
741-
return hash(other) == hash(self)
742-
return False
743-
744-
def __lt__(self, other: Any) -> bool:
745-
if isinstance(other, LicenseChoice):
746-
return ComparableTuple((self.license, self.expression)) < ComparableTuple(
747-
(other.license, other.expression))
748-
return NotImplemented
749-
750-
def __hash__(self) -> int:
751-
return hash((self.license, self.expression))
752-
753-
def __repr__(self) -> str:
754-
return f'<LicenseChoice license={self.license}, expression={self.expression}>'
755-
756-
757579
@serializable.serializable_class
758580
class Property:
759581
"""

0 commit comments

Comments
 (0)