|
10 | 10 | import wireup |
11 | 11 | from wireup._annotations import Inject, abstract, injectable |
12 | 12 | from wireup.errors import WireupError |
| 13 | +from wireup.ioc import registry_validation |
| 14 | +from wireup.ioc.types import get_container_object_id |
13 | 15 |
|
14 | 16 | from test.unit.services.no_annotations.random.random_service import RandomService |
15 | 17 |
|
@@ -208,6 +210,131 @@ def make_foo_no_dependency() -> Foo: |
208 | 210 | wireup.create_sync_container(injectables=[make_foo, make_bar, make_foo_no_dependency]) |
209 | 211 |
|
210 | 212 |
|
| 213 | +def test_validates_container_walks_shared_dependencies_once(monkeypatch: pytest.MonkeyPatch) -> None: |
| 214 | + @wireup.injectable |
| 215 | + class Leaf: ... |
| 216 | + |
| 217 | + @wireup.injectable |
| 218 | + @dataclass |
| 219 | + class Shared: |
| 220 | + leaf: Leaf |
| 221 | + |
| 222 | + @wireup.injectable |
| 223 | + @dataclass |
| 224 | + class Foo: |
| 225 | + shared: Shared |
| 226 | + |
| 227 | + @wireup.injectable |
| 228 | + @dataclass |
| 229 | + class Bar: |
| 230 | + shared: Shared |
| 231 | + |
| 232 | + @wireup.injectable |
| 233 | + @dataclass |
| 234 | + class Baz: |
| 235 | + foo: Foo |
| 236 | + bar: Bar |
| 237 | + shared: Shared |
| 238 | + |
| 239 | + reached: list[type] = [] |
| 240 | + descended: list[type] = [] |
| 241 | + walk = registry_validation.assert_valid_resolution_path |
| 242 | + |
| 243 | + def recording_walk(**kwargs) -> None: |
| 244 | + dependency = kwargs["dependency"] |
| 245 | + reached.append(dependency.klass) |
| 246 | + object_id = get_container_object_id(dependency.klass, dependency.qualifier_value) |
| 247 | + # Dependencies known to be cycle-free return immediately, only record the rest. |
| 248 | + if object_id not in kwargs["known_cycle_free_objects"]: |
| 249 | + descended.append(dependency.klass) |
| 250 | + walk(**kwargs) |
| 251 | + |
| 252 | + monkeypatch.setattr(registry_validation, "assert_valid_resolution_path", recording_walk) |
| 253 | + wireup.create_sync_container(injectables=[Leaf, Shared, Foo, Bar, Baz]) |
| 254 | + |
| 255 | + assert reached.count(Shared) > 1 |
| 256 | + # Every dependency in the graph is walked exactly once no matter how many paths reach it. |
| 257 | + assert descended.count(Shared) == 1 |
| 258 | + assert descended.count(Leaf) == 1 |
| 259 | + assert descended.count(Foo) == 1 |
| 260 | + assert descended.count(Bar) == 1 |
| 261 | + |
| 262 | + |
| 263 | +def test_validates_container_raises_when_cycle_is_behind_a_walked_dependency() -> None: |
| 264 | + class Shared: ... |
| 265 | + |
| 266 | + class Foo: |
| 267 | + def __init__(self, shared, bar): ... |
| 268 | + |
| 269 | + class Bar: |
| 270 | + def __init__(self, shared, foo): ... |
| 271 | + |
| 272 | + @wireup.injectable |
| 273 | + def make_shared() -> Shared: |
| 274 | + return Shared() |
| 275 | + |
| 276 | + # Walking 'shared' clears it, the cycle behind 'bar' must still be found. |
| 277 | + @wireup.injectable |
| 278 | + def make_foo(shared: Shared, bar: Bar) -> Foo: |
| 279 | + return Foo(shared, bar) |
| 280 | + |
| 281 | + @wireup.injectable |
| 282 | + def make_bar(shared: Shared, foo: Foo) -> Bar: |
| 283 | + return Bar(shared, foo) |
| 284 | + |
| 285 | + with pytest.raises( |
| 286 | + WireupError, |
| 287 | + match=re.escape( |
| 288 | + f"Circular dependency detected for {Bar!r} (created via {make_bar.__module__}.{make_bar.__name__})" |
| 289 | + f"\n -> {Foo!r} (created via {make_foo.__module__}.{make_foo.__name__})" |
| 290 | + f"\n -> {Bar!r} (created via {make_bar.__module__}.{make_bar.__name__})" |
| 291 | + " ! Cycle here" |
| 292 | + ), |
| 293 | + ): |
| 294 | + wireup.create_sync_container(injectables=[make_shared, make_foo, make_bar]) |
| 295 | + |
| 296 | + |
| 297 | +def test_validates_container_does_not_reuse_walked_dependencies_between_containers() -> None: |
| 298 | + class Foo: |
| 299 | + def __init__(self, bar): ... |
| 300 | + |
| 301 | + class Bar: |
| 302 | + def __init__(self, foo): ... |
| 303 | + |
| 304 | + class Baz: |
| 305 | + def __init__(self, foo): ... |
| 306 | + |
| 307 | + @wireup.injectable |
| 308 | + def make_foo() -> Foo: |
| 309 | + return Foo(None) |
| 310 | + |
| 311 | + @wireup.injectable |
| 312 | + def make_baz(foo: Foo) -> Baz: |
| 313 | + return Baz(foo) |
| 314 | + |
| 315 | + # Foo is walked and recorded cycle-free here, which says nothing about another container's graph. |
| 316 | + wireup.create_sync_container(injectables=[make_foo, make_baz]) |
| 317 | + |
| 318 | + @wireup.injectable |
| 319 | + def make_cyclical_foo(bar: Bar) -> Foo: |
| 320 | + return Foo(bar) |
| 321 | + |
| 322 | + @wireup.injectable |
| 323 | + def make_bar(foo: Foo) -> Bar: |
| 324 | + return Bar(foo) |
| 325 | + |
| 326 | + with pytest.raises( |
| 327 | + WireupError, |
| 328 | + match=re.escape( |
| 329 | + f"Circular dependency detected for {Bar!r} (created via {make_bar.__module__}.{make_bar.__name__})" |
| 330 | + f"\n -> {Foo!r} (created via {make_cyclical_foo.__module__}.{make_cyclical_foo.__name__})" |
| 331 | + f"\n -> {Bar!r} (created via {make_bar.__module__}.{make_bar.__name__})" |
| 332 | + " ! Cycle here" |
| 333 | + ), |
| 334 | + ): |
| 335 | + wireup.create_sync_container(injectables=[make_cyclical_foo, make_bar]) |
| 336 | + |
| 337 | + |
211 | 338 | def test_lifetimes_match_factories() -> None: |
212 | 339 | class ScopedService: ... |
213 | 340 |
|
|
0 commit comments