Skip to content

Commit 17cab4e

Browse files
harsh21234iHarsh Thakare
andauthored
Fix Python 3.14 event handler annotation resolution (#6896)
* Fix Python 3.14 event annotation resolution * Avoid eager annotation work for unbound handlers * Preserve cached event handler annotations * Retry unresolved event annotations * Annotate deliberate unresolved type test --------- Co-authored-by: Harsh Thakare <harsh@pybex.io>
1 parent 2e9f51f commit 17cab4e

4 files changed

Lines changed: 83 additions & 13 deletions

File tree

‎news/6890.bugfix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cache event handler annotations before runtime state-class patches can shadow builtin names on Python 3.14.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Resolve event handler annotations before runtime state-class patches can shadow builtin names on Python 3.14.

‎packages/reflex-base/src/reflex_base/event/__init__.py‎

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,7 @@ def resolve_upload_handler_param(handler: "EventHandler") -> tuple[str, Any]:
343343
)
344344
raise UploadTypeError(msg)
345345

346-
func = handler.fn.func if isinstance(handler.fn, partial) else handler.fn
347-
for name, annotation in get_type_hints(func).items():
346+
for name, annotation in handler._get_type_hints().items():
348347
if name == "return" or get_origin(annotation) is not list:
349348
continue
350349
args = get_args(annotation)
@@ -380,8 +379,7 @@ def resolve_upload_chunk_handler_param(handler: "EventHandler") -> tuple[str, ty
380379
msg = f"@rx.event(background=True) is required for upload_files_chunk handler `{handler_name}`."
381380
raise UploadTypeError(msg)
382381

383-
func = handler.fn.func if isinstance(handler.fn, partial) else handler.fn
384-
for name, annotation in get_type_hints(func).items():
382+
for name, annotation in handler._get_type_hints().items():
385383
if name == "return":
386384
continue
387385
if annotation is UploadChunkIterator:
@@ -490,6 +488,39 @@ class EventHandler(EventActionsMixin):
490488

491489
state: "type[BaseState] | None" = dataclasses.field(default=None, repr=False)
492490

491+
_type_hints: dict[str, Any] | None = dataclasses.field(
492+
default=None, repr=False, compare=False
493+
)
494+
495+
def __post_init__(self) -> None:
496+
"""Resolve handler annotations while the state class is stable."""
497+
if self.state is not None:
498+
self._get_type_hints()
499+
500+
def _get_type_hints(self) -> dict[str, Any]:
501+
"""Get and cache the type hints for the handler function.
502+
503+
Caching successful resolution at handler creation avoids deferred
504+
annotation evaluation observing attributes assigned to the owning
505+
state class after the handler was registered.
506+
507+
Returns:
508+
The resolved type hints, or an empty mapping when forward references
509+
cannot be resolved yet.
510+
"""
511+
if self._type_hints is not None:
512+
return self._type_hints
513+
if self.fn is None:
514+
object.__setattr__(self, "_type_hints", {})
515+
return {}
516+
func = self.fn.func if isinstance(self.fn, partial) else self.fn
517+
try:
518+
type_hints = get_type_hints(func)
519+
except NameError:
520+
return {}
521+
object.__setattr__(self, "_type_hints", type_hints)
522+
return type_hints
523+
493524
@property
494525
def state_full_name(self) -> str:
495526
"""Get the full name of the state class this event handler is attached to.
@@ -2035,10 +2066,7 @@ def call_event_handler(
20352066

20362067
event_callback_spec_args = list(parameters)
20372068

2038-
try:
2039-
type_hints_of_provided_callback = get_type_hints(event_callback.handler.fn)
2040-
except NameError:
2041-
type_hints_of_provided_callback = {}
2069+
type_hints_of_provided_callback = event_callback.handler._get_type_hints()
20422070

20432071
argument_names = [str(arg) for arg, value in event_callback.args]
20442072

@@ -2073,10 +2101,7 @@ def call_event_handler(
20732101
if event_spec_return_types:
20742102
event_callback_spec_args = list(parameters)
20752103

2076-
try:
2077-
type_hints_of_provided_callback = get_type_hints(event_callback.fn)
2078-
except NameError:
2079-
type_hints_of_provided_callback = {}
2104+
type_hints_of_provided_callback = event_callback._get_type_hints()
20802105

20812106
_check_event_args_subclass_of_callback(
20822107
event_callback_spec_args[n_self_args:],

‎tests/units/test_event.py‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ def spec(a2: Var[str]) -> list[Var[str]]:
131131
assert (
132132
format.format_event(event_spec) == 'ReflexEvent("fn_with_args", {arg1:first})'
133133
)
134-
135134
assert event_spec2 is not event_spec
136135
assert event_spec2.handler == handler
137136
assert len(event_spec2.args) == 2
@@ -145,6 +144,50 @@ def spec(a2: Var[str]) -> list[Var[str]]:
145144
)
146145

147146

147+
def test_state_event_handler_type_hints_are_stable_after_class_patch():
148+
"""Runtime state-class patches must not change handler annotations."""
149+
150+
class S(BaseState):
151+
@event
152+
def on_event(self, event: dict):
153+
pass
154+
155+
handler = cast(EventHandler, S.on_event)
156+
assert handler._get_type_hints()["event"] is dict
157+
158+
# Python 3.14 evaluates deferred method annotations in the owning class
159+
# namespace, so this assignment would shadow the builtin ``dict``.
160+
type.__setattr__(S, "dict", lambda self: {})
161+
162+
def args_spec(value: Var[dict]) -> list[Var[dict]]:
163+
return [value]
164+
165+
call_event_handler(handler(), args_spec)
166+
assert handler.prevent_default._type_hints is handler._type_hints
167+
168+
169+
def test_state_event_handler_caches_unresolved_type_hints():
170+
"""Unresolved annotations should be retried after their type is defined."""
171+
172+
class S(BaseState):
173+
@event
174+
def on_event(
175+
self,
176+
event: "_LateBoundEventType", # pyright: ignore[reportUndefinedVariable] # noqa: F821
177+
):
178+
pass
179+
180+
handler = cast(EventHandler, S.on_event)
181+
assert handler._type_hints is None
182+
assert handler._get_type_hints() == {}
183+
184+
globals()["_LateBoundEventType"] = dict
185+
try:
186+
assert handler._get_type_hints()["event"] is dict
187+
finally:
188+
del globals()["_LateBoundEventType"]
189+
190+
148191
@pytest.mark.parametrize(
149192
("arg1", "arg2"),
150193
[

0 commit comments

Comments
 (0)