Skip to content

Commit b9f410a

Browse files
authored
Merge pull request #3360 from Wurschdhaud/fix/execute-in-revit-context-stale-modules
fix: restore module globals in execute_in_revit_context before ExternalEvent fires
2 parents 25b608c + f57e480 commit b9f410a

1 file changed

Lines changed: 48 additions & 11 deletions

File tree

pyrevitlib/pyrevit/revit/events.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Revit events handler management."""
22
#pylint: disable=unused-argument
3+
from collections import deque
34
from pyrevit import HOST_APP
45
from pyrevit import EXEC_PARAMS, DB, UI
56
from pyrevit import framework
@@ -143,20 +144,23 @@ def stop_events():
143144

144145
class _GenericExternalEventHandler(UI.IExternalEventHandler):
145146
def __init__(self):
146-
self.func = None
147-
self.args = ()
148-
self.kwargs = {}
147+
self._queue = deque()
149148

150149
def Execute(self, uiapp):
151-
try:
152-
if self.func:
153-
self.func(*self.args, **self.kwargs)
154-
except Exception as ex:
155-
mlogger.error("ExternalEvent error: {}".format(ex))
150+
while self._queue:
151+
fn = self._queue.popleft()
152+
try:
153+
fn()
154+
except Exception as ex:
155+
mlogger.error("ExternalEvent error: {}".format(ex))
156156

157157
def GetName(self):
158158
return "GenericExternalEventHandler"
159159

160+
def schedule(self, func):
161+
self._queue.append(func)
162+
163+
160164
if compat.IRONPY:
161165
_HANDLER = _GenericExternalEventHandler()
162166
_EXTERNAL_EVENT = UI.ExternalEvent.Create(_HANDLER)
@@ -199,7 +203,40 @@ def on_button_click(sender, args):
199203
"""
200204
if not compat.IRONPY:
201205
PyRevitCPythonNotSupported("pyrevit.revit.events.execute_in_revit_context")
202-
_HANDLER.func = func
203-
_HANDLER.args = args
204-
_HANDLER.kwargs = kwargs
206+
207+
# Snapshot module-level imports from the function's globals at scheduling
208+
# time. When the ExternalEvent fires asynchronously, the IronPython engine
209+
# may have cleared the script scope (non-persistent engines) or module
210+
# references may be stale after extension changes / session reload.
211+
# This affects any module whose __init__.py uses conditional imports at
212+
# load time (e.g. forms, revit submodules). Only module-type objects are
213+
# restored to avoid contaminating mutable script state (doc, uidoc, etc.).
214+
_module_type = type(compat)
215+
_saved_modules = {
216+
k: v for k, v in func.__globals__.items()
217+
if type(v) is _module_type
218+
}
219+
220+
def _wrapper():
221+
# types.FunctionType with closures is not supported in IronPython 2,
222+
# so we do a bounded mutation: save, patch, call, restore.
223+
_MISSING = object()
224+
g = func.__globals__
225+
saved = {
226+
k: g.get(k, _MISSING)
227+
for k in _saved_modules
228+
if k not in g or g[k] is None
229+
}
230+
for k in saved:
231+
g[k] = _saved_modules[k]
232+
try:
233+
func(*args, **kwargs)
234+
finally:
235+
for k, old in saved.items():
236+
if old is _MISSING:
237+
g.pop(k, None)
238+
else:
239+
g[k] = old
240+
241+
_HANDLER.schedule(_wrapper)
205242
_EXTERNAL_EVENT.Raise()

0 commit comments

Comments
 (0)