Skip to content

Commit

Permalink
fix #365
Browse files Browse the repository at this point in the history
Signed-off-by: Jan Kowalleck <[email protected]>
  • Loading branch information
jkowalleck committed Sep 25, 2023
1 parent 7186b52 commit afe0ba9
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 233 deletions.
24 changes: 12 additions & 12 deletions cyclonedx/factory/license.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@

from typing import Optional

from ..model.license import License, LicenseChoice, Expression
from ..exception.factory import InvalidLicenseExpressionException, InvalidSpdxLicenseException
from ..model import AttachedText, License, LicenseChoice, XsUri
from ..model import AttachedText, XsUri
from ..spdx import fixup_id as spdx_fixup, is_compound_expression as is_spdx_compound_expression


Expand Down Expand Up @@ -51,40 +52,39 @@ def make_with_name(self, name: str, *, text: Optional[AttachedText] = None, url:


class LicenseChoiceFactory:
"""Factory for :class:`cyclonedx.model.LicenseChoice`."""
"""Factory for :type:`cyclonedx.model.LicenseChoice`."""

def __init__(self, *, license_factory: LicenseFactory) -> None:
self.license_factory = license_factory

def make_from_string(self, expression_or_name_or_spdx: str) -> LicenseChoice:
"""Make a :class:`cyclonedx.model.LicenseChoice` from a string.
"""Make a :type:`cyclonedx.model.LicenseChoice` from a string.
Priority: SPDX license ID, SPDX license expression, named license
"""
try:
return LicenseChoice(license=self.license_factory.make_with_id(expression_or_name_or_spdx))
return self.license_factory.make_with_id(expression_or_name_or_spdx)
except InvalidSpdxLicenseException:
pass
try:
return self.make_with_compound_expression(expression_or_name_or_spdx)
except InvalidLicenseExpressionException:
pass
return LicenseChoice(license=self.license_factory.make_with_name(expression_or_name_or_spdx))
return self.license_factory.make_with_name(expression_or_name_or_spdx)

def make_with_compound_expression(self, compound_expression: str) -> LicenseChoice:
"""Make a :class:`cyclonedx.model.LicenseChoice` with a compound expression.
def make_with_compound_expression(self, compound_expression: str) -> Expression:
"""Make a :class:`cyclonedx.model.LicenseExpression` with a compound expression.
Utilizes :func:`cyclonedx.spdx.is_compound_expression`.
:raises InvalidLicenseExpressionException: if `expression` is not known/supported license expression
"""
if is_spdx_compound_expression(compound_expression):
return LicenseChoice(expression=compound_expression)
return Expression(compound_expression)
raise InvalidLicenseExpressionException(compound_expression)

def make_with_license(self, name_or_spdx: str, *,
license_text: Optional[AttachedText] = None,
license_url: Optional[XsUri] = None) -> LicenseChoice:
"""Make a :class:`cyclonedx.model.LicenseChoice` with a license (name or SPDX-ID)."""
return LicenseChoice(license=self.license_factory.make_from_string(
name_or_spdx, license_text=license_text, license_url=license_url))
license_url: Optional[XsUri] = None) -> License:
"""Make a :class:`cyclonedx.model.License`."""
return self.license_factory.make_from_string(name_or_spdx, license_text=license_text, license_url=license_url)
181 changes: 2 additions & 179 deletions cyclonedx/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import hashlib
import re
import warnings
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Iterable, Optional, Tuple, TypeVar
Expand All @@ -29,7 +28,6 @@
from ..exception.model import (
InvalidLocaleTypeException,
InvalidUriException,
MutuallyExclusivePropertiesException,
NoPropertiesProvidedException,
UnknownHashTypeException,
)
Expand Down Expand Up @@ -449,14 +447,14 @@ def uri(self) -> str:
return self._uri

@classmethod
def serialize(cls, o: object) -> str:
def serialize(cls, o: object, t: 'SerializationType') -> str:
if isinstance(o, XsUri):
return str(o)

raise ValueError(f'Attempt to serialize a non-XsUri: {o.__class__}')

@classmethod
def deserialize(cls, o: object) -> 'XsUri':
def deserialize(cls, o: object, t: 'SerializationType') -> 'XsUri':
try:
return XsUri(uri=str(o))
except ValueError:
Expand Down Expand Up @@ -584,181 +582,6 @@ def __repr__(self) -> str:
return f'<ExternalReference {self.type.name}, {self.url}>'


@serializable.serializable_class
class License:
"""
This is our internal representation of `licenseType` complex type that can be used in multiple places within
a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseType
"""

def __init__(self, *, id: Optional[str] = None, name: Optional[str] = None,
text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> None:
if not id and not name:
raise MutuallyExclusivePropertiesException('Either `id` or `name` MUST be supplied')
if id and name:
warnings.warn(
'Both `id` and `name` have been supplied - `name` will be ignored!',
RuntimeWarning
)
self.id = id
if not id:
self.name = name
else:
self.name = None
self.text = text
self.url = url

@property
def id(self) -> Optional[str]:
"""
A valid SPDX license ID
Returns:
`str` or `None`
"""
return self._id

@id.setter
def id(self, id: Optional[str]) -> None:
self._id = id

@property
def name(self) -> Optional[str]:
"""
If SPDX does not define the license used, this field may be used to provide the license name.
Returns:
`str` or `None`
"""
return self._name

@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name

@property
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
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 __eq__(self, other: object) -> bool:
if isinstance(other, License):
return hash(other) == hash(self)
return False

def __lt__(self, other: Any) -> bool:
if isinstance(other, License):
return ComparableTuple((self.id, self.name)) < ComparableTuple((other.id, other.name))
return NotImplemented

def __hash__(self) -> int:
return hash((self.id, self.name, self.text, self.url))

def __repr__(self) -> str:
return f'<License id={self.id}, name={self.name}>'


@serializable.serializable_class
class LicenseChoice:
"""
This is our internal representation of `licenseChoiceType` complex type that can be used in multiple places within
a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseChoiceType
"""

def __init__(self, *, license: Optional[License] = None, expression: Optional[str] = None) -> None:
if not license and not expression:
raise NoPropertiesProvidedException(
'One of `license` or `expression` must be supplied - neither supplied'
)
if license and expression:
warnings.warn(
'Both `license` and `expression` have been supplied - `license` will take precedence',
RuntimeWarning
)
self.license = license
if not license:
self.expression = expression
else:
self.expression = None

@property
def license(self) -> Optional[License]:
"""
License definition
Returns:
`License` or `None`
"""
return self._license

@license.setter
def license(self, license: Optional[License]) -> None:
self._license = license

@property
def expression(self) -> Optional[str]:
"""
A valid SPDX license expression (not enforced).
Refer to https://spdx.org/specifications for syntax requirements.
Returns:
`str` or `None`
"""
return self._expression

@expression.setter
def expression(self, expression: Optional[str]) -> None:
self._expression = expression

def __eq__(self, other: object) -> bool:
if isinstance(other, LicenseChoice):
return hash(other) == hash(self)
return False

def __lt__(self, other: Any) -> bool:
if isinstance(other, LicenseChoice):
return ComparableTuple((self.license, self.expression)) < ComparableTuple(
(other.license, other.expression))
return NotImplemented

def __hash__(self) -> int:
return hash((self.license, self.expression))

def __repr__(self) -> str:
return f'<LicenseChoice license={self.license}, expression={self.expression}>'


@serializable.serializable_class
class Property:
"""
Expand Down
11 changes: 5 additions & 6 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
import serializable
from sortedcontainers import SortedSet

from cyclonedx.serialization import UrnUuidHelper
from cyclonedx.serialization import UrnUuidHelper, LicenseRepositoryHelper
from .license import LicenseRepository, LicenseChoice

from ..exception.model import UnknownComponentDependencyException
from ..parser import BaseParser
Expand All @@ -37,7 +38,6 @@
)
from . import (
ExternalReference,
LicenseChoice,
OrganizationalContact,
OrganizationalEntity,
Property,
Expand Down Expand Up @@ -195,9 +195,8 @@ def supplier(self, supplier: Optional[OrganizationalEntity]) -> None:
@property
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'licenses')
@serializable.xml_sequence(7)
def licenses(self) -> "SortedSet[LicenseChoice]":
@serializable.type_mapping(LicenseRepositoryHelper)
def licenses(self) -> LicenseRepository:
"""
A optional list of statements about how this BOM is licensed.
Expand All @@ -208,7 +207,7 @@ def licenses(self) -> "SortedSet[LicenseChoice]":

@licenses.setter
def licenses(self, licenses: Iterable[LicenseChoice]) -> None:
self._licenses = SortedSet(licenses)
self._licenses = LicenseRepository(licenses)

@property
@serializable.view(SchemaVersion1Dot3)
Expand Down
20 changes: 10 additions & 10 deletions cyclonedx/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from packageurl import PackageURL
from sortedcontainers import SortedSet

from .license import LicenseChoice, LicenseRepository
from ..exception.model import NoPropertiesProvidedException
from ..schema.schema import (
SchemaVersion1Dot0,
Expand All @@ -36,7 +37,7 @@
SchemaVersion1Dot3,
SchemaVersion1Dot4,
)
from ..serialization import BomRefHelper, PackageUrl
from ..serialization import BomRefHelper, PackageUrl, LicenseRepositoryHelper
from . import (
AttachedText,
ComparableTuple,
Expand All @@ -45,11 +46,10 @@
HashAlgorithm,
HashType,
IdentifiableAction,
LicenseChoice,
OrganizationalEntity,
Property,
XsUri,
sha1sum,
sha1sum
)
from .bom_ref import BomRef
from .dependency import Dependable
Expand Down Expand Up @@ -196,8 +196,8 @@ def __init__(self, *, licenses: Optional[Iterable[LicenseChoice]] = None,
self.copyright = copyright or [] # type: ignore

@property
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'license')
def licenses(self) -> "SortedSet[LicenseChoice]":
@serializable.type_mapping(LicenseRepositoryHelper)
def licenses(self) -> LicenseRepository:
"""
Optional list of licenses obtained during analysis.
Expand All @@ -208,7 +208,7 @@ def licenses(self) -> "SortedSet[LicenseChoice]":

@licenses.setter
def licenses(self, licenses: Iterable[LicenseChoice]) -> None:
self._licenses = SortedSet(licenses)
self._licenses = LicenseRepository(licenses)

@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'text')
Expand Down Expand Up @@ -809,7 +809,7 @@ def __init__(self, *, name: str, type: ComponentType = ComponentType.LIBRARY,
'standard', DeprecationWarning
)
if not licenses:
self.licenses = [LicenseChoice(expression=license_str)] # type: ignore
self.licenses = [LicenseExpression(license_str)] # type: ignore

self.__dependencies: "SortedSet[BomRef]" = SortedSet()

Expand Down Expand Up @@ -1032,9 +1032,9 @@ def hashes(self, hashes: Iterable[HashType]) -> None:
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'licenses')
@serializable.type_mapping(LicenseRepositoryHelper)
@serializable.xml_sequence(10)
def licenses(self) -> "SortedSet[LicenseChoice]":
def licenses(self) -> LicenseRepository:
"""
A optional list of statements about how this Component is licensed.
Expand All @@ -1045,7 +1045,7 @@ def licenses(self) -> "SortedSet[LicenseChoice]":

@licenses.setter
def licenses(self, licenses: Iterable[LicenseChoice]) -> None:
self._licenses = SortedSet(licenses)
self._licenses = LicenseRepository(licenses)

@property
@serializable.xml_sequence(11)
Expand Down
Loading

0 comments on commit afe0ba9

Please sign in to comment.