Skip to content
Open
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
Empty file.
31 changes: 31 additions & 0 deletions test/unit/services/collection_scan/greeters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Protocol

from wireup import injectable


class Greeter(Protocol):
def hi(self) -> str: ...


@injectable(as_type=Greeter, qualifier="delta")
class Delta:
def hi(self) -> str:
return "delta"


@injectable(as_type=Greeter, qualifier="beta")
class Beta:
def hi(self) -> str:
return "beta"


@injectable(as_type=Greeter)
class Alpha:
def hi(self) -> str:
return "alpha"


@injectable(as_type=Greeter, qualifier="gamma")
class Gamma:
def hi(self) -> str:
return "gamma"
20 changes: 20 additions & 0 deletions test/unit/test_discovery_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import unittest
from collections.abc import Sequence

import wireup

from test.unit.services.collection_scan import greeters


class DiscoveryOrderTest(unittest.TestCase):
def test_module_scan_collection_injection_has_deterministic_order(self):
container = wireup.create_sync_container(injectables=[greeters])
result = [g.hi() for g in container.get(Sequence[greeters.Greeter])]

self.assertEqual(["alpha", "beta", "delta", "gamma"], result)

def test_module_scan_discovery_order_is_stable_across_repeated_scans(self):
first = [g.hi() for g in wireup.create_sync_container(injectables=[greeters]).get(Sequence[greeters.Greeter])]
second = [g.hi() for g in wireup.create_sync_container(injectables=[greeters]).get(Sequence[greeters.Greeter])]

self.assertEqual(first, second)
22 changes: 11 additions & 11 deletions wireup/_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def _is_valid_wireup_target(obj: Any) -> bool:
# "from flask import g" would cause a hasattr call to g outside of app context.
return (isinstance(obj, FunctionType) or inspect.isclass(obj)) and hasattr(obj, "__wireup_registration__")

all_targets = {
m for module in injectable_modules for m in _find_objects_in_module(module, predicate=_is_valid_wireup_target)
}
all_targets: dict[type, None] = {}
for module in injectable_modules:
all_targets.update(dict.fromkeys(_find_objects_in_module(module, predicate=_is_valid_wireup_target)))

for cls in all_targets:
reg = getattr(cls, "__wireup_registration__", None)
Expand All @@ -39,14 +39,14 @@ def _is_valid_wireup_target(obj: Any) -> bool:
return abstract_registrations, injectable_registrations


def _find_objects_in_module(module: ModuleType, predicate: Callable[[Any], bool]) -> set[type]:
classes: set[type[Any]] = set()
def _find_objects_in_module(module: ModuleType, predicate: Callable[[Any], bool]) -> list[type]:
classes: dict[type[Any], None] = {}

def _module_get_objects(m: ModuleType) -> set[type]:
return {obj for _, obj in inspect.getmembers(m) if predicate(obj)}
def _module_get_objects(m: ModuleType) -> list[type]:
return [obj for _, obj in inspect.getmembers(m) if predicate(obj)]

def _find_in_path(path: Path, parent_module_name: str) -> None:
for file in path.iterdir():
for file in sorted(path.iterdir()):
if file.name == "__pycache__":
continue

Expand All @@ -60,12 +60,12 @@ def _find_in_path(path: Path, parent_module_name: str) -> None:
parent_module_name if file.name == "__init__.py" else f"{parent_module_name}.{file.name[:-3]}"
)
sub_module = importlib.import_module(full_module_name)
classes.update(_module_get_objects(sub_module))
classes.update(dict.fromkeys(_module_get_objects(sub_module)))

if f := module.__file__:
if f.endswith("__init__.py"):
_find_in_path(Path(f).parent, module.__name__)
else:
classes.update(_module_get_objects(module))
classes.update(dict.fromkeys(_module_get_objects(module)))

return classes
return list(classes)
Loading