Skip to content

Commit 6b4be72

Browse files
yt-msMidnighter
authored andcommitted
refactor: rationalise add* methods for adding components to containers.
1 parent 29360df commit 6b4be72

File tree

6 files changed

+58
-43
lines changed

6 files changed

+58
-43
lines changed

src/structurizr/model/container.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
"""Provide a container model."""
1717

18+
from __future__ import annotations
1819

1920
from typing import TYPE_CHECKING, Iterable, List, Optional
2021

@@ -70,7 +71,7 @@ class Container(StaticStructureElement):
7071
technology: The technology associated with this container
7172
(e.g. Apache Tomcat).
7273
tags: A comma separated list of tags associated with this container.
73-
components: The set of components within this container.
74+
components: The set of components within this container. Do not modify directly.
7475
properties: A set of arbitrary name-value properties.
7576
relationships: The set of relationships from this container to
7677
other elements.
@@ -83,7 +84,7 @@ def __init__(
8384
parent: "SoftwareSystem",
8485
technology: str = "",
8586
components: Iterable[Component] = (),
86-
**kwargs
87+
**kwargs,
8788
):
8889
"""
8990
Initialize a container model.
@@ -116,22 +117,39 @@ def hydrate(
116117
parent=software_system,
117118
technology=container_io.technology,
118119
)
120+
model.add_container(container)
119121

120122
for component_io in container_io.components:
121123
component = Component.hydrate(component_io, container=container)
122-
model.add_component(component, parent=container)
124+
container += component
123125

124126
return container
125127

126-
def add_component(self, component: Component = None, **kwargs) -> Component:
127-
return self.get_model().add_component(
128-
parent=self,
129-
component=component,
130-
**kwargs,
131-
)
132-
133-
def add(self, component: Component) -> None:
128+
def add_component(self, **kwargs) -> Component:
129+
"""Add a new component to this container."""
130+
component = Component(**kwargs)
131+
self += component
132+
return component
133+
134+
def __iadd__(self, component: Component) -> Container:
135+
if component in self.components:
136+
# Nothing to do
137+
return self
138+
139+
if self.get_component_with_name(component.name):
140+
raise ValueError(
141+
f"Component with name {component.name} already exists for {self}."
142+
)
143+
144+
if component.parent is None:
145+
component.parent = self
146+
elif component.parent is not self:
147+
raise ValueError(
148+
f"Component with name {component.name} already has parent {component.parent}. Cannot add to {self}."
149+
)
134150
self.components.add(component)
151+
self.get_model().add_component(component)
152+
return self
135153

136154
def get_component_with_name(self, name: str) -> Component:
137155
for component in self.components:

src/structurizr/model/model.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -274,22 +274,15 @@ def add_container_instance(
274274

275275
def add_component(
276276
self,
277-
component: Optional[Component] = None,
278-
**kwargs,
277+
component: Component,
279278
) -> Component:
280-
if not component:
281-
component = Component(**kwargs)
282-
283-
# TODO (Midnighter): It seems like a lot of this logic could be on the
284-
# Component constructor.
285-
if component.parent.get_component_with_name(component.name):
279+
"""Register a newly constructed Component with the model."""
280+
if component.parent is None:
286281
raise ValueError(
287-
f"Component with name {component.name} already exists for "
288-
f"{component.parent}."
282+
f"Component with name {component.name} has no parent container. "
283+
f"Adding to container will register with the container's model."
289284
)
290285

291-
# TODO(ilaif): @midnighter - Might want to improve this impl:
292-
component.parent.add(component)
293286
self._add_element(component)
294287
return component
295288

src/structurizr/model/software_system.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,10 @@ def hydrate(
9999
)
100100

101101
for container_io in software_system_io.containers:
102-
container = Container.hydrate(
102+
Container.hydrate(
103103
container_io,
104104
software_system=software_system,
105105
model=model,
106106
)
107-
model.add_container(container, parent=software_system)
108107

109108
return software_system

tests/unit/model/test_container.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import pytest
1818

19-
from structurizr.model import Component, Container, Model, SoftwareSystem
19+
from structurizr.model import Component, Container, Model
2020

2121

2222
_model = Model()
@@ -44,47 +44,44 @@ def test_container_init(attributes):
4444

4545

4646
def test_container_add_component_adds_to_component_list(empty_container: Container):
47-
"""Ensure that add_component() adds the new component to Container.components and sets up other properties."""
47+
"""Verify add_component() adds the new component to Container.components."""
4848
component = empty_container.add_component(name="Component")
4949
assert component in empty_container.components
5050
assert component.id != ""
5151
assert component.model is _model
5252
assert component.parent is empty_container
5353

5454

55-
@pytest.mark.xfail(strict=True)
5655
def test_container_add_constructed_component(empty_container: Container):
57-
"""Verify behaviour when adding a newly constructed Container rather than calling add_container()."""
56+
"""Verify behaviour when adding a newly constructed Container."""
5857
component = Component(name="Component")
59-
empty_container.add(component)
58+
empty_container += component
6059
assert component in empty_container.components
61-
assert component.id != "" # Currently failing
62-
assert component.model is _model # Currently failing
63-
assert component.parent is empty_container # Currently failing
60+
assert component.id != ""
61+
assert component.model is _model
62+
assert component.parent is empty_container
6463

6564

6665
def test_container_adding_component_twice_is_ok(empty_container: Container):
6766
"""Defensive check that adding the same component twice is OK."""
6867
component = Component(name="Component")
69-
empty_container.add(component)
70-
empty_container.add(component)
68+
empty_container += component
69+
empty_container += component
7170
assert len(empty_container.components) == 1
7271

7372

74-
@pytest.mark.xfail(strict=True)
7573
def test_container_adding_component_with_same_name_fails(empty_container: Container):
76-
"""Defensive check that adding a component with the same name as an existing one fails."""
74+
"""Check that adding a component with the same name as an existing one fails."""
7775
empty_container.add_component(name="Component")
7876
with pytest.raises(ValueError, match="Component with name .* already exists"):
7977
empty_container.add_component(name="Component")
8078
with pytest.raises(ValueError, match="Component with name .* already exists"):
81-
empty_container.add(Component(name="Component")) # Doesn't currently raise
79+
empty_container += Component(name="Component")
8280

8381

84-
@pytest.mark.xfail(strict=True)
8582
def test_adding_component_with_existing_parent_fails(empty_container: Container):
86-
"""Defensive check that if a component already has a (different) parent then it can't be added."""
83+
"""Check that adding a component with a different parent fails."""
8784
container2 = _system.add_container(name="Container 2", description="Description")
8885
component = empty_container.add_component(name="Component")
89-
with pytest.raises(ValueError):
90-
container2.add(component) # Doesn't currently raise
86+
with pytest.raises(ValueError, match="Component with name .* already has parent"):
87+
container2 += component

tests/unit/model/test_model.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import pytest
1818

19-
from structurizr.model import Model
19+
from structurizr.model import Component, Model
2020

2121

2222
@pytest.fixture(scope="function")
@@ -62,3 +62,10 @@ def test_model_cannot_add_relationship_with_same_id_as_element(empty_model: Mode
6262
ValueError, match="Relationship.* has the same ID as SoftwareSystem.*"
6363
):
6464
empty_model.add_relationship(source=sys1, destination=sys2, id=sys1.id)
65+
66+
67+
def test_model_add_component_must_have_parent(empty_model: Model):
68+
"""Ensure that Model rejects adding Components that aren't within a Container."""
69+
component = Component(name="c1")
70+
with pytest.raises(ValueError, match="Component with name .* has no parent"):
71+
empty_model.add_component(component)

tests/unit/model/test_software_system.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def test_add_container_accepts_additional_args():
5151

5252

5353
def test_add_container_technology_is_optional(empty_model: Model):
54+
"""Ensure that you don't have to specify the technology."""
5455
system = empty_model.add_software_system(name="sys")
5556
container = system.add_container(name="Container", description="Description")
5657
assert container.technology == ""

0 commit comments

Comments
 (0)