|
1 | 1 | """Revit events handler management.""" |
2 | 2 | #pylint: disable=unused-argument |
| 3 | +from collections import deque |
3 | 4 | from pyrevit import HOST_APP |
4 | 5 | from pyrevit import EXEC_PARAMS, DB, UI |
5 | 6 | from pyrevit import framework |
@@ -143,20 +144,23 @@ def stop_events(): |
143 | 144 |
|
144 | 145 | class _GenericExternalEventHandler(UI.IExternalEventHandler): |
145 | 146 | def __init__(self): |
146 | | - self.func = None |
147 | | - self.args = () |
148 | | - self.kwargs = {} |
| 147 | + self._queue = deque() |
149 | 148 |
|
150 | 149 | 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)) |
156 | 156 |
|
157 | 157 | def GetName(self): |
158 | 158 | return "GenericExternalEventHandler" |
159 | 159 |
|
| 160 | + def schedule(self, func): |
| 161 | + self._queue.append(func) |
| 162 | + |
| 163 | + |
160 | 164 | if compat.IRONPY: |
161 | 165 | _HANDLER = _GenericExternalEventHandler() |
162 | 166 | _EXTERNAL_EVENT = UI.ExternalEvent.Create(_HANDLER) |
@@ -199,7 +203,40 @@ def on_button_click(sender, args): |
199 | 203 | """ |
200 | 204 | if not compat.IRONPY: |
201 | 205 | 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) |
205 | 242 | _EXTERNAL_EVENT.Raise() |
0 commit comments