Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,9 +743,9 @@ def has_component(self, component: Component) -> bool:

def _get_all_components(self) -> Generator[Component, None, None]:
if self.metadata.component:
yield from self.metadata.component.get_all_nested_components(include_self=True)
yield from self.metadata.component.iter_all_nested_components(include_self=True)
for c in self.components:
yield from c.get_all_nested_components(include_self=True)
yield from c.iter_all_nested_components(include_self=True)

def get_vulnerabilities_for_bom_ref(self, bom_ref: BomRef) -> 'SortedSet[Vulnerability]':
"""
Expand Down Expand Up @@ -856,8 +856,8 @@ def validate(self) -> bool:
elem: Union[BomMetaData, Component, Service]
for elem in chain( # type:ignore[assignment]
[self.metadata],
self.metadata.component.get_all_nested_components(include_self=True) if self.metadata.component else [],
chain.from_iterable(c.get_all_nested_components(include_self=True) for c in self.components),
self.metadata.component.iter_all_nested_components(include_self=True) if self.metadata.component else [],
chain.from_iterable(c.iter_all_nested_components(include_self=True) for c in self.components),
self.services
):
if len(elem.licenses) > 1 and any(isinstance(li, LicenseExpression) for li in elem.licenses):
Expand Down
30 changes: 22 additions & 8 deletions cyclonedx/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import re
import sys
from collections.abc import Iterable
from collections.abc import Iterable, Iterator
from enum import Enum
from typing import Any, Optional, Union
from warnings import warn
Expand Down Expand Up @@ -1684,15 +1684,29 @@ def tags(self) -> 'SortedSet[str]':
def tags(self, tags: Iterable[str]) -> None:
self._tags = SortedSet(tags)

def get_all_nested_components(self, include_self: bool = False) -> set['Component']:
components = set()
if include_self:
components.add(self)
def iter_all_nested_components(self, include_self: bool = False) -> Iterator['Component']:
"""
Iterate over this component and all its nested components (assemblies), recursively.

for c in self.components:
components.update(c.get_all_nested_components(include_self=True))
Components are deduplicated by object identity — NOT by equality.
This never calls the (costly) ``Component.__hash__``/``__eq__``,
which makes it dramatically faster than :func:`get_all_nested_components`
for large component trees. Safe against cyclic component graphs.

return components
Order of iteration is not guaranteed.
"""
seen: dict[int, 'Component'] = {}
stack: list['Component'] = [self] if include_self else list(self.components)
while stack:
current = stack.pop()
current_id = id(current)
if current_id not in seen:
seen[current_id] = current
stack.extend(current.components)
return iter(seen.values())

def get_all_nested_components(self, include_self: bool = False) -> set['Component']:
return set(self.iter_all_nested_components(include_self=include_self))

def get_pypi_url(self) -> str:
if self.version:
Expand Down
5 changes: 5 additions & 0 deletions tests/test_model_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def test_empty_basic_component(self) -> None:
self.assertIsNone(c.release_notes)
self.assertEqual(len(c.components), 0)
self.assertEqual(len(c.get_all_nested_components(include_self=True)), 1)
self.assertEqual(len(list(c.iter_all_nested_components(include_self=True))), 1)
Comment thread
jkowalleck marked this conversation as resolved.

def test_multiple_basic_components(self) -> None:
c1 = Component(name='test-component')
Expand Down Expand Up @@ -312,6 +313,8 @@ def test_nested_components_1(self) -> None:
self.assertEqual(1, len(comp_b.components))
self.assertEqual(2, len(comp_b.get_all_nested_components(include_self=True)))
self.assertEqual(1, len(comp_b.get_all_nested_components(include_self=False)))
self.assertEqual(2, len(list(comp_b.iter_all_nested_components(include_self=True))))
self.assertEqual(1, len(list(comp_b.iter_all_nested_components(include_self=False))))

def test_nested_components_2(self) -> None:
comp_a = Component(name='comp_a')
Expand All @@ -323,6 +326,8 @@ def test_nested_components_2(self) -> None:
self.assertEqual(2, len(comp_b.components))
self.assertEqual(3, len(comp_b.get_all_nested_components(include_self=True)))
self.assertEqual(2, len(comp_b.get_all_nested_components(include_self=False)))
self.assertEqual(3, len(list(comp_b.iter_all_nested_components(include_self=True))))
self.assertEqual(2, len(list(comp_b.iter_all_nested_components(include_self=False))))


class TestModelDiff(TestCase):
Expand Down
Loading