Skip to content

Commit ab738f1

Browse files
committed
wip
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent bda96aa commit ab738f1

4 files changed

Lines changed: 99 additions & 58 deletions

File tree

cyclonedx/contrib/bom/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# This file is part of CycloneDX Python Library
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
# Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
"""Bom related functionality"""

cyclonedx/contrib/bom/utils.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# This file is part of CycloneDX Python Library
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
# Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
"""Bom related utilities"""
19+
20+
__all__ = ['BomDependencyGraphFlatMerger']
21+
22+
from collections.abc import Iterable
23+
from itertools import chain
24+
from typing import TYPE_CHECKING, Any
25+
26+
from ...model.dependency import Dependency
27+
28+
if TYPE_CHECKING: # pragma: no cover
29+
from ...model.bom import Bom, BomRef
30+
31+
32+
class BomDependencyGraphFlatMerger:
33+
def __init__(self, bom: 'Bom'):
34+
self._bom = bom
35+
# do NOT use the getter - see `reset()` for reasons
36+
self._deps = self._bom._dependencies
37+
38+
def __enter__(self) -> None:
39+
self.flatten_merge()
40+
41+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
42+
self.reset()
43+
44+
def reset(self) -> None:
45+
# Do NOT use the setter - this would create overhead and most importantly,
46+
# and this could cause deduplication of an existing malformed set.
47+
# Just access the internal field directly!
48+
self._bom._dependencies = self._deps
49+
50+
def flatten_merge(self) -> None:
51+
self._bom.dependencies = self._merge_deps(chain.from_iterable(
52+
self._flatten_dep(dep) for dep in self._deps
53+
))
54+
55+
@staticmethod
56+
def _merge_deps(deps: Iterable[Dependency]) -> Iterable[Dependency]:
57+
merged: dict[BomRef, Dependency] = {}
58+
for dep in deps:
59+
if m := merged.get(dep.ref):
60+
m.dependencies.update(dep.dependencies)
61+
else:
62+
merged[dep.ref] = Dependency(dep.ref, dep.dependencies)
63+
return merged.values()
64+
65+
@staticmethod
66+
def _flatten_dep(dep: Dependency) -> Iterable[Dependency]:
67+
if not dep.dependencies:
68+
return dep,
69+
flat: list[Dependency] = []
70+
todos: list[Dependency] = [dep]
71+
while todos:
72+
todo = todos.pop()
73+
if todo.dependencies:
74+
flat.append(Dependency(todo.ref, (Dependency(d.ref) for d in todo.dependencies)))
75+
todos.extend(todo.dependencies)
76+
return flat

cyclonedx/output/json.py

Lines changed: 3 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717

1818
from abc import abstractmethod
19-
from itertools import chain
2019
from json import dumps as json_dumps, loads as json_loads
21-
from typing import TYPE_CHECKING, Any, Iterable, Literal, Optional, Union
20+
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
2221

22+
from ..contrib.bom.utils import BomDependencyGraphFlatMerger
2323
from ..exception.output import FormatNotSupportedException
24-
from ..model.dependency import Dependency
2524
from ..schema import OutputFormat, SchemaVersion
2625
from ..schema.schema import (
2726
SCHEMA_VERSIONS,
@@ -41,58 +40,6 @@
4140
from ..model.bom import Bom, BomRef
4241

4342

44-
class _BomDependencyGraphFlatMerger:
45-
"""
46-
!!! THIS CLASS IS INTERNAL.
47-
Everything might change without any notice.
48-
"""
49-
50-
def __init__(self, bom: 'Bom'):
51-
self._bom = bom
52-
# do NOT use the getter - see `reset()` for reasons
53-
self._deps = self._bom._dependencies
54-
55-
def __enter__(self) -> None:
56-
self.flatten_merge()
57-
58-
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
59-
self.reset()
60-
61-
def reset(self) -> None:
62-
# Do NOT use the setter - this would create overhead and most importantly,
63-
# and this could cause deduplication of an existing malformed set.
64-
# Just access the internal field directly!
65-
self._bom._dependencies = self._deps
66-
67-
def flatten_merge(self) -> None:
68-
self._bom.dependencies = self._merge_deps(chain.from_iterable(
69-
self._flatten_dep(dep) for dep in self._deps
70-
))
71-
72-
@staticmethod
73-
def _merge_deps(deps: Iterable[Dependency]) -> Iterable[Dependency]:
74-
merged: dict[BomRef, Dependency] = {}
75-
for dep in deps:
76-
if m := merged.get(dep.ref):
77-
m.dependencies.update(dep.dependencies)
78-
else:
79-
merged[dep.ref] = Dependency(dep.ref, dep.dependencies)
80-
return merged.values()
81-
82-
@staticmethod
83-
def _flatten_dep(dep: Dependency) -> Iterable[Dependency]:
84-
if not dep.dependencies:
85-
return dep,
86-
flat: list[Dependency] = []
87-
todos: list[Dependency] = [dep]
88-
while todos:
89-
todo = todos.pop()
90-
if todo.dependencies:
91-
flat.append(Dependency(todo.ref, (Dependency(d.ref) for d in todo.dependencies)))
92-
todos.extend(todo.dependencies)
93-
return flat
94-
95-
9643
class Json(BaseOutput, BaseSchemaVersion):
9744

9845
def __init__(self, bom: 'Bom') -> None:
@@ -126,7 +73,7 @@ def generate(self, force_regeneration: bool = False) -> None:
12673
bom.validate()
12774
# utilize contrib.dependency.flatten() somewhere here
12875
with BomRefDiscriminator.from_bom(bom):
129-
with _BomDependencyGraphFlatMerger(bom):
76+
with BomDependencyGraphFlatMerger(bom):
13077
bom_json: dict[str, Any] = json_loads(
13178
bom.as_json( # type:ignore[attr-defined]
13279
view_=_view))

tests/_data/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,7 +1637,7 @@ def get_bom_for_issue941_nested_dependencies_irreversible_migrate() -> Bom:
16371637
dependencies=[
16381638
Dependency(component3.bom_ref)
16391639
]
1640-
),
1640+
),
16411641
]
16421642
),
16431643
Dependency(
@@ -1654,7 +1654,7 @@ def get_bom_for_issue941_nested_dependencies_irreversible_migrate() -> Bom:
16541654
dependencies=[
16551655
Dependency(component4.bom_ref)
16561656
]
1657-
)
1657+
)
16581658
)
16591659

16601660
return bom

0 commit comments

Comments
 (0)