Fix Python console command execution without a __main__ module

Account for sys.modules not to have a __main__ module, various other
minor improvements too.

- Use a context manager to temporarily override the __main__ module.
- Use a context manager to override the stdin when auto-completing.
- Remove `_BPY_MAIN_OWN` constant as the __main__ module is needed.
This commit is contained in:
Campbell Barton
2023-09-15 12:23:40 +10:00
parent 2c28f87230
commit 7516a9c0b6

View File

@@ -7,9 +7,29 @@ import bpy
language_id = "python"
# store our own __main__ module, not 100% needed
# but python expects this in some places
_BPY_MAIN_OWN = True
class _TempModuleOverride:
__slots__ = (
"module_name",
"module",
"module_override",
)
def __init__(self, module_name, module_override):
self.module_name = module_name
self.module = None
self.module_override = module_override
def __enter__(self):
self.module = sys.modules.get(self.module_name)
sys.modules[self.module_name] = self.module_override
def __exit__(self, type, value, traceback):
if self.module is None:
# Account for removal of `module_override` (albeit unlikely).
sys.modules.pop(self.module_name, None)
else:
sys.modules[self.module_name] = self.module
def add_scrollback(text, text_type):
@@ -72,12 +92,9 @@ def get_console(console_id):
stdout = io.StringIO()
stderr = io.StringIO()
else:
if _BPY_MAIN_OWN:
import types
bpy_main_mod = types.ModuleType("__main__")
namespace = bpy_main_mod.__dict__
else:
namespace = {}
import types
bpy_main_mod = types.ModuleType("__main__")
namespace = bpy_main_mod.__dict__
namespace["__builtins__"] = sys.modules["builtins"]
namespace["bpy"] = bpy
@@ -94,8 +111,7 @@ def get_console(console_id):
console.push("from mathutils import *")
console.push("from math import *")
if _BPY_MAIN_OWN:
console._bpy_main_mod = bpy_main_mod
console._bpy_main_mod = bpy_main_mod
import io
stdout = io.StringIO()
@@ -121,25 +137,23 @@ def execute(context, is_interactive):
console, stdout, stderr = get_console(hash(context.region))
if _BPY_MAIN_OWN:
main_mod_back = sys.modules["__main__"]
sys.modules["__main__"] = console._bpy_main_mod
# redirect output
from contextlib import (
redirect_stdout,
redirect_stderr,
)
# not included with Python
# Not included with Python.
class redirect_stdin(redirect_stdout.__base__):
_stream = "stdin"
# don't allow the stdin to be used, can lock blender.
with redirect_stdout(stdout), \
redirect_stderr(stderr), \
redirect_stdin(None):
with (
redirect_stdout(stdout),
redirect_stderr(stderr),
# Don't allow the `stdin` to be used because it can lock Blender.
redirect_stdin(None),
_TempModuleOverride("__main__", console._bpy_main_mod),
):
# in case exception happens
line = "" # in case of encoding error
is_multiline = False
@@ -156,9 +170,6 @@ def execute(context, is_interactive):
import traceback
stderr.write(traceback.format_exc())
if _BPY_MAIN_OWN:
sys.modules["__main__"] = main_mod_back
output = stdout.getvalue()
output_err = stderr.getvalue()
@@ -220,46 +231,48 @@ def autocomplete(context):
if not console:
return {'CANCELLED'}
# don't allow the stdin to be used, can lock blender.
# note: unlikely stdin would be used for autocomplete. but its possible.
stdin_backup = sys.stdin
sys.stdin = None
scrollback = ""
scrollback_error = ""
if _BPY_MAIN_OWN:
main_mod_back = sys.modules["__main__"]
sys.modules["__main__"] = console._bpy_main_mod
# Don't allow the `stdin` to be used, can lock blender.
# note: unlikely `stdin` would be used for auto-complete - but it's possible.
try:
current_line = sc.history[-1]
line = current_line.body
from contextlib import redirect_stdout
# This function isn't aware of the text editor or being an operator
# just does the autocomplete then copy its results back
result = intellisense.expand(
line=line,
cursor=current_line.current_character,
namespace=console.locals,
private=bpy.app.debug_python)
# Not included with Python.
class redirect_stdin(redirect_stdout.__base__):
_stream = "stdin"
line_new = result[0]
current_line.body, current_line.current_character, scrollback = result
del result
with (
# Don't allow the `stdin` to be used because it can lock Blender.
redirect_stdin(None),
_TempModuleOverride("__main__", console._bpy_main_mod),
):
try:
current_line = sc.history[-1]
line = current_line.body
# update selection. setting body should really do this!
ofs = len(line_new) - len(line)
sc.select_start += ofs
sc.select_end += ofs
except:
# unlikely, but this can happen with unicode errors for example.
# or if the api attribute access itself causes an error.
import traceback
scrollback_error = traceback.format_exc()
# This function isn't aware of the text editor or being an operator
# just does the autocomplete then copy its results back
result = intellisense.expand(
line=line,
cursor=current_line.current_character,
namespace=console.locals,
private=bpy.app.debug_python)
if _BPY_MAIN_OWN:
sys.modules["__main__"] = main_mod_back
line_new = result[0]
current_line.body, current_line.current_character, scrollback = result
del result
# update selection. setting body should really do this!
ofs = len(line_new) - len(line)
sc.select_start += ofs
sc.select_end += ofs
except:
# unlikely, but this can happen with unicode errors for example.
# or if the API attribute access itself causes an error.
import traceback
scrollback_error = traceback.format_exc()
# Separate autocomplete output by command prompts
if scrollback != '':
@@ -275,9 +288,6 @@ def autocomplete(context):
if scrollback_error:
add_scrollback(scrollback_error, 'ERROR')
# restore the stdin
sys.stdin = stdin_backup
context.area.tag_redraw()
return {'FINISHED'}