Skip to content

Commit a2547bb

Browse files
committed
feat: add support for properties in external references
Signed-off-by: Johannes Feichtner <[email protected]>
1 parent 5d42b55 commit a2547bb

File tree

2 files changed

+106
-77
lines changed

2 files changed

+106
-77
lines changed

cyclonedx/model/__init__.py

Lines changed: 93 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,81 @@ def is_bom_link(self) -> bool:
819819
return self._uri.startswith(_BOM_LINK_PREFIX)
820820

821821

822+
@serializable.serializable_class(ignore_unknown_during_deserialization=True)
823+
class Property:
824+
"""
825+
This is our internal representation of `propertyType` complex type that can be used in multiple places within
826+
a CycloneDX BOM document.
827+
828+
.. note::
829+
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.7/xml/#type_propertyType
830+
831+
Specifies an individual property with a name and value.
832+
"""
833+
834+
def __init__(
835+
self, *,
836+
name: str,
837+
value: Optional[str] = None,
838+
) -> None:
839+
self.name = name
840+
self.value = value
841+
842+
@property
843+
@serializable.xml_attribute()
844+
def name(self) -> str:
845+
"""
846+
The name of the property.
847+
848+
Duplicate names are allowed, each potentially having a different value.
849+
850+
Returns:
851+
`str`
852+
"""
853+
return self._name
854+
855+
@name.setter
856+
def name(self, name: str) -> None:
857+
self._name = name
858+
859+
@property
860+
@serializable.xml_name('.')
861+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
862+
def value(self) -> Optional[str]:
863+
"""
864+
Value of this Property.
865+
866+
Returns:
867+
`str`
868+
"""
869+
return self._value
870+
871+
@value.setter
872+
def value(self, value: Optional[str]) -> None:
873+
self._value = value
874+
875+
def __comparable_tuple(self) -> _ComparableTuple:
876+
return _ComparableTuple((
877+
self.name, self.value
878+
))
879+
880+
def __eq__(self, other: object) -> bool:
881+
if isinstance(other, Property):
882+
return self.__comparable_tuple() == other.__comparable_tuple()
883+
return False
884+
885+
def __lt__(self, other: Any) -> bool:
886+
if isinstance(other, Property):
887+
return self.__comparable_tuple() < other.__comparable_tuple()
888+
return NotImplemented
889+
890+
def __hash__(self) -> int:
891+
return hash(self.__comparable_tuple())
892+
893+
def __repr__(self) -> str:
894+
return f'<Property name={self.name}>'
895+
896+
822897
@serializable.serializable_class(ignore_unknown_during_deserialization=True)
823898
class ExternalReference:
824899
"""
@@ -835,11 +910,13 @@ def __init__(
835910
url: XsUri,
836911
comment: Optional[str] = None,
837912
hashes: Optional[Iterable[HashType]] = None,
913+
properties: Optional[Iterable[Property]] = None,
838914
) -> None:
839915
self.url = url
840916
self.comment = comment
841917
self.type = type
842918
self.hashes = hashes or []
919+
self.properties = properties or []
843920

844921
@property
845922
@serializable.xml_sequence(1)
@@ -909,102 +986,44 @@ def hashes(self) -> 'SortedSet[HashType]':
909986
def hashes(self, hashes: Iterable[HashType]) -> None:
910987
self._hashes = SortedSet(hashes)
911988

912-
def __comparable_tuple(self) -> _ComparableTuple:
913-
return _ComparableTuple((
914-
self._type, self._url, self._comment,
915-
_ComparableTuple(self._hashes)
916-
))
917-
918-
def __eq__(self, other: object) -> bool:
919-
if isinstance(other, ExternalReference):
920-
return self.__comparable_tuple() == other.__comparable_tuple()
921-
return False
922-
923-
def __lt__(self, other: Any) -> bool:
924-
if isinstance(other, ExternalReference):
925-
return self.__comparable_tuple() < other.__comparable_tuple()
926-
return NotImplemented
927-
928-
def __hash__(self) -> int:
929-
return hash(self.__comparable_tuple())
930-
931-
def __repr__(self) -> str:
932-
return f'<ExternalReference {self.type.name}, {self.url}>'
933-
934-
935-
@serializable.serializable_class(ignore_unknown_during_deserialization=True)
936-
class Property:
937-
"""
938-
This is our internal representation of `propertyType` complex type that can be used in multiple places within
939-
a CycloneDX BOM document.
940-
941-
.. note::
942-
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.7/xml/#type_propertyType
943-
944-
Specifies an individual property with a name and value.
945-
"""
946-
947-
def __init__(
948-
self, *,
949-
name: str,
950-
value: Optional[str] = None,
951-
) -> None:
952-
self.name = name
953-
self.value = value
954-
955-
@property
956-
@serializable.xml_attribute()
957-
def name(self) -> str:
958-
"""
959-
The name of the property.
960-
961-
Duplicate names are allowed, each potentially having a different value.
962-
963-
Returns:
964-
`str`
965-
"""
966-
return self._name
967-
968-
@name.setter
969-
def name(self, name: str) -> None:
970-
self._name = name
971-
972989
@property
973-
@serializable.xml_name('.')
974-
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
975-
def value(self) -> Optional[str]:
990+
@serializable.view(SchemaVersion1Dot7)
991+
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
992+
def properties(self) -> 'SortedSet[Property]':
976993
"""
977-
Value of this Property.
994+
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
995+
officially supported in the standard without having to use additional namespaces or create extensions.
978996
979-
Returns:
980-
`str`
997+
Return:
998+
Set of `Property`
981999
"""
982-
return self._value
1000+
return self._properties
9831001

984-
@value.setter
985-
def value(self, value: Optional[str]) -> None:
986-
self._value = value
1002+
@properties.setter
1003+
def properties(self, properties: Iterable[Property]) -> None:
1004+
self._properties = SortedSet(properties)
9871005

9881006
def __comparable_tuple(self) -> _ComparableTuple:
9891007
return _ComparableTuple((
990-
self.name, self.value
1008+
self._type, self._url, self._comment,
1009+
_ComparableTuple(self._hashes), _ComparableTuple(self.properties),
9911010
))
9921011

9931012
def __eq__(self, other: object) -> bool:
994-
if isinstance(other, Property):
1013+
if isinstance(other, ExternalReference):
9951014
return self.__comparable_tuple() == other.__comparable_tuple()
9961015
return False
9971016

9981017
def __lt__(self, other: Any) -> bool:
999-
if isinstance(other, Property):
1018+
if isinstance(other, ExternalReference):
10001019
return self.__comparable_tuple() < other.__comparable_tuple()
10011020
return NotImplemented
10021021

10031022
def __hash__(self) -> int:
10041023
return hash(self.__comparable_tuple())
10051024

10061025
def __repr__(self) -> str:
1007-
return f'<Property name={self.name}>'
1026+
return f'<ExternalReference {self.type.name}, {self.url}>'
10081027

10091028

10101029
@serializable.serializable_class(ignore_unknown_during_deserialization=True)

tests/test_model_component.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,24 @@ def test_multiple_basic_components(self) -> None:
132132

133133
def test_external_references(self) -> None:
134134
c1 = Component(name='test-component')
135+
properties = [
136+
Property(name='property_1', value='value_1'),
137+
Property(name='property_2', value='value_2')
138+
]
135139
c1.external_references.add(ExternalReference(
136140
type=ExternalReferenceType.OTHER,
137141
url=XsUri('https://cyclonedx.org'),
138-
comment='No comment'
142+
comment='No comment',
143+
properties=properties
139144
))
140145
self.assertEqual(c1.name, 'test-component')
141146
self.assertIsNone(c1.version)
142147
self.assertEqual(c1.type, ComponentType.LIBRARY)
143148
self.assertEqual(len(c1.external_references), 1)
144149
self.assertEqual(len(c1.hashes), 0)
150+
self.assertIsNotNone(c1.external_references[0].properties)
151+
self.assertIn(properties[0], c1.external_references[0].properties)
152+
self.assertIn(properties[1], c1.external_references[0].properties)
145153

146154
c2 = Component(name='test2-component')
147155
self.assertEqual(c2.name, 'test2-component')
@@ -163,13 +171,15 @@ def test_component_equal_1(self) -> None:
163171
c1.external_references.add(ExternalReference(
164172
type=ExternalReferenceType.OTHER,
165173
url=XsUri('https://cyclonedx.org'),
166-
comment='No comment'
174+
comment='No comment',
175+
properties=[Property(name='property_1', value='value_1')]
167176
))
168177
c2 = Component(name='test-component')
169178
c2.external_references.add(ExternalReference(
170179
type=ExternalReferenceType.OTHER,
171180
url=XsUri('https://cyclonedx.org'),
172-
comment='No comment'
181+
comment='No comment',
182+
properties=[Property(name='property_1', value='value_1')]
173183
))
174184
self.assertEqual(c1, c2)
175185

0 commit comments

Comments
 (0)