Skip to content

fix: restore module globals in execute_in_revit_context before ExternalEvent fires#3360

Merged
jmcouffin merged 3 commits into
pyrevitlabs:developfrom
Wurschdhaud:fix/execute-in-revit-context-stale-modules
May 7, 2026
Merged

fix: restore module globals in execute_in_revit_context before ExternalEvent fires#3360
jmcouffin merged 3 commits into
pyrevitlabs:developfrom
Wurschdhaud:fix/execute-in-revit-context-stale-modules

Conversation

@Wurschdhaud
Copy link
Copy Markdown
Contributor

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.

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:

  • Code follows the PEP 8 style guide.
  • Code has been formatted with Black using the command:
    pipenv run black {source_file_or_directory}
  • Changes are tested and verified to work as expected.

Related Issues

If applicable, link the issues resolved by this pull request:

…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>
Copy link
Copy Markdown
Contributor

@devloai devloai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary:

  • Fixes NameError: name 'forms' is not defined (#3285) in execute_in_revit_context when 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_modules closure 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:

  1. Silent call-drop with the singleton handler_HANDLER is 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_modules snapshot that gets thrown away. A deque-based queue would fix this properly.

  2. 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.

  3. Scheduling-time snapshot can itself be stale — If forms is 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 _HANDLER with a collections.deque-based queue so rapid successive execute_in_revit_context calls don't silently drop earlier scheduled invocations. Apply
  • Scope the globals restoration with a try/finally that removes injected keys afterward to prevent polluting the shared module __dict__ for other callers. Apply

Comment thread pyrevitlib/pyrevit/revit/events.py Outdated
Comment thread pyrevitlib/pyrevit/revit/events.py
Comment thread pyrevitlib/pyrevit/revit/events.py Outdated
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>
@jmcouffin jmcouffin requested a review from sanzoghenzo May 5, 2026 18:26
Copy link
Copy Markdown
Contributor

@sanzoghenzo sanzoghenzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't used IronPython for far too much to know if my comments are applicable or not 😅
looks good overall

Comment thread pyrevitlib/pyrevit/revit/events.py Outdated
Comment thread pyrevitlib/pyrevit/revit/events.py Outdated
…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>
@jmcouffin jmcouffin merged commit b9f410a into pyrevitlabs:develop May 7, 2026
@Wurschdhaud Wurschdhaud deleted the fix/execute-in-revit-context-stale-modules branch May 12, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: On the first run of "Measure 3D" an error is thrown

3 participants