fix: restore module globals in execute_in_revit_context before ExternalEvent fires#3360
Conversation
…alEvent fires When the IronPython engine clears a non-persistent script scope, or when the cached engine for a command is the startup-script engine (same TypeId via SessionUUID:EngineType:Extension), module references like `forms`, `revit`, and any other split-backend module can disappear from func.__globals__ before the ExternalEvent callback executes, causing NameError. Snapshot all module-type globals at scheduling time and restore any that are missing just before the function is called. Only module objects (type(v) is type(compat)) are restored, so mutable Revit state (doc, uidoc, etc.) is never overwritten with stale values. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
PR Summary:
- Fixes
NameError: name 'forms' is not defined(#3285) inexecute_in_revit_contextwhen a non-persistent IronPython engine clears its script scope before an ExternalEvent fires. - Snapshots all module-type globals from
func.__globals__at scheduling time and restores any that are missing just before the function is called. - Wraps the user function in a
_func_with_modulesclosure assigned to the singleton_HANDLER.
Review Summary:
The fix correctly identifies that asynchronous ExternalEvent dispatch can fire after the IronPython engine has cleared the script scope, wiping module references like forms. However, the implementation has a few concerns worth considering:
-
Silent call-drop with the singleton handler —
_HANDLERis module-level singleton; rapid back-to-back calls overwrite.func/.args/.kwargs, silently discarding all but the last scheduled invocation. This pre-existing design limitation becomes more impactful here because each call now creates a unique closure with its own_saved_modulessnapshot that gets thrown away. Adeque-based queue would fix this properly. -
func.__globals__mutation leaks into the module namespace — In IronPython 2,func.__globals__is the module's__dict__. Lines 218–220 mutate it directly, making the restored names visible to all code running in that module, not just this call. A local shadow dict (copy of globals + restored entries) passed to a new function object would isolate the side effects. -
Scheduling-time snapshot can itself be stale — If
formsis cleared by an engine reset and then freshly re-imported between scheduling and execution, the snapshotted old reference gets restored over the fresh import, which is the opposite of the intended effect. The simpler fix in PR #3327 (adding the missing import to the correct module) directly removes the root cause without these runtime patching concerns.
Suggestions
- Replace the singleton
_HANDLERwith acollections.deque-based queue so rapid successiveexecute_in_revit_contextcalls don't silently drop earlier scheduled invocations. Apply - Scope the globals restoration with a
try/finallythat removes injected keys afterward to prevent polluting the shared module__dict__for other callers. Apply
Replace singleton func/args/kwargs slots with a queue.Queue so rapid back-to-back calls no longer silently drop earlier invocations. Scope globals restoration with save/patch/finally-restore so only missing or None module names are touched and the original state is always put back, limiting the mutation window to the call itself. Guard restoration with `k not in g or g[k] is None` so a freshly re-imported module between scheduling and execution is never overwritten by the scheduling-time snapshot. Note: types.FunctionType with non-empty closures is unsupported in IronPython 2, so a shadow-dict approach is not viable; bounded mutation is the safest available option. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sanzoghenzo
left a comment
There was a problem hiding this comment.
I haven't used IronPython for far too much to know if my comments are applicable or not 😅
looks good overall
…nalEventHandler deque.append/popleft is atomic under the GIL, lighter weight, and removes the six.moves dependency. The Execute drain loop is simplified by checking truthiness instead of catching queue.Empty. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the IronPython engine clears a non-persistent script scope, or when the cached engine for a command is the startup-script engine (same TypeId via SessionUUID:EngineType:Extension), module references like
forms,revit, and any other split-backend module can disappear fromfunc.__globals__before the ExternalEvent callback executes, causing NameError.Snapshot all module-type globals at scheduling time and restore any that are missing just before the function is called. Only module objects (type(v) is type(compat)) are restored, so mutable Revit state (doc, uidoc, etc.) is never overwritten with stale values.
Unsure, but seems to be a less of a bandaid fix than #3327. Maybe another AI has an opinion on that?
Checklist
Before submitting your pull request, ensure the following requirements are met:
pipenv run black {source_file_or_directory}Related Issues
If applicable, link the issues resolved by this pull request: