Skip to content

Commit 2badde1

Browse files
yt-msMidnighter
authored andcommitted
refactor: rationalise add* methods for adding containers to software systems.
1 parent 14599b3 commit 2badde1

File tree

5 files changed

+92
-59
lines changed

5 files changed

+92
-59
lines changed

src/structurizr/model/model.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -209,38 +209,29 @@ def add_software_system(self, software_system=None, **kwargs) -> SoftwareSystem:
209209
self.software_systems.add(software_system)
210210
return software_system
211211

212-
def add_container(
213-
self, container: Optional[Container] = None, **kwargs
214-
) -> Container:
212+
def add_container(self, container: Container) -> Container:
215213
"""
216-
Add a new container to the model.
214+
Register a newly constructed container with the model.
217215
218216
Args:
219-
container (Container, optional): Either provide a
220-
`Container` instance or
221-
**kwargs: Provide keyword arguments for instantiating a `Container`
222-
(recommended).
217+
container (Container): `Container` instance to register.
223218
224219
Returns:
225-
SoftwareSystem: Either the same or a new instance, depending on arguments.
220+
Container: The provided container.
226221
227222
Raises:
228-
ValueError: When a container with the same name already exists.
223+
ValueError: When the container isn't a child of a `SoftwareSystem`
229224
230225
See Also:
231226
Container
232227
233228
"""
234-
if container is None:
235-
container = Container(**kwargs)
236-
if any(container.name == c.name for c in container.parent.containers):
237-
ValueError(
238-
f"A container with the name {container.name} already "
239-
f"exists in the model."
229+
if container.parent is None:
230+
raise ValueError(
231+
f"Container with name {container.name} has no parent software system."
232+
f"Adding to software system will register with the system's model."
240233
)
241-
# TODO (midnighter): Modifying the parent seems like creating an undesired
242-
# tight link here.
243-
container.parent.add(container)
234+
244235
self._add_element(container)
245236
return container
246237

src/structurizr/model/software_system.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,41 @@ def __init__(self, *, location: Location = Location.Unspecified, **kwargs) -> No
7474
self.tags.add(Tags.ELEMENT)
7575
self.tags.add(Tags.SOFTWARE_SYSTEM)
7676

77-
def add(self, container: Container):
78-
self.containers.add(container)
79-
8077
def add_container(
8178
self, name: str, description: str, technology: str = "", **kwargs
8279
) -> Container:
83-
return self.get_model().add_container(
84-
parent=self,
85-
name=name,
86-
description=description,
87-
technology=technology,
88-
**kwargs,
80+
"""Construct a new `Container` and add to this system and its model."""
81+
container = Container(
82+
name=name, description=description, technology=technology, **kwargs
8983
)
84+
self += container
85+
return container
86+
87+
def __iadd__(self, container: Container) -> "SoftwareSystem":
88+
"""Add a newly constructed container to this system and register with its model."""
89+
# TODO: once we move past python 3.6 change to proper return type via __future__.annotations
90+
if container in self.containers:
91+
# Nothing to do
92+
return self
93+
94+
if self.get_container_with_name(container.name):
95+
raise ValueError(
96+
f"Container with name {container.name} already exists for {self}."
97+
)
98+
99+
if container.parent is None:
100+
container.parent = self
101+
elif container.parent is not self:
102+
raise ValueError(
103+
f"Container with name {container.name} already has parent {container.parent}. Cannot add to {self}."
104+
)
105+
self.containers.add(container)
106+
self.get_model().add_container(container)
107+
return self
108+
109+
def get_container_with_name(self, name: str) -> Container:
110+
"""Return the container with the given name, or None."""
111+
return next((c for c in self.containers if c.name == name), None)
90112

91113
@classmethod
92114
def hydrate(

tests/unit/model/test_container.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,23 @@
1919
from structurizr.model import Component, Container, Model
2020

2121

22-
_model = Model()
23-
_system = _model.add_software_system(name="Sys")
22+
_model = (
23+
Model()
24+
) # Have to create outside the fixture so it doesn't get garbage-collected.
2425

2526

2627
@pytest.fixture(scope="function")
2728
def empty_container() -> Container:
2829
"""Provide an empty Container on demand for test cases to use."""
29-
return _system.add_container(name="Container", description="Description")
30+
system = _model.add_software_system(name="Sys")
31+
return system.add_container(name="Container", description="Description")
3032

3133

3234
@pytest.mark.parametrize(
3335
"attributes",
3436
[
3537
pytest.param({}, marks=pytest.mark.raises(exception=TypeError)),
36-
{"name": "Mobile App", "parent": _system, "technology": "tech1"},
38+
{"name": "Mobile App", "technology": "tech1"},
3739
],
3840
)
3941
def test_container_init(attributes):
@@ -81,7 +83,9 @@ def test_container_adding_component_with_same_name_fails(empty_container: Contai
8183

8284
def test_adding_component_with_existing_parent_fails(empty_container: Container):
8385
"""Check that adding a component with a different parent fails."""
84-
container2 = _system.add_container(name="Container 2", description="Description")
86+
container2 = empty_container.parent.add_container(
87+
name="Container 2", description="Description"
88+
)
8589
component = empty_container.add_component(name="Component")
8690
with pytest.raises(ValueError, match="Component with name .* already has parent"):
8791
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 Component, Model
19+
from structurizr.model import Component, Container, Model
2020

2121

2222
@pytest.fixture(scope="function")
@@ -69,3 +69,10 @@ def test_model_add_component_must_have_parent(empty_model: Model):
6969
component = Component(name="c1")
7070
with pytest.raises(ValueError, match="Component with name .* has no parent"):
7171
empty_model.add_component(component)
72+
73+
74+
def test_model_add_container_must_have_parent(empty_model: Model):
75+
"""Ensure that Model rejects adding Containers that aren't within a SoftwareSystem."""
76+
container = Container(name="c1")
77+
with pytest.raises(ValueError, match="Container with name .* has no parent"):
78+
empty_model.add_container(container)

tests/unit/model/test_software_system.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@
1818

1919
import pytest
2020

21-
from structurizr.model.model import Model
2221
from structurizr.model.container import Container
22+
from structurizr.model.model import Model
2323
from structurizr.model.software_system import SoftwareSystem
2424

2525

26-
@pytest.fixture(scope="function")
27-
def empty_model() -> Model:
28-
"""Provide an empty Model on demand for test cases to use."""
29-
return Model()
26+
_model = (
27+
Model()
28+
) # Have to create outside the fixture so it doesn't get garbage-collected.
3029

3130

3231
@pytest.fixture(scope="function")
3332
def empty_system() -> SoftwareSystem:
3433
"""Provide an empty SoftwareSystem on demand for test cases to use."""
35-
return Model().add_software_system("Sys")
34+
software_system = _model.add_software_system(name="Sys")
35+
return software_system
3636

3737

3838
@pytest.mark.parametrize(
@@ -57,35 +57,33 @@ def test_add_container_accepts_additional_args():
5757
assert container.id == "id1"
5858

5959

60-
def test_add_container_technology_is_optional(empty_model: Model):
60+
def test_add_container_technology_is_optional(empty_system: SoftwareSystem):
6161
"""Ensure that you don't have to specify the technology."""
62-
system = empty_model.add_software_system(name="sys")
63-
container = system.add_container(name="Container", description="Description")
62+
container = empty_system.add_container(name="Container", description="Description")
6463
assert container.technology == ""
6564

66-
67-
@pytest.mark.xfail(strict=True)
68-
def test_software_system_add_container_adds_to_container_list(empty_system: SoftwareSystem):
65+
66+
def test_software_system_add_container_adds_to_container_list(
67+
empty_system: SoftwareSystem,
68+
):
6969
"""Ensure that add_container() adds the new container to SoftwareSystem.containers and sets up other properties."""
7070
container = empty_system.add_container(name="Container", description="Description")
7171
assert container in empty_system.containers
7272
assert container.id != ""
73-
assert container.model is empty_system.model
73+
assert container.model is _model
7474
assert container.parent is empty_system
7575

7676

77-
@pytest.mark.xfail(strict=True)
7877
def test_software_system_add_constructed_container(empty_system: SoftwareSystem):
7978
"""Verify behaviour when adding a newly constructed Container rather than calling add_container()."""
8079
container = Container(name="Container")
8180
empty_system += container
8281
assert container in empty_system.containers
8382
assert container.id != ""
84-
assert container.model is empty_system.model
83+
assert container.model is _model
8584
assert container.parent is empty_system
8685

8786

88-
@pytest.mark.xfail(strict=True)
8987
def test_software_system_adding_container_twice_is_ok(empty_system: SoftwareSystem):
9088
"""Defensive check that adding the same container twice is OK."""
9189
container = Container(name="Container")
@@ -94,20 +92,31 @@ def test_software_system_adding_container_twice_is_ok(empty_system: SoftwareSyst
9492
assert len(empty_system.containers) == 1
9593

9694

97-
@pytest.mark.xfail(strict=True)
98-
def test_software_system_adding_container_with_same_name_fails(empty_system: SoftwareSystem):
95+
def test_software_system_adding_container_with_same_name_fails(
96+
empty_system: SoftwareSystem,
97+
):
9998
"""Defensive check that adding a container with the same name as an existing one fails."""
100-
empty_system.add_container(name="Container")
99+
empty_system.add_container(name="Container", description="Description")
101100
with pytest.raises(ValueError, match="Container with name .* already exists"):
102-
empty_system.add_container(name="Container")
101+
empty_system.add_container(name="Container", description="Description2")
103102
with pytest.raises(ValueError, match="Container with name .* already exists"):
104-
empty_system += Container(name="Container")
103+
empty_system += Container(name="Container", description="Description2")
105104

106105

107-
@pytest.mark.xfail(strict=True)
108-
def test_software_system_adding_container_with_existing_parent_fails(empty_system: SoftwareSystem):
106+
def test_software_system_adding_container_with_existing_parent_fails(
107+
empty_system: SoftwareSystem,
108+
):
109109
"""Defensive check that if a container already has a (different) parent then it can't be added."""
110-
system2 = empty_system.model.add_software_system(name="System 2", description="Description")
111-
container = empty_system.add_container(name="Container")
110+
system2 = empty_system.model.add_software_system(
111+
name="System 2", description="Description"
112+
)
113+
container = empty_system.add_container(name="Container", description="Description")
112114
with pytest.raises(ValueError, match="Container with name .* already has parent"):
113115
system2 += container
116+
117+
118+
def test_software_system_get_container_with_name(empty_system: SoftwareSystem):
119+
"""Test getting containers by name."""
120+
container = empty_system.add_container(name="Test", description="Description")
121+
assert empty_system.get_container_with_name("Test") is container
122+
assert empty_system.get_container_with_name("FooBar") is None

0 commit comments

Comments
 (0)