Skip to content

Commit 6d3c3aa

Browse files
yt-msMidnighter
authored andcommitted
refactor: make add_* methods constistently call +=
1 parent fcb8e1f commit 6d3c3aa

File tree

6 files changed

+85
-101
lines changed

6 files changed

+85
-101
lines changed

src/structurizr/model/container.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def hydrate(
120120
parent=software_system,
121121
technology=container_io.technology,
122122
)
123-
model.add_container(container)
123+
model += container
124124

125125
for component_io in container_io.components:
126126
component = Component.hydrate(component_io, container=container)
@@ -139,7 +139,6 @@ def __iadd__(self, component: Component) -> "Container":
139139
# TODO: once we move past python 3.6 change to proper return type via
140140
# __future__.annotations
141141
if component in self.components:
142-
# Nothing to do
143142
return self
144143

145144
if self.get_component_with_name(component.name):
@@ -155,7 +154,8 @@ def __iadd__(self, component: Component) -> "Container":
155154
f"{component.parent}. Cannot add to {self}."
156155
)
157156
self._components.add(component)
158-
self.get_model().add_component(component)
157+
model = self.get_model()
158+
model += component
159159
return self
160160

161161
def get_component_with_name(self, name: str) -> Component:

src/structurizr/model/model.py

Lines changed: 42 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@
1717

1818

1919
import logging
20-
from typing import Iterable, List, Optional, Union, ValuesView
20+
from typing import Iterable, List, Optional, Set, ValuesView
2121

2222
from pydantic import Field
2323

24-
from structurizr.model.deployment_node import DeploymentNode, DeploymentNodeIO
24+
from structurizr.model.deployment_node import DeploymentNode
2525

2626
from ..abstract_base import AbstractBase
2727
from ..base_model import BaseModel
28-
from .component import Component
2928
from .container import Container
3029
from .container_instance import ContainerInstance
3130
from .element import Element
@@ -106,20 +105,29 @@ def __init__(
106105
"""
107106
super().__init__(**kwargs)
108107
self.enterprise = enterprise
109-
self.people = set(people)
110-
self.software_systems = set(software_systems)
111108
self.deployment_nodes = set(deployment_nodes)
112109
# TODO: simply iterate attributes
113110
self._elements_by_id = {}
114111
self._relationships_by_id = {}
115112
self._id_generator = SequentialIntegerIDGenerator()
116113

117114
def __contains__(self, element: Element):
115+
"""Return True if the element is in the model."""
118116
return element in self.get_elements()
119117

118+
@property
119+
def software_systems(self) -> Set[SoftwareSystem]:
120+
"""Return the software systems in the model."""
121+
return set([e for e in self.get_elements() if isinstance(e, SoftwareSystem)])
122+
123+
@property
124+
def people(self) -> Set[Person]:
125+
"""Return the people in the model."""
126+
return set([e for e in self.get_elements() if isinstance(e, Person)])
127+
120128
@classmethod
121129
def hydrate(cls, model_io: ModelIO) -> "Model":
122-
""""""
130+
"""Return a new model, hydrated from its IO."""
123131
model = cls(
124132
enterprise=Enterprise.hydrate(model_io.enterprise)
125133
if model_io.enterprise is not None
@@ -128,12 +136,10 @@ def hydrate(cls, model_io: ModelIO) -> "Model":
128136
)
129137

130138
for person_io in model_io.people:
131-
person = Person.hydrate(person_io)
132-
model.add_person(person=person)
139+
model += Person.hydrate(person_io)
133140

134141
for software_system_io in model_io.software_systems:
135-
software_system = SoftwareSystem.hydrate(software_system_io, model=model)
136-
model.add_software_system(software_system=software_system)
142+
model += SoftwareSystem.hydrate(software_system_io, model=model)
137143

138144
# for deployment_node_io in model_io.deployment_nodes:
139145
# deployment_node = DeploymentNode.hydrate(deployment_node_io)
@@ -154,12 +160,10 @@ def add_person(self, person=None, **kwargs) -> Person:
154160
Add a new person to the model.
155161
156162
Args:
157-
person (Person, optional): Either provide a `Person` instance or
158-
**kwargs: Provide keyword arguments for instantiating a `Person`
159-
(recommended).
163+
**kwargs: Provide keyword arguments for instantiating a `Person`.
160164
161165
Returns:
162-
Person: Either the same or a new instance, depending on arguments.
166+
Person: New instance.
163167
164168
Raises:
165169
ValueError: When a person with the same name already exists.
@@ -168,28 +172,20 @@ def add_person(self, person=None, **kwargs) -> Person:
168172
Person
169173
170174
"""
171-
if person is None:
172-
person = Person(**kwargs)
173-
if any(person.name == p.name for p in self.people):
174-
ValueError(
175-
f"A person with the name '{person.name}' already exists in the model."
176-
)
177-
self._add_element(person)
178-
self.people.add(person)
175+
person = Person(**kwargs)
176+
self += person
179177
return person
180178

181-
def add_software_system(self, software_system=None, **kwargs) -> SoftwareSystem:
179+
def add_software_system(self, **kwargs) -> SoftwareSystem:
182180
"""
183181
Add a new software system to the model.
184182
185183
Args:
186-
software_system (SoftwareSystem, optional): Either provide a
187-
`SoftwareSystem` instance or
188184
**kwargs: Provide keyword arguments for instantiating a `SoftwareSystem`
189185
(recommended).
190186
191187
Returns:
192-
SoftwareSystem: Either the same or a new instance, depending on arguments.
188+
SoftwareSystem: New instance.
193189
194190
Raises:
195191
ValueError: When a software system with the same name already exists.
@@ -198,55 +194,32 @@ def add_software_system(self, software_system=None, **kwargs) -> SoftwareSystem:
198194
SoftwareSystem
199195
200196
"""
201-
if software_system is None:
202-
software_system = SoftwareSystem(**kwargs)
203-
if any(software_system.name == s.name for s in self.software_systems):
204-
ValueError(
205-
f"A software system with the name {software_system.name} already "
206-
f"exists in the model."
207-
)
208-
self._add_element(software_system)
209-
self.software_systems.add(software_system)
197+
software_system = SoftwareSystem(**kwargs)
198+
self += software_system
210199
return software_system
211200

212-
def __iadd__(self, element: Union[Person, SoftwareSystem]) -> "Model":
213-
"""Add a new Person or SoftwareSystem to the model."""
201+
def __iadd__(self, element: Element) -> "Model":
202+
"""Add a newly constructed element to the model."""
214203
if isinstance(element, Person):
215-
self.add_person(person=element)
204+
if any(element.name == p.name for p in self.people):
205+
raise ValueError(
206+
f"A person with the name '{element.name}' already exists in the "
207+
f"model."
208+
)
216209
elif isinstance(element, SoftwareSystem):
217-
self.add_software_system(software_system=element)
218-
else:
210+
if any(element.name == s.name for s in self.software_systems):
211+
raise ValueError(
212+
f"A software system with the name '{element.name}' already "
213+
f"exists in the model."
214+
)
215+
elif element.parent is None:
219216
raise ValueError(
220-
f"Cannot add element with the name {element.name} to Model with += as it is not a Person or a SoftwareSystem."
217+
f"Element with name {element.name} has no parent. Please ensure "
218+
f"you have added it to the parent element."
221219
)
220+
self._add_element(element)
222221
return self
223222

224-
def add_container(self, container: Container) -> Container:
225-
"""
226-
Register a newly constructed container with the model.
227-
228-
Args:
229-
container (Container): `Container` instance to register.
230-
231-
Returns:
232-
Container: The provided container.
233-
234-
Raises:
235-
ValueError: When the container isn't a child of a `SoftwareSystem`
236-
237-
See Also:
238-
Container
239-
240-
"""
241-
if container.parent is None:
242-
raise ValueError(
243-
f"Container with name {container.name} has no parent software system."
244-
f"Adding to software system will register with the system's model."
245-
)
246-
247-
self._add_element(container)
248-
return container
249-
250223
def add_container_instance(
251224
self,
252225
deployment_node: DeploymentNode,
@@ -275,20 +248,6 @@ def add_container_instance(
275248
# TODO: implement
276249
# instance_number =
277250

278-
def add_component(
279-
self,
280-
component: Component,
281-
) -> Component:
282-
"""Register a newly constructed Component with the model."""
283-
if component.parent is None:
284-
raise ValueError(
285-
f"Component with name {component.name} has no parent container. "
286-
f"Adding to container will register with the container's model."
287-
)
288-
289-
self._add_element(component)
290-
return component
291-
292251
def add_deployment_node(
293252
self, deployment_node: Optional[DeploymentNode] = None, **kwargs
294253
) -> DeploymentNode:
@@ -383,16 +342,17 @@ def get_relationships(self) -> ValuesView[Relationship]:
383342
return self._relationships_by_id.values()
384343

385344
def get_elements(self) -> ValuesView[Element]:
345+
"""Return an iterator over all elements contained in this model."""
386346
return self._elements_by_id.values()
387347

388348
def get_software_system_with_id(self, id: str) -> Optional[SoftwareSystem]:
349+
"""Return the software system with a given ID."""
389350
result = self.get_element(id)
390351
if not isinstance(result, SoftwareSystem):
391352
return None
392353
return result
393354

394355
def _add_element(self, element: Element) -> None:
395-
""""""
396356
if not element.id:
397357
element.id = self._id_generator.generate_id()
398358
elif (
@@ -405,7 +365,6 @@ def _add_element(self, element: Element) -> None:
405365
self._id_generator.found(element.id)
406366

407367
def _add_relationship(self, relationship: Relationship) -> bool:
408-
""""""
409368
if relationship in self.get_relationships():
410369
return True
411370
if not relationship.id:

src/structurizr/model/person.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
"""Provide a person model."""
1717

1818

19+
from typing import Optional
20+
1921
from pydantic import Field
2022

2123
from .location import Location
24+
from .relationship import Relationship
2225
from .static_structure_element import StaticStructureElement, StaticStructureElementIO
2326
from .tags import Tags
2427

@@ -50,19 +53,22 @@ class Person(StaticStructureElement):
5053
"""
5154

5255
def __init__(self, *, location: Location = Location.Unspecified, **kwargs) -> None:
53-
""""""
56+
"""Initialise a Person."""
5457
super().__init__(**kwargs)
5558
self.location = location
5659

5760
self.tags.add(Tags.PERSON)
5861

5962
@classmethod
6063
def hydrate(cls, person_io: PersonIO) -> "Person":
61-
""""""
64+
"""Create a new person and hydrate from its IO."""
6265
return cls(
6366
**cls.hydrate_arguments(person_io),
6467
location=person_io.location,
6568
)
6669

67-
def interacts_with(self, destination: "Person", description: str, **kwargs):
70+
def interacts_with(
71+
self, destination: "Person", description: str, **kwargs
72+
) -> Optional[Relationship]:
73+
"""Create a relationship with the given other Person."""
6874
return self.uses(destination=destination, description=description, **kwargs)

tests/integration/test_model_elements.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_add_person_from_args(attributes: dict, model: Model):
4848
def test_add_person(attributes: dict, model: Model):
4949
"""Expect that a person can be added to the model."""
5050
person = Person(**attributes)
51-
model.add_person(person)
51+
model += person
5252
assert person.id == "1"
5353
assert len(model.people) == 1
5454
for attr, expected in attributes.items():
@@ -75,7 +75,7 @@ def test_add_software_system_from_args(attributes: dict, model: Model):
7575
def test_add_software_system(attributes: dict, model: Model):
7676
"""Expect that a software system can be added to the model."""
7777
software_system = SoftwareSystem(**attributes)
78-
model.add_software_system(software_system)
78+
model += software_system
7979
assert software_system.id == "1"
8080
assert len(model.software_systems) == 1
8181
for attr, expected in attributes.items():

tests/unit/model/test_container.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
class MockModel:
2323
"""Implement a mock model for testing."""
2424

25-
def add_component(self, component):
25+
def __iadd__(self, component):
2626
"""Simulate the model assigning IDs to new elements."""
2727
if not component.id:
2828
component.id = "id"
2929
component.set_model(self)
30-
pass
30+
return self
3131

3232

3333
_model = MockModel()

tests/unit/model/test_model.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,15 @@ def test_model_cannot_add_relationship_with_same_id_as_element(empty_model: Mode
6767
def test_model_add_component_must_have_parent(empty_model: Model):
6868
"""Ensure that Model rejects adding Components that aren't within a Container."""
6969
component = Component(name="c1")
70-
with pytest.raises(ValueError, match="Component with name .* has no parent"):
71-
empty_model.add_component(component)
70+
with pytest.raises(ValueError, match="Element with name .* has no parent"):
71+
empty_model += component
7272

7373

7474
def test_model_add_container_must_have_parent(empty_model: Model):
7575
"""Ensure Model rejects adding Containers that aren't within a SoftwareSystem."""
7676
container = Container(name="c1")
77-
with pytest.raises(ValueError, match="Container with name .* has no parent"):
78-
empty_model.add_container(container)
77+
with pytest.raises(ValueError, match="Element with name .* has no parent"):
78+
empty_model += container
7979

8080

8181
def test_model_add_person_with_plusequals(empty_model: Model):
@@ -94,12 +94,31 @@ def test_model_add_software_system_with_plusequals(empty_model: Model):
9494
assert sys.id != ""
9595

9696

97-
def test_model_can_only_add_person_or_software_system_with_plusequals(
97+
def test_model_can_add_elements_with_plusequals(
9898
empty_model: Model,
9999
):
100-
"""Ensure passing something other than a Person or SoftwareSystem in to += fails."""
100+
"""Ensure passing something other than a Person or SoftwareSystem to += works."""
101+
sys = SoftwareSystem(name="Sys")
101102
c = Container(name="C")
103+
c.parent = sys
104+
empty_model += c
105+
assert c in empty_model.get_elements()
106+
107+
108+
def test_model_cannot_add_two_people_with_same_name(empty_model: Model):
109+
"""Ensure duplicate people are not allowed."""
110+
empty_model.add_person(name="Bob")
111+
with pytest.raises(
112+
ValueError, match="A person with the name 'Bob' already exists in the model."
113+
):
114+
empty_model.add_person(name="Bob")
115+
116+
117+
def test_model_cannot_add_two_software_systems_with_same_name(empty_model: Model):
118+
"""Ensure duplicate software systems are not allowed."""
119+
empty_model.add_software_system(name="Bob")
102120
with pytest.raises(
103-
ValueError, match="Cannot add element with the name .* to Model"
121+
ValueError,
122+
match="A software system with the name 'Bob' already exists in the model.",
104123
):
105-
empty_model += c
124+
empty_model.add_software_system(name="Bob")

0 commit comments

Comments
 (0)