From f1bcc49ca83cc5d1d3466900346c53dc393bbd10 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 11 Dec 2025 01:30:50 -0800 Subject: [PATCH 1/4] ENG-8509: computed var dependency tracking for locally imported states Import dep tracking: * all forms of function-local imports should be usable in `get_state` * get deps from get_state through chained attributes --- reflex/vars/dep_tracking.py | 173 ++++++++++++++++++++++---- tests/units/states/mutation.py | 3 + tests/units/vars/test_dep_tracking.py | 166 +++++++++++++++++++++++- 3 files changed, 319 insertions(+), 23 deletions(-) diff --git a/reflex/vars/dep_tracking.py b/reflex/vars/dep_tracking.py index 4b7fb204d6a..cf9203ddeea 100644 --- a/reflex/vars/dep_tracking.py +++ b/reflex/vars/dep_tracking.py @@ -6,9 +6,10 @@ import dataclasses import dis import enum +import importlib import inspect import sys -from types import CellType, CodeType, FunctionType +from types import CellType, CodeType, FunctionType, ModuleType from typing import TYPE_CHECKING, Any, ClassVar, cast from reflex.utils.exceptions import VarValueError @@ -43,9 +44,38 @@ class ScanStatus(enum.Enum): SCANNING = enum.auto() GETTING_ATTR = enum.auto() GETTING_STATE = enum.auto() + GETTING_STATE_POST_AWAIT = enum.auto() GETTING_VAR = enum.auto() +class UntrackedLocalVarError(VarValueError): + """Raised when a local variable is referenced, but it is not tracked in the current scope.""" + + +def assert_base_state( + local_value: Any, + local_name: str | None = None, +) -> type[BaseState]: + """Assert that a local variable is a BaseState subclass. + + Args: + local_value: The value of the local variable to check. + local_name: The name of the local variable to check. + + Returns: + The local variable value if it is a BaseState subclass. + + Raises: + VarValueError: If the object is not a BaseState subclass. + """ + from reflex.state import BaseState + + if not isinstance(local_value, type) or not issubclass(local_value, BaseState): + msg = f"Cannot determine dependencies in fetched state {local_name!r}: {local_value!r} is not a BaseState." + raise VarValueError(msg) + return local_value + + @dataclasses.dataclass class DependencyTracker: """State machine for identifying state attributes that are accessed by a function.""" @@ -58,10 +88,15 @@ class DependencyTracker: scan_status: ScanStatus = dataclasses.field(default=ScanStatus.SCANNING) top_of_stack: str | None = dataclasses.field(default=None) - tracked_locals: dict[str, type[BaseState]] = dataclasses.field(default_factory=dict) + tracked_locals: dict[str, type[BaseState] | ModuleType] = dataclasses.field( + default_factory=dict + ) - _getting_state_class: type[BaseState] | None = dataclasses.field(default=None) + _getting_state_class: type[BaseState] | ModuleType | None = dataclasses.field( + default=None + ) _get_var_value_positions: dis.Positions | None = dataclasses.field(default=None) + _last_import_name: str | None = dataclasses.field(default=None) INVALID_NAMES: ClassVar[list[str]] = ["parent_state", "substates", "get_substate"] @@ -90,6 +125,26 @@ def _merge_deps(self, tracker: DependencyTracker) -> None: for state_name, dep_name in tracker.dependencies.items(): self.dependencies.setdefault(state_name, set()).update(dep_name) + def get_tracked_local(self, local_name: str) -> type[BaseState] | ModuleType: + """Get the value of a local name tracked in the current function scope. + + Args: + local_name: The name of the local variable to fetch. + + Returns: + The value of local name tracked in the current scope (a referenced + BaseState subclass or imported module). + + Raises: + UntrackedLocalVarError: If the local variable is not being tracked. + """ + try: + local_value = self.tracked_locals[local_name] + except KeyError as ke: + msg = f"{local_name!r} is not tracked in the current scope." + raise UntrackedLocalVarError(msg) from ke + return local_value + def load_attr_or_method(self, instruction: dis.Instruction) -> None: """Handle loading an attribute or method from the object on top of the stack. @@ -100,7 +155,8 @@ def load_attr_or_method(self, instruction: dis.Instruction) -> None: instruction: The dis instruction to process. Raises: - VarValueError: if the attribute is an disallowed name. + VarValueError: if the attribute is an disallowed name or attribute + does not reference a BaseState. """ from .base import ComputedVar @@ -122,7 +178,8 @@ def load_attr_or_method(self, instruction: dis.Instruction) -> None: self.scan_status = ScanStatus.SCANNING if not self.top_of_stack: return - target_state = self.tracked_locals[self.top_of_stack] + target_obj = self.get_tracked_local(self.top_of_stack) + target_state = assert_base_state(target_obj, local_name=self.top_of_stack) try: ref_obj = getattr(target_state, instruction.argval) except AttributeError: @@ -190,15 +247,14 @@ def handle_getting_state(self, instruction: dis.Instruction) -> None: Raises: VarValueError: if the state class cannot be determined from the instruction. """ - from reflex.state import BaseState - - if instruction.opname in ("LOAD_FAST", "LOAD_FAST_BORROW"): - msg = f"Dependency detection cannot identify get_state class from local var {instruction.argval}." - raise VarValueError(msg) if isinstance(self.func, CodeType): msg = "Dependency detection cannot identify get_state class from a code object." raise VarValueError(msg) - if instruction.opname == "LOAD_GLOBAL": + if instruction.opname in ("LOAD_FAST", "LOAD_FAST_BORROW"): + self._getting_state_class = self.get_tracked_local( + local_name=instruction.argval, + ) + elif instruction.opname == "LOAD_GLOBAL": # Special case: referencing state class from global scope. try: self._getting_state_class = self._get_globals()[instruction.argval] @@ -212,16 +268,43 @@ def handle_getting_state(self, instruction: dis.Instruction) -> None: except (ValueError, KeyError) as ve: msg = f"Cached var {self!s} cannot access arbitrary state `{instruction.argval}`, is it defined yet?" raise VarValueError(msg) from ve - elif instruction.opname == "STORE_FAST": + elif instruction.opname in ("LOAD_ATTR", "LOAD_METHOD"): + self._getting_state_class = getattr( + self._getting_state_class, + instruction.argval, + ) + elif instruction.opname == "END_SEND": + # Now outside of the `await` machinery, subsequent instructions + # operate on the result of the `get_state` call. + self.scan_status = ScanStatus.GETTING_STATE_POST_AWAIT + if self._getting_state_class is not None: + self.top_of_stack = "_" + self.tracked_locals[self.top_of_stack] = self._getting_state_class + self._getting_state_class = None + + def handle_getting_state_post_await(self, instruction: dis.Instruction) -> None: + """Handle bytecode analysis after `get_state` was called in the function. + + This function is called _after_ awaiting self.get_state to capture the + local variable holding the state instance or directly record access to + attributes accessed on the result of get_state. + + Args: + instruction: The dis instruction to process. + + Raises: + VarValueError: if the state class cannot be determined from the instruction. + """ + if instruction.opname == "STORE_FAST" and self.top_of_stack: # Storing the result of get_state in a local variable. - if not isinstance(self._getting_state_class, type) or not issubclass( - self._getting_state_class, BaseState - ): - msg = f"Cached var {self!s} cannot determine dependencies in fetched state `{instruction.argval}`." - raise VarValueError(msg) - self.tracked_locals[instruction.argval] = self._getting_state_class + self.tracked_locals[instruction.argval] = self.tracked_locals.pop( + self.top_of_stack + ) + self.top_of_stack = None self.scan_status = ScanStatus.SCANNING - self._getting_state_class = None + elif instruction.opname in ("LOAD_ATTR", "LOAD_METHOD"): + # Attribute access on an inline `get_state`, not assigned to a variable. + self.load_attr_or_method(instruction) def _eval_var(self, positions: dis.Positions) -> Var: """Evaluate instructions from the wrapped function to get the Var object. @@ -262,8 +345,12 @@ def _eval_var(self, positions: dis.Positions) -> Var: ]) else: snipped_source = source[0][start_column:end_column] - # Evaluate the string in the context of the function's globals and closure. - return eval(f"({snipped_source})", self._get_globals(), self._get_closure()) + # Evaluate the string in the context of the function's globals, closure and tracked local scope. + return eval( + f"({snipped_source})", + self._get_globals(), + {**self._get_closure(), **self.tracked_locals}, + ) def handle_getting_var(self, instruction: dis.Instruction) -> None: """Handle bytecode analysis when `get_var_value` was called in the function. @@ -304,6 +391,8 @@ def _populate_dependencies(self) -> None: for instruction in dis.get_instructions(self.func): if self.scan_status == ScanStatus.GETTING_STATE: self.handle_getting_state(instruction) + elif self.scan_status == ScanStatus.GETTING_STATE_POST_AWAIT: + self.handle_getting_state_post_await(instruction) elif self.scan_status == ScanStatus.GETTING_VAR: self.handle_getting_var(instruction) elif ( @@ -314,6 +403,15 @@ def _populate_dependencies(self) -> None: # is referencing an attribute on self self.top_of_stack = instruction.argval self.scan_status = ScanStatus.GETTING_ATTR + elif ( + instruction.opname + in ("LOAD_FAST_LOAD_FAST", "LOAD_FAST_BORROW_LOAD_FAST_BORROW") + and instruction.argval[-1] in self.tracked_locals + ): + # Double LOAD_FAST family instructions load multiple values onto the stack, + # the last value in the argval list is the top of the stack. + self.top_of_stack = instruction.argval[-1] + self.scan_status = ScanStatus.GETTING_ATTR elif self.scan_status == ScanStatus.GETTING_ATTR and instruction.opname in ( "LOAD_ATTR", "LOAD_METHOD", @@ -332,3 +430,36 @@ def _populate_dependencies(self) -> None: tracked_locals=self.tracked_locals, ) ) + elif instruction.opname == "IMPORT_NAME" and instruction.argval is not None: + self._last_import_name = instruction.argval + importlib.import_module(instruction.argval) + top_module_name = instruction.argval.split(".")[0] + self.tracked_locals[instruction.argval] = sys.modules[top_module_name] + self.top_of_stack = instruction.argval + elif instruction.opname == "IMPORT_FROM": + if not self._last_import_name: + msg = f"Cannot find package associated with import {instruction.argval} in {self.func!r}." + raise VarValueError(msg) + if instruction.argval in self._last_import_name.split("."): + # `import ... as ...` case: + # import from interim package, update tracked_locals for the last imported name. + self.tracked_locals[self._last_import_name] = getattr( + self.tracked_locals[self._last_import_name], instruction.argval + ) + continue + # Importing a name from a package/module. + if self._last_import_name is not None and self.top_of_stack: + # The full import name does NOT end up in scope for a `from ... import`. + self.tracked_locals.pop(self._last_import_name) + self.tracked_locals[instruction.argval] = getattr( + importlib.import_module(self._last_import_name), + instruction.argval, + ) + # If we see a STORE_FAST, we can assign the top of stack to an aliased name. + self.top_of_stack = instruction.argval + self._last_import_name = None + elif instruction.opname == "STORE_FAST" and self.top_of_stack is not None: + self.tracked_locals[instruction.argval] = self.tracked_locals.pop( + self.top_of_stack + ) + self.top_of_stack = None diff --git a/tests/units/states/mutation.py b/tests/units/states/mutation.py index eaeacd1d756..4075f192d4d 100644 --- a/tests/units/states/mutation.py +++ b/tests/units/states/mutation.py @@ -46,3 +46,6 @@ def reassign_mutables(self): "mod_third_key": {"key": "value"}, } self.test_set = {1, 2, 3, 4, "five"} + + def _get_array(self) -> list[str | int | list | dict[str, str]]: + return self.array diff --git a/tests/units/vars/test_dep_tracking.py b/tests/units/vars/test_dep_tracking.py index 36896488897..6789f627233 100644 --- a/tests/units/vars/test_dep_tracking.py +++ b/tests/units/vars/test_dep_tracking.py @@ -7,9 +7,14 @@ import pytest import reflex as rx +import tests.units.states.upload as tus_upload from reflex.state import State from reflex.utils.exceptions import VarValueError -from reflex.vars.dep_tracking import DependencyTracker, get_cell_value +from reflex.vars.dep_tracking import ( + DependencyTracker, + UntrackedLocalVarError, + get_cell_value, +) class DependencyTestState(State): @@ -122,6 +127,18 @@ async def func_with_get_state(self: DependencyTestState): assert tracker.dependencies == expected_deps +def test_get_state_functionality_direct(): + """Test tracking dependencies when using get_state without assigning to interim local variable.""" + + async def func_with_get_state_direct(self: DependencyTestState): + return (await self.get_state(AnotherTestState)).value + + tracker = DependencyTracker(func_with_get_state_direct, DependencyTestState) + + expected_deps = {AnotherTestState.get_full_name(): {"value"}} + assert tracker.dependencies == expected_deps + + def test_get_state_with_local_var_error(): """Test that get_state with local variables raises appropriate error.""" @@ -130,11 +147,99 @@ async def invalid_get_state_func(self: DependencyTestState): return (await self.get_state(state_cls)).value with pytest.raises( - VarValueError, match="cannot identify get_state class from local var" + UntrackedLocalVarError, match="'state_cls' is not tracked in the current scope" ): DependencyTracker(invalid_get_state_func, DependencyTestState) +def test_get_state_with_import_from(): + """Test that get_state with function-local `from ... import ...` finds correct dependency.""" + + async def get_state_import_from(self: DependencyTestState): + from tests.units.states.mutation import MutableTestState + + return (await self.get_state(MutableTestState)).hashmap + + from tests.units.states.mutation import MutableTestState + + tracker = DependencyTracker(get_state_import_from, DependencyTestState) + expected_deps = {MutableTestState.get_full_name(): {"hashmap"}} + assert tracker.dependencies == expected_deps + + +def test_get_state_with_import_from_as(): + """Test that get_state with function-local `from ... import ... as ...` finds correct dependency.""" + + async def get_state_import_from_as(self: DependencyTestState): + from tests.units.states.mutation import MutableTestState as mts + + return (await self.get_state(mts)).hashmap + + from tests.units.states.mutation import MutableTestState + + tracker = DependencyTracker(get_state_import_from_as, DependencyTestState) + expected_deps = {MutableTestState.get_full_name(): {"hashmap"}} + assert tracker.dependencies == expected_deps + + +def test_get_state_with_import(): + """Test that get_state with function-local `import ...` finds correct dependency.""" + + async def get_state_import(self: DependencyTestState): + import tests.units.states.mutation + + return ( + await self.get_state(tests.units.states.mutation.MutableTestState) + ).hashmap + + from tests.units.states.mutation import MutableTestState + + tracker = DependencyTracker(get_state_import, DependencyTestState) + expected_deps = {MutableTestState.get_full_name(): {"hashmap"}} + assert tracker.dependencies == expected_deps + + +def test_get_state_with_import_as(): + """Test that get_state with function-local `import ... as ...` finds correct dependency.""" + + async def get_state_import_as(self: DependencyTestState): + import tests.units.states.mutation as mutation + + return (await self.get_state(mutation.MutableTestState)).hashmap + + from tests.units.states.mutation import MutableTestState + + tracker = DependencyTracker(get_state_import_as, DependencyTestState) + expected_deps = {MutableTestState.get_full_name(): {"hashmap"}} + assert tracker.dependencies == expected_deps + + +def test_get_state_with_import_from_method(): + """Test that get_state with function-local `from ... import ...` finds correct dependency through a method call.""" + + async def get_state_import_from(self: DependencyTestState): + from tests.units.states.mutation import MutableTestState + + return (await self.get_state(MutableTestState))._get_array() + + from tests.units.states.mutation import MutableTestState + + tracker = DependencyTracker(get_state_import_from, DependencyTestState) + expected_deps = {MutableTestState.get_full_name(): {"array"}} + assert tracker.dependencies == expected_deps + + +def test_get_state_access_imported_global_module(): + """Test tracking simple attribute access on self.""" + + async def get_state_imported_global(self: DependencyTestState): + return (await self.get_state(tus_upload.SubUploadState)).img + + tracker = DependencyTracker(get_state_imported_global, DependencyTestState) + expected_deps = {tus_upload.SubUploadState.get_full_name(): {"img"}} + assert tracker.dependencies == expected_deps + + @pytest.mark.skipif( sys.version_info < (3, 11), reason="Requires Python 3.11+ for positions" ) @@ -167,6 +272,21 @@ async def func_with_get_var_value(self: DependencyTestState): assert tracker.dependencies == expected_deps +def test_get_var_value_with_import_from(): + """Test that get_var_value with function-local `from ... import ...` finds correct dependency.""" + + async def get_state_import_from(self: DependencyTestState): + from tests.units.states.mutation import MutableTestState + + return await self.get_var_value(MutableTestState.hashmap) # pyright: ignore[reportArgumentType] + + from tests.units.states.mutation import MutableTestState + + tracker = DependencyTracker(get_state_import_from, DependencyTestState) + expected_deps = {MutableTestState.get_full_name(): {"hashmap"}} + assert tracker.dependencies == expected_deps + + def test_merge_deps(): """Test merging dependencies from multiple trackers.""" @@ -282,6 +402,48 @@ def complex_func(self: DependencyTestState): assert tracker.dependencies == expected_deps +def test_equality_expression_dependencies(): + """Test tracking dependencies in equality expressions. + + With the state attribute on the right hand side, python generates + LOAD_FAST_LOAD_FAST family instructions. + """ + + def equality_func(self: DependencyTestState): + my_val = 2 + return my_val == self.count + + tracker = DependencyTracker(equality_func, DependencyTestState) + expected_deps = {DependencyTestState.get_full_name(): {"count"}} + assert tracker.dependencies == expected_deps + + +def test_equality_expression_dependencies_lhs(): + """Test tracking dependencies in equality expressions (state on left hand side).""" + + def equality_func(self: DependencyTestState): + my_val = 2 + return self.count == my_val + + tracker = DependencyTracker(equality_func, DependencyTestState) + expected_deps = {DependencyTestState.get_full_name(): {"count"}} + assert tracker.dependencies == expected_deps + + +def test_equality_expression_dependencies_get_state(): + """Test tracking dependencies in equality expressions with retrieved state.""" + + async def equality_func_get_state(self: DependencyTestState): + another_state = await self.get_state(AnotherTestState) + my_val = 2 + return my_val == another_state.value + + tracker = DependencyTracker(equality_func_get_state, DependencyTestState) + + expected_deps = {AnotherTestState.get_full_name(): {"value"}} + assert tracker.dependencies == expected_deps + + def test_get_cell_value_with_valid_cell(): """Test get_cell_value with a valid cell containing a value.""" # Create a closure to get a cell object From ab4a2ef689967295a08a3cc94079f331ded73be9 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 11 Dec 2025 02:38:24 -0800 Subject: [PATCH 2/4] py3.11 compatibility --- reflex/vars/dep_tracking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reflex/vars/dep_tracking.py b/reflex/vars/dep_tracking.py index cf9203ddeea..44ff067e077 100644 --- a/reflex/vars/dep_tracking.py +++ b/reflex/vars/dep_tracking.py @@ -273,8 +273,8 @@ def handle_getting_state(self, instruction: dis.Instruction) -> None: self._getting_state_class, instruction.argval, ) - elif instruction.opname == "END_SEND": - # Now outside of the `await` machinery, subsequent instructions + elif instruction.opname == "GET_AWAITABLE": + # Now inside the `await` machinery, subsequent instructions # operate on the result of the `get_state` call. self.scan_status = ScanStatus.GETTING_STATE_POST_AWAIT if self._getting_state_class is not None: From cfc8ee8c377f16f63c08bae4bbf17897412d23ba Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 11 Dec 2025 04:32:18 -0800 Subject: [PATCH 3/4] skip get_var_value test on py3.10 --- tests/units/vars/test_dep_tracking.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/units/vars/test_dep_tracking.py b/tests/units/vars/test_dep_tracking.py index 6789f627233..14ea3016485 100644 --- a/tests/units/vars/test_dep_tracking.py +++ b/tests/units/vars/test_dep_tracking.py @@ -272,6 +272,9 @@ async def func_with_get_var_value(self: DependencyTestState): assert tracker.dependencies == expected_deps +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Requires Python 3.11+ for positions" +) def test_get_var_value_with_import_from(): """Test that get_var_value with function-local `from ... import ...` finds correct dependency.""" From 24b13192b97cfda58fc9e6136b79f097d5f500a7 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 11 Dec 2025 05:02:08 -0800 Subject: [PATCH 4/4] Fix more edge cases with multiple imports and nested list comprehensions --- reflex/vars/dep_tracking.py | 16 +++++++++++--- tests/units/vars/test_dep_tracking.py | 31 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/reflex/vars/dep_tracking.py b/reflex/vars/dep_tracking.py index 44ff067e077..030f77a104e 100644 --- a/reflex/vars/dep_tracking.py +++ b/reflex/vars/dep_tracking.py @@ -396,7 +396,14 @@ def _populate_dependencies(self) -> None: elif self.scan_status == ScanStatus.GETTING_VAR: self.handle_getting_var(instruction) elif ( - instruction.opname in ("LOAD_FAST", "LOAD_DEREF", "LOAD_FAST_BORROW") + instruction.opname + in ( + "LOAD_FAST", + "LOAD_DEREF", + "LOAD_FAST_BORROW", + "LOAD_FAST_CHECK", + "LOAD_FAST_AND_CLEAR", + ) and instruction.argval in self.tracked_locals ): # bytecode loaded the class instance to the top of stack, next load instruction @@ -405,7 +412,11 @@ def _populate_dependencies(self) -> None: self.scan_status = ScanStatus.GETTING_ATTR elif ( instruction.opname - in ("LOAD_FAST_LOAD_FAST", "LOAD_FAST_BORROW_LOAD_FAST_BORROW") + in ( + "LOAD_FAST_LOAD_FAST", + "LOAD_FAST_BORROW_LOAD_FAST_BORROW", + "STORE_FAST_LOAD_FAST", + ) and instruction.argval[-1] in self.tracked_locals ): # Double LOAD_FAST family instructions load multiple values onto the stack, @@ -457,7 +468,6 @@ def _populate_dependencies(self) -> None: ) # If we see a STORE_FAST, we can assign the top of stack to an aliased name. self.top_of_stack = instruction.argval - self._last_import_name = None elif instruction.opname == "STORE_FAST" and self.top_of_stack is not None: self.tracked_locals[instruction.argval] = self.tracked_locals.pop( self.top_of_stack diff --git a/tests/units/vars/test_dep_tracking.py b/tests/units/vars/test_dep_tracking.py index 14ea3016485..99bbd0ef467 100644 --- a/tests/units/vars/test_dep_tracking.py +++ b/tests/units/vars/test_dep_tracking.py @@ -23,6 +23,7 @@ class DependencyTestState(State): count: rx.Field[int] = rx.field(default=0) name: rx.Field[str] = rx.field(default="test") items: rx.Field[list[str]] = rx.field(default_factory=list) + board: rx.Field[list[list[int]]] = rx.field(default_factory=list) class AnotherTestState(State): @@ -102,6 +103,18 @@ def func_with_comprehension(self: DependencyTestState): assert tracker.dependencies == expected_deps +def test_list_comprehension_dependencies_2(): + """Test tracking dependencies in list comprehensions.""" + + def func_with_comprehension(self: DependencyTestState): + return [[self.board[r][c] for r in range(3)] for c in range(5)] + + tracker = DependencyTracker(func_with_comprehension, DependencyTestState) + + expected_deps = {DependencyTestState.get_full_name(): {"board"}} + assert tracker.dependencies == expected_deps + + def test_invalid_attribute_access(): """Test that accessing invalid attributes raises VarValueError.""" @@ -167,6 +180,24 @@ async def get_state_import_from(self: DependencyTestState): assert tracker.dependencies == expected_deps +def test_get_state_with_import_from_multiple(): + """Test that get_state with function-local `from ... import ...` finds correct dependency.""" + + async def get_state_import_from(self: DependencyTestState): + from tests.units.states.upload import ChildFileUploadState, SubUploadState + + return (await self.get_state(SubUploadState)).img, ( + await self.get_state(ChildFileUploadState) + ).img_list + + tracker = DependencyTracker(get_state_import_from, DependencyTestState) + expected_deps = { + tus_upload.SubUploadState.get_full_name(): {"img"}, + tus_upload.ChildFileUploadState.get_full_name(): {"img_list"}, + } + assert tracker.dependencies == expected_deps + + def test_get_state_with_import_from_as(): """Test that get_state with function-local `from ... import ... as ...` finds correct dependency."""