Skip to content

Commit 8223373

Browse files
committed
sw360_objects: support subprojects and de-duplicate objects
1 parent 9fc932c commit 8223373

File tree

4 files changed

+94
-48
lines changed

4 files changed

+94
-48
lines changed

sw360/sw360_objects.py

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,50 @@ class SW360Resource:
2323
release etc.
2424
"""
2525

26-
def __init__(self, json=None, resource_id=None, sw360=None, **kwargs):
27-
self.details = {}
26+
all_resources = {}
27+
28+
def __new__(cls, json=None, resource_id=None, **kwargs):
29+
"""Check if this resource already exists."""
30+
key = None
31+
if resource_id:
32+
key = (cls.__name__, resource_id)
33+
elif json and "id" in json:
34+
key = (cls.__name__, json["id"])
35+
36+
if key:
37+
if key not in cls.all_releases:
38+
cls.all_resources[key] = super(SW360Resource, cls).__new__(cls)
39+
return cls.all_resources[key]
40+
else:
41+
return super(SW360Resource, cls).__new__(cls)
42+
43+
def __init__(self, json=None, resource_id=None, parent=None, users={}, sw360=None, **kwargs):
44+
if not hasattr(self, "details"):
45+
self.details = {}
2846
"""All resource details which are not explicitely supported by the
2947
constructor parameters of the derived objexts are stored in the
3048
`details` attribute. Shall use names and types as specified in SW360
3149
REST API."""
3250

51+
if not hasattr(self, "users"):
52+
self.users = {}
53+
54+
if resource_id and getattr(self, "id", resource_id) != resource_id:
55+
raise ValueError("Resource id mismatch", self.id, resource_id)
3356
self.id = resource_id
3457
"""All SW360 resource instances have an `id`. If it is set to `None`,
3558
the object is yet unknown to SW360 - otherwise, it stores the SW360
3659
id (release_id, component_id, etc.)."""
3760

61+
if parent and hasattr(self, "parent"):
62+
if self.parent.id != parent.id:
63+
raise ValueError("Resource parent mismatch", self.parent.id, parent.id)
64+
self.parent = parent
65+
66+
for k, v in users.items():
67+
# merge new users with existing ones
68+
self.users[k] = v
69+
3870
self.sw360 = sw360
3971
"""SW360 api object"""
4072

@@ -44,43 +76,66 @@ def __init__(self, json=None, resource_id=None, sw360=None, **kwargs):
4476
if json is not None:
4577
self.from_json(json)
4678

47-
def _parse_release_list(self, json_list, component_id=None):
79+
def __setattr__(self, name, value):
80+
# assure that the resource is registered in the global resource list
81+
# even if the id is set later
82+
super().__setattr__(name, value)
83+
if name == "id" and value is not None:
84+
key = (self.__class__.__name__, value)
85+
self.all_resources[key] = self
86+
87+
def _parse_release_list(self, json_list, parent=None, users={}):
4888
"""Parse a JSON list of releases, create according objects and add
4989
them to `container`."""
5090
releases = {}
5191
for release_json in json_list:
52-
release = Release(component_id=component_id, sw360=self.sw360)
92+
release = Release(parent=parent, users=users, sw360=self.sw360)
5393
release.from_json(release_json)
5494
releases[release.id] = release
5595
return releases
5696

57-
def _parse_attachment_list(self, json_list, resources=[]):
97+
def _parse_attachment_list(self, json_list, parent=None):
5898
"""Parse a JSON list of releases, create according objects and add
5999
them to `container`."""
60100
attachments = {}
61101
for attachment_json in json_list:
62-
attachment = Attachment(resources=resources, sw360=self.sw360)
102+
attachment = Attachment(parent=parent, sw360=self.sw360)
63103
attachment.from_json(attachment_json)
64104
attachments[attachment.id] = attachment
65105
return attachments
66106

107+
def _parse_project_list(self, json_list, users={}):
108+
"""Parse a JSON list of projects, create according objects and add
109+
them to `container`."""
110+
projects = {}
111+
for project_json in json_list:
112+
project = Project(users=users, sw360=self.sw360)
113+
project.from_json(project_json)
114+
projects[project.id] = project
115+
return projects
116+
67117
def _parse_link(self, key, links_key, links_value):
68118
"""Parse a _links or _embedded section in JSON"""
69119
if links_key == "sw360:component":
70-
self.component_id = links_value["href"].split("/")[-1]
120+
self.parent = Component(component_id=links_value["href"].split("/")[-1], sw360=self.sw360)
71121
elif links_key == "sw360:downloadLink":
72122
self.download_link = links_value["href"]
73123
elif links_key == "sw360:attachments":
74124
self.attachments = self._parse_attachment_list(
75125
links_value,
76-
resources=[self])
126+
parent=self)
77127
elif links_key == "sw360:releases":
78-
component_id = None
79128
if isinstance(self, Component):
80-
component_id = self.id
129+
parent = self
130+
users = {}
131+
else:
132+
parent = None
133+
users = {self.id: self}
81134
self.releases = self._parse_release_list(
82-
links_value,
83-
component_id=component_id)
135+
links_value, parent=parent, users=users)
136+
elif links_key == "sw360:projects":
137+
self.projects = self._parse_project_list(
138+
links_value, {self.id: self})
84139
elif links_key == "self":
85140
self.id = links_value["href"].split("/")[-1]
86141
else:
@@ -162,35 +217,38 @@ class Release(SW360Resource):
162217
For JSON parsing, please read documentation of from_json() method.
163218
164219
:param json: create release from SW360 JSON object by calling from_json()
165-
:param component_id: SW360 id of the component the release belongs to
220+
:param parent: SW360 component the release belongs to
221+
:param users: dictionary of SW360 resources which link to the release
222+
(instances of Release() or Project() with id as key)
166223
:param version: the actual version
167224
:param downloadurl: URL the release was downloaded from
168225
:param release_id: id of the release (if exists in SW360 already)
169226
:param sw360: your SW360 instance for interacting with the API
170227
:param kwargs: additional relase details as specified in the SW360 REST API
171228
:type json: SW360 JSON object
172-
:type component_id: string
229+
:type parent: Component() object
230+
:type users: dictionary
173231
:type version: string
174232
:type downloadurl: string
175233
:type release_id: string
176234
:type sw360: instance from SW360 class
177235
:type kwargs: dictionary
178236
"""
179-
def __init__(self, json=None, release_id=None, component_id=None,
237+
def __init__(self, json=None, release_id=None, parent=None, users={},
180238
name=None, version=None, downloadurl=None, sw360=None, **kwargs):
181239
self.attachments = {}
182240
self.external_ids = {}
183241
self.purls = []
184242

185243
self.name = name
186244
self.version = version
187-
self.component_id = component_id
188245
self.downloadurl = downloadurl
189-
super().__init__(json, release_id, sw360=sw360, **kwargs)
246+
super().__init__(json=json, resource_id=release_id, parent=parent, users=users,
247+
sw360=sw360, **kwargs)
190248

191249
def from_json(self, json):
192250
"""Parse release JSON object from SW360 REST API. The component it
193-
belongs to will be extracted and stored in the `component_id`
251+
belongs to will be extracted and stored in the `parent`
194252
attribute.
195253
196254
SW360 external ids will be stored in the `external_ids` attribute.
@@ -214,12 +272,6 @@ def get(self, sw360=None, id_=None):
214272
self.from_json(self.sw360.get_release(self.id))
215273
return self
216274

217-
def get_component(self, sw360=None):
218-
"""Retrieve/update component of this release."""
219-
if sw360:
220-
self.sw360 = sw360
221-
return Component().get(self.sw360, self.component_id)
222-
223275
def __str__(self):
224276
return f'{self.name} {self.version} ({self.id})'
225277

@@ -239,8 +291,7 @@ class Attachment(SW360Resource):
239291
240292
:param json: create it from SW360 JSON object by calling from_json()
241293
:param attachment_id: SW360 id of the attachment (if it exists already)
242-
:param resources: dictionary of SW360 resource objects the attachment belongs to
243-
(instances of Release(), Component() or Project() with id as key)
294+
:param parent: SW360 resource (release, component or project) the attachment belongs to
244295
:param filename: the filename of the attachment
245296
:param sha1: SHA1 sum of the file to check its integrity
246297
:param attachment_type: one of "DOCUMENT", "SOURCE", "SOURCE_SELF"
@@ -257,14 +308,14 @@ class Attachment(SW360Resource):
257308
:type sw360: instance from SW360 class
258309
:type kwargs: dictionary
259310
"""
260-
def __init__(self, json=None, attachment_id=None, resources={},
311+
def __init__(self, json=None, attachment_id=None, parent=None,
261312
filename=None, sha1=None, attachment_type=None, sw360=None, **kwargs):
262-
self.resources = resources
263313
self.attachment_type = attachment_type
264314
self.filename = filename
265315
self.sha1 = sha1
266316
self.download_link = None
267-
super().__init__(json, attachment_id, sw360, **kwargs)
317+
super().__init__(json=json, resource_id=attachment_id, parent=parent,
318+
sw360=sw360, **kwargs)
268319

269320
def from_json(self, json):
270321
"""Parse attachment JSON object from SW360 REST API. For now, we don't
@@ -296,16 +347,6 @@ def get(self, sw360=None, id_=None):
296347
self.from_json(self.sw360.get_attachment(self.id))
297348
return self
298349

299-
def get_releases(self, sw360=None):
300-
"""Retrieve/update releases of this attachment."""
301-
if sw360:
302-
self.sw360 = sw360
303-
releases = [
304-
Release().get(self.sw360, id_)
305-
for id_ in self.releases
306-
]
307-
return releases
308-
309350
def download(self, target_path, filename=None):
310351
"""download an attachment to local file.
311352
@@ -364,7 +405,7 @@ def __init__(self, json=None, component_id=None, name=None, description=None,
364405
self.homepage = homepage
365406
self.component_type = component_type
366407

367-
super().__init__(json, component_id, sw360, **kwargs)
408+
super().__init__(json=json, resource_id=component_id, sw360=sw360, **kwargs)
368409

369410
def from_json(self, json):
370411
"""Parse component JSON object from SW360 REST API. Information for
@@ -401,7 +442,7 @@ def __str__(self):
401442

402443

403444
class Project(SW360Resource):
404-
"""A project is SW360 abstraction for a collection of software components
445+
"""A project is SW360 abstraction for a collection of software releases
405446
used in a project/product. It can contain links to other `Project`s or
406447
`Release`s.
407448
@@ -414,6 +455,8 @@ class Project(SW360Resource):
414455
415456
:param json: create component from SW360 JSON object by calling from_json()
416457
:param project_id: id of the project (if exists in SW360 already)
458+
:param users: dictionary of SW360 resources which link to the project
459+
(instances of Project() with id as key)
417460
:param name: name of the project
418461
:param version: version of the project
419462
:param description: short description for project
@@ -434,8 +477,8 @@ class Project(SW360Resource):
434477
:type sw360: instance from SW360 class
435478
:type kwargs: dictionary
436479
"""
437-
def __init__(self, json=None, project_id=None, name=None, version=None,
438-
description=None, visibility=None, project_type=None,
480+
def __init__(self, json=None, project_id=None, users={},
481+
name=None, version=None, description=None, visibility=None, project_type=None,
439482
sw360=None, **kwargs):
440483
self.releases = {}
441484
self.external_ids = {}
@@ -446,7 +489,8 @@ def __init__(self, json=None, project_id=None, name=None, version=None,
446489
self.description = description
447490
self.visibility = visibility
448491
self.project_type = project_type
449-
super().__init__(json, project_id, sw360, **kwargs)
492+
super().__init__(json=json, resource_id=project_id, users=users,
493+
sw360=sw360, **kwargs)
450494

451495
def from_json(self, json):
452496
"""Parse project JSON object from SW360 REST API. Information for

tests/test_sw360obj_component.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ def test_get_component(self):
4646
self.assertEqual(comp.details["somekey"], "value")
4747
self.assertEqual(len(comp.releases), 1)
4848
self.assertEqual(len(comp.purls), 0)
49-
self.assertEqual(comp.releases["7c4"].component_id, "123")
49+
self.assertEqual(comp.releases["7c4"].parent.id, "123")
50+
self.assertEqual(len(comp.all_resources), 2)
5051

5152
@responses.activate
5253
def test_get_component_with_purls(self):

tests/test_sw360obj_project.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ def test_get_project(self):
4545
self.assertEqual(proj.name, "MyProj")
4646
self.assertEqual(proj.version, "11.0")
4747
self.assertEqual(len(proj.releases), 1)
48-
self.assertIsNone(proj.releases["7c4"].component_id)
48+
self.assertEqual(len(proj.projects), 0)
49+
self.assertIsNone(proj.releases["7c4"].parent.id)
4950

5051
self.assertEqual(str(proj), "MyProj 11.0 (123)")
5152

tests/test_sw360obj_release.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
class Sw360ObjTestRelease(Sw360ObjTestBase):
1717
def test_repr(self):
1818
r = Release(release_id="123", name="TestCmp", version="1.4",
19-
component_id="456", downloadurl="http://www")
19+
downloadurl="http://www")
2020

2121
r = eval(repr(r))
2222
assert r.id == "123"
@@ -46,7 +46,7 @@ def test_get_release(self):
4646
self.assertEqual(r.name, "acl")
4747
self.assertEqual(r.details["somekey"], "value")
4848
self.assertEqual(len(r.purls), 0)
49-
self.assertEqual(r.component_id, "7b4")
49+
self.assertEqual(r.parent.id, "7b4")
5050

5151
@responses.activate
5252
def test_get_release_extid(self):

0 commit comments

Comments
 (0)