Skip to content

Commit 0791748

Browse files
authored
magicbot: Inject into component __init__ (#204)
1 parent cd5b663 commit 0791748

File tree

3 files changed

+51
-11
lines changed

3 files changed

+51
-11
lines changed

magicbot/inject.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ def get_injection_requests(
2323
for n, inject_type in type_hints.items():
2424
# If the variable is private ignore it
2525
if n.startswith("_"):
26+
if component is None:
27+
message = f"Cannot inject into component {cname} __init__ param {n}"
28+
raise MagicInjectError(message)
2629
continue
2730

2831
# If the variable has been set, skip it
29-
if hasattr(component, n):
32+
if component is not None and hasattr(component, n):
3033
continue
3134

3235
# Check for generic types from the typing module

magicbot/magicrobot.py

+19-9
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,8 @@ def _create_components(self) -> None:
595595
# .. this hack is necessary for pybind11 based modules
596596
sys.modules["pybind11_builtins"] = types.SimpleNamespace() # type: ignore
597597

598+
injectables = self._collect_injectables()
599+
598600
for m, ctyp in typing.get_type_hints(cls).items():
599601
# Ignore private variables
600602
if m.startswith("_"):
@@ -611,24 +613,23 @@ def _create_components(self) -> None:
611613
% (cls.__name__, m, ctyp)
612614
)
613615

614-
component = self._create_component(m, ctyp)
616+
component = self._create_component(m, ctyp, injectables)
615617

616618
# Store for later
617619
components.append((m, component))
618-
619-
self._injectables = self._collect_injectables()
620+
injectables[m] = component
620621

621622
# For each new component, perform magic injection
622623
for cname, component in components:
623624
setup_tunables(component, cname, "components")
624-
self._setup_vars(cname, component)
625+
self._setup_vars(cname, component, injectables)
625626
self._setup_reset_vars(component)
626627

627628
# Do it for autonomous modes too
628629
for mode in self._automodes.modes.values():
629630
mode.logger = logging.getLogger(mode.MODE_NAME)
630631
setup_tunables(mode, mode.MODE_NAME, "autonomous")
631-
self._setup_vars(mode.MODE_NAME, mode)
632+
self._setup_vars(mode.MODE_NAME, mode, injectables)
632633

633634
# And for self too
634635
setup_tunables(self, "robot", None)
@@ -672,9 +673,18 @@ def _collect_injectables(self) -> Dict[str, Any]:
672673

673674
return injectables
674675

675-
def _create_component(self, name: str, ctyp: type):
676+
def _create_component(self, name: str, ctyp: type, injectables: Dict[str, Any]):
677+
type_hints = typing.get_type_hints(ctyp.__init__)
678+
NoneType = type(None)
679+
init_return_type = type_hints.pop("return", NoneType)
680+
assert (
681+
init_return_type is NoneType
682+
), f"{ctyp!r} __init__ had an unexpected non-None return type hint"
683+
requests = get_injection_requests(type_hints, name)
684+
injections = find_injections(requests, injectables, name)
685+
676686
# Create instance, set it on self
677-
component = ctyp()
687+
component = ctyp(**injections)
678688
setattr(self, name, component)
679689

680690
# Ensure that mandatory methods are there
@@ -691,12 +701,12 @@ def _create_component(self, name: str, ctyp: type):
691701

692702
return component
693703

694-
def _setup_vars(self, cname: str, component) -> None:
704+
def _setup_vars(self, cname: str, component, injectables: Dict[str, Any]) -> None:
695705
self.logger.debug("Injecting magic variables into %s", cname)
696706

697707
type_hints = typing.get_type_hints(type(component))
698708
requests = get_injection_requests(type_hints, cname, component)
699-
injections = find_injections(requests, self._injectables, cname)
709+
injections = find_injections(requests, injectables, cname)
700710
component.__dict__.update(injections)
701711

702712
def _setup_reset_vars(self, component) -> None:

tests/test_magicbot_injection.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Type, TypeVar
1+
from typing import List, Tuple, Type, TypeVar
22
from unittest.mock import Mock
33

44
import magicbot
@@ -33,12 +33,30 @@ def execute(self):
3333
pass
3434

3535

36+
class Component3:
37+
intvar: int
38+
39+
def __init__(
40+
self,
41+
tupvar: Tuple[int, int],
42+
injectable: Injectable,
43+
component2: Component2,
44+
) -> None:
45+
self.tuple_ = tupvar
46+
self.injectable_ = injectable
47+
self.component_2 = component2
48+
49+
def execute(self):
50+
pass
51+
52+
3653
class SimpleBot(magicbot.MagicRobot):
3754
intvar = 1
3855
tupvar = 1, 2
3956

4057
component1: Component1
4158
component2: Component2
59+
component3: Component3
4260

4361
def createObjects(self):
4462
self.injectable = Injectable(42)
@@ -158,6 +176,15 @@ def test_simple_annotation_inject():
158176
assert bot.component2.tupvar == (1, 2)
159177
assert bot.component2.component1 is bot.component1
160178

179+
assert bot.component3.intvar == 1
180+
assert bot.component3.tuple_ == (1, 2)
181+
assert isinstance(bot.component3.injectable_, Injectable)
182+
assert bot.component3.injectable_.num == 42
183+
assert bot.component3.component_2 is bot.component2
184+
185+
# Check the method hasn't been mutated
186+
assert str(Component3.__init__.__annotations__["return"]) == "None"
187+
161188

162189
def test_multilevel_annotation_inject():
163190
bot = _make_bot(MultilevelBot)

0 commit comments

Comments
 (0)