# -*- coding: utf-8 -*- """mcpyrate-enabled `code.InteractiveConsole`. Special commands: - `obj?` shows obj's docstring, `obj??` shows its source code. - `macros?` shows macro bindings. - `macro(f)` binds a function as a macro. Works also as a decorator. """ # Based on `imacropy.console.MacroConsole` by Juha Jeronen, # which was based on `macropy.core.MacroConsole` by Li Haoyi, # Justin Holmgren, Alberto Berti and all the other contributors, # 2013-2019. Used under the MIT license. # https://github.com/azazel75/macropy # https://github.com/Technologicat/imacropy __all__ = ["MacroConsole"] import ast import code import textwrap from .. import __version__ as mcpyrate_version from ..core import MacroExpansionError from ..debug import format_bindings from ..expander import find_macros, MacroExpander, global_postprocess from .utils import get_makemacro_sourcecode # Boot up `mcpyrate` so that the REPL can import modules that use macros. # Despite the meta-levels, there's just one global importer for the Python process. from .. import activate # noqa: F401 class MacroConsole(code.InteractiveConsole): def __init__(self, locals=None, filename=""): """Parameters like in `code.InteractiveConsole`.""" self.expander = MacroExpander(bindings={}, filename=filename) self._macro_bindings_changed = False if locals is None: locals = {} # Lucky that both meta-levels speak the same language, eh? locals['__macro_expander__'] = self.expander super().__init__(locals, filename) # Support for special REPL commands. self._internal_execute(get_makemacro_sourcecode()) self._internal_execute("import mcpyrate.repl.utils") def _internal_execute(self, source): """Execute given source in the console session. This is support magic for internal operation of the console session itself, e.g. for auto-loading macro functions. The source must be pure Python, i.e. no macros. The source is NOT added to the session history. This bypasses `runsource`, so it too can use this function. """ source = textwrap.dedent(source) tree = ast.parse(source) tree = ast.Interactive(tree.body) code = compile(tree, "", "single", self.compile.compiler.flags, 1) self.runcode(code) def interact(self, banner=None, exitmsg=None): """See `code.InteractiveConsole.interact`. The only thing we customize here is that if `banner is None`, in which case `code.InteractiveConsole` will print its default banner, we print help for our special commands and a line containing the `mcpyrate` version before that default banner. """ if banner is None: self.write(f"mcpyrate {mcpyrate_version} -- Advanced macro expander for Python.\n") self.write("- obj? to view obj's docstring, and obj?? to view its source code.\n") self.write("- macros? to view macro bindings.\n") self.write("- macro(f) to bind function f as a macro. Works also as a decorator.\n") return super().interact(banner, exitmsg) def runsource(self, source, filename="", symbol="single"): # Special REPL commands. if source == "macros?": self.write(format_bindings(self.expander)) return False # complete input elif source.endswith("??"): return self.runsource(f'mcpyrate.repl.utils.sourcecode({source[:-2]})') elif source.endswith("?"): return self.runsource(f"mcpyrate.repl.utils.doc({source[:-1]})") try: code = self.compile(source, filename, symbol) except (OverflowError, SyntaxError, ValueError): code = "" if code is None: # incomplete input return True try: # TODO: If we want to support dialects in the REPL, this is where to do it. tree = ast.parse(source) bindings = find_macros(tree, filename=self.expander.filename, reload=True) # macro-imports (this will import the modules) if bindings: self._macro_bindings_changed = True self.expander.bindings.update(bindings) tree = self.expander.visit(tree) tree = global_postprocess(tree) tree = ast.Interactive(tree.body) code = compile(tree, filename, symbol, self.compile.compiler.flags, 1) except (OverflowError, SyntaxError, ValueError, MacroExpansionError): self.showsyntaxerror(filename) return False # erroneous input except ModuleNotFoundError as err: # during macro module lookup # In this case, the standard stack trace is long and points only to our code and the stdlib, # not the erroneous input that's the actual culprit. Better ignore it, and emulate showsyntaxerror. # TODO: support sys.excepthook. self.write(f"{err.__class__.__name__}: {str(err)}\n") return False # erroneous input except ImportError as err: # during macro lookup in a successfully imported module self.write(f"{err.__class__.__name__}: {str(err)}\n") return False # erroneous input self.runcode(code) self._refresh_macro_functions() return False # Successfully compiled. `runcode` takes care of any runtime failures. def _refresh_macro_functions(self): """Refresh macro function imports. Called after successfully compiling and running an input, so that `some_macro.__doc__` points to the right docstring. """ if not self._macro_bindings_changed: return self._macro_bindings_changed = False for asname, function in self.expander.bindings.items(): if not function.__module__: # Macros defined in the REPL have `__module__=None`. continue try: source = f"from {function.__module__} import {function.__qualname__} as {asname}" self._internal_execute(source) except (ModuleNotFoundError, ImportError): pass