diff --git a/docs/contrib/hy_repr.rst b/docs/contrib/hy_repr.rst index c9ad777..51d8842 100644 --- a/docs/contrib/hy_repr.rst +++ b/docs/contrib/hy_repr.rst @@ -5,7 +5,13 @@ Hy representations .. versionadded:: 0.13.0 ``hy.contrib.hy-repr`` is a module containing a single function. -To import it, say ``(import [hy.contrib.hy-repr [hy-repr]])``. +To import it, say:: + + (import [hy.contrib.hy-repr [hy-repr]]) + +To make the Hy REPL use it for output, invoke Hy like so:: + + $ hy --repl-output-fn=hy.contrib.hy-repr.hy-repr .. _hy-repr-fn: diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 50f0e73..19edbb9 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -193,7 +193,6 @@ Hy. Let's experiment with this in the hy interpreter:: [1, 2, 3] => {"dog" "bark" ... "cat" "meow"} - ... {'dog': 'bark', 'cat': 'meow'} => (, 1 2 3) (1, 2, 3) @@ -204,6 +203,20 @@ Hy. Let's experiment with this in the hy interpreter:: Notice the last two lines: Hy has a fraction literal like Clojure. +If you start Hy like this (a shell alias might be helpful):: + + $ hy --repl-output-fn=hy.contrib.hy-repr.hy-repr + +the interactive mode will use :ref:`_hy-repr-fn` instead of Python's +native ``repr`` function to print out values, so you'll see values in +Hy syntax rather than Python syntax:: + + => [1 2 3] + [1 2 3] + => {"dog" "bark" + ... "cat" "meow"} + {"dog" "bark" "cat" "meow"} + If you are familiar with other Lisps, you may be interested that Hy supports the Common Lisp method of quoting: diff --git a/hy/cmdline.py b/hy/cmdline.py index 87dbe67..699e5f6 100644 --- a/hy/cmdline.py +++ b/hy/cmdline.py @@ -32,14 +32,16 @@ import code import ast import sys import os +import importlib import astor.codegen import hy from hy.lex import LexException, PrematureEndOfInput, tokenize -from hy.compiler import hy_compile, HyTypeError -from hy.importer import (ast_compile, import_buffer_to_module, +from hy.lex.parser import hy_symbol_mangle +from hy.compiler import HyTypeError +from hy.importer import (hy_eval, import_buffer_to_module, import_file_to_ast, import_file_to_hst, import_buffer_to_ast, import_buffer_to_hst) from hy.completer import completion @@ -73,25 +75,35 @@ builtins.quit = HyQuitter('quit') builtins.exit = HyQuitter('exit') -def print_python_code(_ast): - # astor cannot handle ast.Interactive, so disguise it as a module - _ast_for_print = ast.Module() - _ast_for_print.body = _ast.body - print(astor.codegen.to_source(_ast_for_print)) - - class HyREPL(code.InteractiveConsole): - def __init__(self, spy=False, locals=None, filename=""): + def __init__(self, spy=False, output_fn=None, locals=None, + filename=""): + self.spy = spy + + if output_fn is None: + self.output_fn = repr + elif callable(output_fn): + self.output_fn = output_fn + else: + f = hy_symbol_mangle(output_fn) + if "." in output_fn: + module, f = f.rsplit(".", 1) + self.output_fn = getattr(importlib.import_module(module), f) + else: + self.output_fn = __builtins__[f] + code.InteractiveConsole.__init__(self, locals=locals, filename=filename) def runsource(self, source, filename='', symbol='single'): global SIMPLE_TRACEBACKS try: - tokens = tokenize(source) - except PrematureEndOfInput: - return True + try: + tokens = tokenize(source) + except PrematureEndOfInput: + return True + tokens = tokenize("(do " + source + "\n)") except LexException as e: if e.source is None: e.source = source @@ -100,10 +112,15 @@ class HyREPL(code.InteractiveConsole): return False try: - _ast = hy_compile(tokens, "__console__", root=ast.Interactive) - if self.spy: - print_python_code(_ast) - code = ast_compile(_ast, filename, symbol) + def ast_callback(main_ast, expr_ast): + if self.spy: + # Mush the two AST chunks into a single module for + # conversion into Python. + new_ast = ast.Module(main_ast.body + + [ast.Expr(expr_ast.body)]) + print(astor.to_source(new_ast)) + value = hy_eval(tokens[0], self.locals, "__console__", + ast_callback) except HyTypeError as e: if e.source is None: e.source = source @@ -117,7 +134,12 @@ class HyREPL(code.InteractiveConsole): self.showtraceback() return False - self.runcode(code) + if value is not None: + # Make the last non-None value available to + # the user as `_`. + self.locals['_'] = value + # Print the value. + print(self.output_fn(value)) return False @@ -211,7 +233,7 @@ def run_file(filename): return 0 -def run_repl(hr=None, spy=False): +def run_repl(hr=None, **kwargs): import platform sys.ps1 = "=> " sys.ps2 = "... " @@ -221,7 +243,7 @@ def run_repl(hr=None, spy=False): with completion(Completer(namespace)): if not hr: - hr = HyREPL(spy, namespace) + hr = HyREPL(locals=namespace, **kwargs) hr.interact("{appname} {version} using " "{py}({build}) {pyversion} on {os}".format( @@ -236,8 +258,8 @@ def run_repl(hr=None, spy=False): return 0 -def run_icommand(source, spy=False): - hr = HyREPL(spy) +def run_icommand(source, **kwargs): + hr = HyREPL(**kwargs) if os.path.exists(source): with open(source, "r") as f: source = f.read() @@ -272,7 +294,9 @@ def cmdline_handler(scriptname, argv): help="program passed in as a string, then stay in REPL") parser.add_argument("--spy", action="store_true", help="print equivalent Python code before executing") - + parser.add_argument("--repl-output-fn", + help="function for printing REPL output " + "(e.g., hy.contrib.hy-repr.hy-repr)") parser.add_argument("-v", "--version", action="version", version=VERSION) parser.add_argument("--show-tracebacks", action="store_true", @@ -315,7 +339,8 @@ def cmdline_handler(scriptname, argv): if options.icommand: # User did "hy -i ..." - return run_icommand(options.icommand, spy=options.spy) + return run_icommand(options.icommand, spy=options.spy, + output_fn=options.repl_output_fn) if options.args: if options.args[0] == "-": @@ -332,7 +357,7 @@ def cmdline_handler(scriptname, argv): sys.exit(e.errno) # User did NOTHING! - return run_repl(spy=options.spy) + return run_repl(spy=options.spy, output_fn=options.repl_output_fn) # entry point for cmd line script "hy" diff --git a/hy/importer.py b/hy/importer.py index 8418b7c..c3c0192 100644 --- a/hy/importer.py +++ b/hy/importer.py @@ -111,7 +111,7 @@ def import_buffer_to_module(module_name, buf): return mod -def hy_eval(hytree, namespace, module_name): +def hy_eval(hytree, namespace, module_name, ast_callback=None): foo = HyObject() foo.start_line = 0 foo.end_line = 0 @@ -133,6 +133,9 @@ def hy_eval(hytree, namespace, module_name): node.lineno = 1 node.col_offset = 1 + if ast_callback: + ast_callback(_ast, expr) + if not isinstance(namespace, dict): raise HyTypeError(foo, "Globals must be a dictionary") diff --git a/tests/test_bin.py b/tests/test_bin.py index 218b030..f670f2c 100644 --- a/tests/test_bin.py +++ b/tests/test_bin.py @@ -23,12 +23,17 @@ # DEALINGS IN THE SOFTWARE. import os import subprocess +import re from hy._compat import PY3 hy_dir = os.environ.get('HY_DIR', '') +def hr(s=""): + return "hy --repl-output-fn=hy.contrib.hy-repr.hy-repr " + s + + def run_cmd(cmd, stdin_data=None, expect=0): p = subprocess.Popen(os.path.join(hy_dir, cmd), stdin=subprocess.PIPE, @@ -58,6 +63,61 @@ def test_bin_hy_stdin(): output, _ = run_cmd("hy", '(koan)') assert "monk" in output + output, _ = run_cmd("hy --spy", '(koan)') + assert "monk" in output + assert "\\n Ummon" in output + + # --spy should work even when an exception is thrown + output, _ = run_cmd("hy --spy", '(foof)') + assert "foof()" in output + + +def test_bin_hy_stdin_multiline(): + output, _ = run_cmd("hy", '(+ "a" "b"\n"c" "d")') + assert "'abcd'" in output + + +def test_bin_hy_stdin_comments(): + _, err_empty = run_cmd("hy", '') + + output, err = run_cmd("hy", '(+ "a" "b") ; "c"') + assert "'ab'" in output + assert err == err_empty + + _, err = run_cmd("hy", '; 1') + assert err == err_empty + + +def test_bin_hy_stdin_assignment(): + # If the last form is an assignment, don't print the value. + + output, _ = run_cmd("hy", '(setv x (+ "A" "Z"))') + assert "AZ" not in output + + output, _ = run_cmd("hy", '(setv x (+ "A" "Z")) (+ "B" "Y")') + assert "AZ" not in output + assert "BY" in output + + output, _ = run_cmd("hy", '(+ "B" "Y") (setv x (+ "A" "Z"))') + assert "AZ" not in output + assert "BY" not in output + + +def test_bin_hy_stdin_hy_repr(): + output, _ = run_cmd("hy", '(+ [1] [2])') + assert "[1, 2]" in output.replace('L', '') + + output, _ = run_cmd(hr(), '(+ [1] [2])') + assert "[1 2]" in output + + output, _ = run_cmd(hr("--spy"), '(+ [1] [2])') + assert "[1]+[2]" in output.replace('L', '').replace(' ', '') + assert "[1 2]" in output + + # --spy should work even when an exception is thrown + output, _ = run_cmd(hr("--spy"), '(+ [1] [2] (foof))') + assert "[1]+[2]" in output.replace('L', '').replace(' ', '') + def test_bin_hy_cmd(): output, _ = run_cmd("hy -c \"(koan)\"")