Skip to content

Commit b221eb3

Browse files
committed
Fix regressions in tunables/injections
- ClassVar annotations were treated as injectable dependencies - Toposort setup dependency graph included non-component injectables
1 parent 2d70776 commit b221eb3

4 files changed

Lines changed: 75 additions & 4 deletions

File tree

magicbot/inject.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import inspect
12
import logging
3+
import typing
24
from typing import Any, Optional
35

46
logger = logging.getLogger(__name__)
@@ -28,12 +30,22 @@ def get_injection_requests(
2830
raise MagicInjectError(message)
2931
continue
3032

31-
# If the variable has been set, skip it
32-
if component is not None and hasattr(component, n):
33+
# If the variable has been set (on instance or class), skip it.
34+
# Use getattr_static so descriptors (ex: tunable) do not execute.
35+
if component is not None:
36+
try:
37+
inspect.getattr_static(component, n)
38+
except AttributeError:
39+
pass
40+
else:
41+
continue
42+
43+
# Ignore class variables, they are not injection requests
44+
origin = getattr(inject_type, "__origin__", None)
45+
if origin is typing.ClassVar:
3346
continue
3447

3548
# Check for generic types from the typing module
36-
origin = getattr(inject_type, "__origin__", None)
3749
if origin is not None:
3850
inject_type = origin
3951

magicbot/magicrobot.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,8 @@ def _create_components(self) -> None:
624624

625625
# Sort components so that setup functions can rely on the setup of an
626626
# injected variable being called already
627+
component_by_id = {id(component): cname for cname, component in components}
628+
627629
dag = {}
628630
for cname, component in components:
629631
setup = getattr(component, "setup", None)
@@ -632,7 +634,18 @@ def _create_components(self) -> None:
632634
else:
633635
type_hints = typing.get_type_hints(type(component))
634636
requests = get_injection_requests(type_hints, cname, component)
635-
dag[cname] = list(requests.keys())
637+
638+
deps = []
639+
for n in requests:
640+
injectable = injectables.get(n)
641+
if injectable is None:
642+
injectable = injectables.get(f"{cname}_{n}")
643+
644+
dep_name = component_by_id.get(id(injectable))
645+
if dep_name is not None:
646+
deps.append(dep_name)
647+
648+
dag[cname] = deps
636649

637650
# Create an ordered component list based on possible setup() dependencies
638651
setup_ordered_components = [

tests/test_magicbot_inject.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import typing
2+
from typing import ClassVar
23

34
import pytest
45

56
from magicbot.inject import get_injection_requests
7+
from magicbot.magic_tunable import tunable
68

79

810
def test_ctor_invalid_type_hint_message():
@@ -18,3 +20,27 @@ def __init__(self, foo: 1): ...
1820
get_injection_requests(type_hints, "bar")
1921

2022
assert exc_info.value.args[0] == "Component bar has a non-type annotation foo: 1"
23+
24+
25+
def test_component_classvar_annotation_is_not_treated_as_injection_request():
26+
class Component:
27+
injected: int
28+
state_names: ClassVar[int]
29+
30+
requests = get_injection_requests(
31+
typing.get_type_hints(Component), "component", Component()
32+
)
33+
34+
assert requests == {"injected": int}
35+
36+
37+
def test_component_tunable_annotation_is_not_treated_as_injection_request():
38+
class Component:
39+
injected: int
40+
speed: tunable[float] = tunable(0.0)
41+
42+
requests = get_injection_requests(
43+
typing.get_type_hints(Component), "component", Component()
44+
)
45+
46+
assert requests == {"injected": int}

tests/test_magicbot_injection.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,20 @@ def createObjects(self) -> None:
199199
pass
200200

201201

202+
class ExternalDependencyComponent(DumbComponent):
203+
setting: float
204+
205+
def setup(self):
206+
pass
207+
208+
209+
class ExternalDependencyBot(magicbot.MagicRobot):
210+
component: ExternalDependencyComponent
211+
212+
def createObjects(self) -> None:
213+
self.component_setting = 2.5
214+
215+
202216
R = TypeVar("R", bound=magicbot.MagicRobot)
203217

204218

@@ -282,3 +296,9 @@ def test_toposort_inject():
282296
expected_names = ["a", "b", "c", "d"]
283297
for (name, _), expected in zip(bot._components, expected_names):
284298
assert name == expected
299+
300+
301+
def test_toposort_ignores_non_component_dependencies():
302+
bot = _make_bot(ExternalDependencyBot)
303+
304+
assert bot.component.setting == 2.5

0 commit comments

Comments
 (0)