Source code for cli_demo.code

# -*- coding: utf-8 -*-

"""This module contains CodeDemo, a Demo subclass that supports code commands."""

# For py2.7 compatibility
from __future__ import print_function
import sys
if sys.version_info < (3,3):
    input = raw_input

import inspect
import pprint
from .demo import Demo
from .exceptions import catch_exc


[docs]class CodeDemo(Demo): """CodeDemo improves Demo by introducing a feature called :attr:`~cli_demo.code.CodeDemo.commands`, which allows the user to select from a set of code snippets and view the result of it being passed into :meth:`~cli_demo.code.CodeDemo.execute`. Attributes: setup_code (str): The code to run in :meth:`~cli_demo.code.CodeDemo.setup_callback`. command_prompt (str): The input prompt for :meth:`~cli_demo.code.CodeDemo.get_commands`. commands (list[str]): The code snippets for the user to choose from in :meth:`~cli_demo.code.CodeDemo.get_commands`. locals (dict): The local namespace populated in :meth:`~cli_demo.code.CodeDemo.setup_callback`. globals (dict): The global namespace populated in :meth:`~cli_demo.code.CodeDemo.setup_callback`. """ help_text = """CodeDemo improves Demo by introducing a feature called `commands`, which allows the user to select from a set of code snippets and view the result of it being executed.""" setup_code = """\ # Setup code here. foo = 1 + 1 bar = 5 * 2 spam = 14""" command_prompt = "Choose a command: " commands = [ "1 # Comments will be removed.", "response + \" was your response\" # Variables are stored in memory", "foo + bar # Operations will print their result.", "eggs = spam + 5 # Assignments will print the assigned value.", "spam / 0 # Errors will get printed too!" ] options = Demo.options.copy()
[docs] @catch_exc def run(self): """The main logic of a :class:`~cli_demo.code.CodeDemo` program. First, call :meth:`~cli_demo.demo.Demo.print_intro`, then print the options for :meth:`~cli_demo.demo.Demo.run_setup` using :meth:`~cli_demo.demo.Demo.print_options` before calling :meth:`~cli_demo.demo.Demo.run_setup`, and then repeat the same process for :meth:`~cli_demo.code.CodeDemo.get_commands`. Note: :meth:`~cli_demo.demo.Demo.run` is decorated with:: @catch_exc def run(self): ... """ self.print_intro() self.print_options(key="setup") self.run_setup() self.print_options(key="commands") self.get_commands()
[docs] @options.register("c", "Setup code.", retry=True, newline=True) def print_setup(self): """Print :attr:`~cli_demo.code.CodeDemo.setup_code`. Note: :meth:`~cli_demo.code.CodeDemo.print_setup` is decorated with:: @options.register("c", "Setup code.", retry=True, newline=True) def print_setup(self): ... For more information, refer to :meth:`options.register <cli_demo.options.DemoOptions.register>`. """ print("Setup:") self.print_in(self.setup_code) print()
[docs] @options.register("setup") def setup_callback(self, response): """Handle user input to :meth:`~cli_demo.demo.Demo.run_setup`. Set :attr:`~cli_demo.code.CodeDemo.locals` to the global namespace of :mod:`__main__` before updating with `response`. Then, copy the ``__builtins__`` of :mod:`__main__` into :attr:`~cli_demo.code.CodeDemo.globals`. Finally, ``exec`` :attr:`~cli_demo.code.CodeDemo.setup_code` in :attr:`~cli_demo.code.CodeDemo.locals` and :attr:`~cli_demo.code.CodeDemo.globals` before printing it using :meth:`~cli_demo.code.CodeDemo.print_setup`. Args: response (str): The user input to :meth:`~cli_demo.demo.Demo.run_setup`. Note: * The :class:`~cli_demo.code.CodeDemo` instance is available in :attr:`~cli_demo.code.CodeDemo.locals` under the name `demo`, and the user response under `response`. * :meth:`~cli_demo.code.CodeDemo.setup_callback` is decorated with:: @options.register("setup") def setup_callback(self, response): ... For more information, refer to :meth:`options.register <cli_demo.options.DemoOptions.register>`. """ main = sys.modules["__main__"] self.globals = vars(main.__builtins__).copy() for name in ["__import__"]: del self.globals[name] self.locals = dict( inspect.getmembers(main, predicate=lambda obj: not inspect.ismodule(obj)), demo=self, response=response) exec(compile(self.setup_code, "<string>", "exec"), {}, self.locals) print() self.print_setup()
[docs] @options("c", "o", "r", "q", key="commands") def get_commands(self): """Prompt the user to select a command from :attr:`~cli_demo.code.CodeDemo.commands`. Note: :meth:`~cli_demo.code.CodeDemo.get_commands` is decorated with:: @options("c", "o", "r", "q", key="commands") def get_commands(self): ... For more information, refer to :meth:`options <cli_demo.options.DemoOptions.__call__>`. """ return input(self.command_prompt)
[docs] @options.register("commands", retry=True) def commands_callback(self, response): """Handle user input to :meth:`~cli_demo.code.CodeDemo.get_commands`. :meth:`~cli_demo.code.CodeDemo.execute` the respective code snippet or all :attr:`~cli_demo.code.CodeDemo.commands` if `response` is a valid index or ``"a"``. Otherwise, :meth:`~cli_demo.demo.Demo.retry` with the error message: ``"Invalid index. Please try again."``. Args: response (str): The user input to :meth:`~cli_demo.code.CodeDemo.get_commands`. Note: :meth:`~cli_demo.code.CodeDemo.commands_callback` is decorated with:: @options.register("commands", retry=True) def commands_callback(self, response): ... For more information, refer to :meth:`options.register <cli_demo.options.DemoOptions.register>`. """ commands = None if response == "a": commands = self.commands[:] elif response in map(str, range(len(self.commands))): commands = self.commands[int(response):int(response)+1] if commands: self.execute(commands) else: self.retry("Invalid index. Please try again.")
[docs] def commands_options(self): """Provide options for :meth:`~cli_demo.code.CodeDemo.get_commands`. Note: * The descriptions and options are the code snippets and their enumerations. * An additional option is ``"a"``, which is ``"Execute all of the above."``. """ for index, command in enumerate(self.commands): yield (str(index), "\n ".join(command.splitlines())) yield ("a", "Execute all of the above.")
[docs] def execute(self, commands, print_in=True): """``exec`` each command in :attr:`~cli_demo.code.CodeDemo.locals` and :attr:`~cli_demo.code.CodeDemo.globals`. :meth:`~cli_demo.code.CodeDemo.print_in` the command if `print_in` is ``True``. Remove any comments, then compile the command if there are multiple lines or assignments. ``exec`` the code snippet, and :meth:`~cli_demo.code.CodeDemo.print_out` the result or catch and print any errors. If there are any assignments in the code snippet, :meth:`~cli_demo.code.CodeDemo.execute` their assigned names. Args: commands (list): The code snippets to ``exec``. print_in (bool): Whether to :meth:`~cli_demo.code.CodeDemo.print_in` a command. """ for command in commands: if print_in: self.print_in(command) while "#" in command: hash_index = command.find("#") newline_index = command.find("\n", hash_index) if newline_index == -1: command = command[:hash_index].rstrip() else: command = (command[:hash_index].rstrip() + command[newline_index:]) assigned_names = [] for line in command.splitlines(): if " = " in line: names = line.split(" = ")[0] if not (names.startswith("\t") or names.startswith(" ")): for name in names.split(","): assigned_names.append(name.strip()) try: if "\n" in command or " = " in command: code = compile(command, "<string>", "exec") elif not command.startswith("print("): code = "demo.print_out(" + command + ")" else: code = command exec(code, self.globals, self.locals) except SyntaxError as exc: print("SyntaxError: invalid syntax (\"{}\", line {})".format( command, exc.lineno)) except Exception as exc: print("{}: {}".format(exc.__class__.__name__, exc)) if assigned_names: self.execute(assigned_names) else: print()
[docs] def print_in(self, text): """Print each line in `text` starting with ``">>>"`` or ``"..."``.""" for line in text.splitlines(): if line.startswith(" "): print("... " + line) else: print(">>> " + line)
[docs] def print_out(self, *args): """Pretty-print `args` using ``pprint()``.""" if args: try: pprint.pprint(*args) except: print(*args)