From 8351ccf9d9a82409c202bc0f73eb28e3861f5415 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Fri, 6 Sep 2019 15:15:22 -0400 Subject: [PATCH] Allow inline Python --- NEWS.rst | 2 ++ docs/language/api.rst | 38 +++++++++++++++++++++++++++++++++++++ docs/language/interop.rst | 6 ++++-- docs/whyhy.rst | 4 ++-- hy/compiler.py | 17 +++++++++++++++++ tests/compilers/test_ast.py | 7 +++++++ tests/resources/pydemo.hy | 11 +++++++++++ tests/test_hy2py.py | 8 +++++++- 8 files changed, 88 insertions(+), 5 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index f11c5c8..3c7db04 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -13,6 +13,8 @@ Removals New Features ------------------------------ +* Added special forms ``py`` to ``pys`` that allow Hy programs to include + inline Python code. * All augmented assignment operators (except `%=` and `^=`) now allow more than two arguments. diff --git a/docs/language/api.rst b/docs/language/api.rst index 5fe8920..496f3da 100644 --- a/docs/language/api.rst +++ b/docs/language/api.rst @@ -1406,6 +1406,44 @@ parameter will be returned. True +.. _py-specialform: + +py +-- + +``py`` parses the given Python code at compile-time and inserts the result into +the generated abstract syntax tree. Thus, you can mix Python code into a Hy +program. Only a Python expression is allowed, not statements; use +:ref:`pys-specialform` if you want to use Python statements. The value of the +expression is returned from the ``py`` form. :: + + (print "A result from Python:" (py "'hello' + 'world'")) + +The code must be given as a single string literal, but you can still use +macros, :ref:`eval`, and related tools to construct the ``py`` form. If you +want to evaluate some Python code that's only defined at run-time, try the +standard Python function :func:`eval`. + +Python code need not syntactically round-trip if you use ``hy2py`` on a Hy +program that uses ``py`` or ``pys``. For example, comments will be removed. + + +.. _pys-specialform: + +pys +--- + +As :ref:`py-specialform`, but the code can consist of zero or more statements, +including compound statements such as ``for`` and ``def``. ``pys`` always +returns ``None``. Also, the code string is dedented with +:func:`textwrap.dedent` before parsing, which allows you to intend the code to +match the surrounding Hy code, but significant leading whitespace in embedded +string literals will be removed. :: + + (pys "myvar = 5") + (print "myvar is" myvar) + + .. _quasiquote: quasiquote diff --git a/docs/language/interop.rst b/docs/language/interop.rst index cb009b4..d4079d3 100644 --- a/docs/language/interop.rst +++ b/docs/language/interop.rst @@ -19,9 +19,11 @@ Hy and Python. For example, Python's ``str.format_map`` can be written Using Python from Hy ==================== -Using Python from Hy is nice and easy, you just have to :ref:`import` it. +You can embed Python code directly into a Hy program with the special operators +:ref:`py-specialform` and :ref:`pys-specialform`. -If you have the following in ``greetings.py`` in Python:: +Using a Python module from Hy is nice and easy: you just have to :ref:`import` +it. If you have the following in ``greetings.py`` in Python:: def greet(name): print("hello," name) diff --git a/docs/whyhy.rst b/docs/whyhy.rst index 55ad8bc..cde6ee3 100644 --- a/docs/whyhy.rst +++ b/docs/whyhy.rst @@ -81,8 +81,8 @@ The Hy compiler works by reading Hy source code into Hy model objects and compiling the Hy model objects into Python abstract syntax tree (:py:mod:`ast`) objects. Python AST objects can then be compiled and run by Python itself, byte-compiled for faster execution later, or rendered into Python source code. -You can even :ref:`mix Python and Hy code in the same project `, which -can be a good way to get your feet wet in Hy. +You can even :ref:`mix Python and Hy code in the same project, or even the same +file,` which can be a good way to get your feet wet in Hy. Hy versus other Lisps diff --git a/hy/compiler.py b/hy/compiler.py index 7b48822..26ff7e1 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -20,6 +20,7 @@ from hy.macros import require, load_macros, macroexpand, tag_macroexpand import hy.core import re +import textwrap import pkgutil import traceback import importlib @@ -1589,6 +1590,22 @@ class HyASTCompiler(object): if ast_str(root) == "eval_and_compile" else Result()) + @special(["py", "pys"], [STR]) + def compile_inline_python(self, expr, root, code): + exec_mode = root == HySymbol("pys") + + try: + o = ast.parse( + textwrap.dedent(code) if exec_mode else code, + self.filename, + 'exec' if exec_mode else 'eval').body + except (SyntaxError, ValueError if PY36 else TypeError) as e: + raise self._syntax_error( + expr, + "Python parse error in '{}': {}".format(root, e)) + + return Result(stmts=o) if exec_mode else o + @builds_model(HyExpression) def compile_expression(self, expr): # Perform macro expansions diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index 70ac563..e5e1d2d 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -639,3 +639,10 @@ def test_futures_imports(): assert hy_ast.body[0].module == '__future__' assert hy_ast.body[1].module == 'hy.core.language' + + +def test_inline_python(): + can_compile('(py "1 + 1")') + cant_compile('(py "1 +")') + can_compile('(pys "if 1:\n 2")') + cant_compile('(pys "if 1\n 2")') diff --git a/tests/resources/pydemo.hy b/tests/resources/pydemo.hy index a23e3c2..5774dac 100644 --- a/tests/resources/pydemo.hy +++ b/tests/resources/pydemo.hy @@ -156,3 +156,14 @@ Call me Ishmael. Some years ago—never mind how long precisely—having little (with [c1 (closing (Closeable)) c2 (closing (Closeable))] (setv c1.x "v1") (setv c2.x "v2")) +(setv closed1 (.copy closed)) + +(pys " + closed = [] + pys_accum = [] + for i in range(5): + with closing(Closeable()) as o: + class C: pass + o.x = C() + pys_accum.append(i)") +(setv py-accum (py "''.join(map(str, pys_accum))")) diff --git a/tests/test_hy2py.py b/tests/test_hy2py.py index d19e4f6..f774b7d 100644 --- a/tests/test_hy2py.py +++ b/tests/test_hy2py.py @@ -120,4 +120,10 @@ def assert_stuff(m): assert issubclass(m.C2, m.C1) assert (m.C2.attr1, m.C2.attr2) == (5, 6) - assert m.closed == ["v2", "v1"] + assert m.closed1 == ["v2", "v1"] + + assert len(m.closed) == 5 + for a, b in itertools.combinations(m.closed, 2): + assert type(a) is not type(b) + assert m.pys_accum == [0, 1, 2, 3, 4] + assert m.py_accum == "01234"