From ecc974de1e43d79c2cb5cc163868bbedbf81dd89 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Tue, 25 Jul 2017 10:28:42 -0700 Subject: [PATCH] Implement Python 2 `exec` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The implementation of `hy.core.language.exec` draws code from the `exec_` function in commit f574c7be6ebc80041ef58ca29588f310248ebed4 of the library Six, which is copyright 2010–2017 Benjamin Peterson and licensed under the Expat license. --- NEWS | 1 + docs/conf.py | 6 ++++++ docs/language/core.rst | 13 +++++++++++ hy/compiler.py | 13 +++++++++++ hy/core/language.hy | 22 +++++++++++++++++-- tests/compilers/test_ast.py | 24 +++++++++++++++++++++ tests/native_tests/core.hy | 43 +++++++++++++++++++++++++++++++++++++ 7 files changed, 120 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 2c52152..c0f1631 100644 --- a/NEWS +++ b/NEWS @@ -23,6 +23,7 @@ Changes from 0.13.0 mistaken for shebang lines * Fixed a bug where REPL history wasn't saved if you quit the REPL with `(quit)` or `(exit)` + * `exec` now works under Python 2 Changes from 0.12.1 diff --git a/docs/conf.py b/docs/conf.py index 18b647d..9877cc4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,6 +5,8 @@ import re, os, sys, time, cgi sys.path.append(os.path.abspath("..")) +extensions = ['sphinx.ext.intersphinx'] + from get_version import __version__ as hy_version # Read the Docs might dirty its checkout, so strip the dirty flag. hy_version = re.sub('[+.]dirty\Z', '', hy_version) @@ -48,3 +50,7 @@ html_show_sphinx = False html_context = dict( hy_descriptive_version = hy_descriptive_version) + +intersphinx_mapping = dict( + py2 = ('https://docs.python.org/2', None), + py = ('https://docs.python.org/3', None)) diff --git a/docs/language/core.rst b/docs/language/core.rst index 4983af9..d5b1cef 100644 --- a/docs/language/core.rst +++ b/docs/language/core.rst @@ -281,6 +281,19 @@ otherwise ``False``. Return ``True`` if *coll* is empty. True +.. _exec-fn: + +exec +---- + +Equivalent to Python 3's built-in function :py:func:`exec`. + +.. code-block:: clj + + => (exec "print(a + b)" {"a" 1} {"b" 2}) + 3 + + .. _float?-fn: float? diff --git a/hy/compiler.py b/hy/compiler.py index 5fd073f..858bc59 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -778,6 +778,19 @@ class HyASTCompiler(object): def compile_unpack_mapping(self, expr): raise HyTypeError(expr, "`unpack-mapping` isn't allowed here") + @builds_if("exec*", not PY3) + # Under Python 3, `exec` is a function rather than a statement type, so Hy + # doesn't need a special form for it. + @checkargs(min=1, max=3) + def compile_exec(self, expr): + expr.pop(0) + return ast.Exec( + lineno=expr.start_line, + col_offset=expr.start_column, + body=self.compile(expr.pop(0)).force_expr, + globals=self.compile(expr.pop(0)).force_expr if expr else None, + locals=self.compile(expr.pop(0)).force_expr if expr else None) + @builds("do") def compile_do(self, expression): expression.pop(0) diff --git a/hy/core/language.hy b/hy/core/language.hy index 9c29d27..5f9378a 100644 --- a/hy/core/language.hy +++ b/hy/core/language.hy @@ -117,6 +117,24 @@ range range zip zip)) +(if-python2 + (defn exec [$code &optional $globals $locals] + "Execute Python code. + + The parameter names contain weird characters to discourage calling this + function with keyword arguments, which isn't supported by Python 3's + `exec`." + (if + (none? $globals) (do + (setv frame (._getframe sys (int 1))) + (try + (setv $globals frame.f_globals $locals frame.f_locals) + (finally (del frame)))) + (none? $locals) + (setv $locals $globals)) + (exec* $code $globals $locals)) + (def exec exec)) + ;; infinite iterators (def count itertools.count @@ -461,8 +479,8 @@ (def *exports* '[*map accumulate butlast calling-module-name chain coll? combinations comp complement compress cons cons? constantly count cycle dec distinct - disassemble drop drop-last drop-while empty? eval even? every? first filter - flatten float? fraction gensym group-by identity inc input instance? + disassemble drop drop-last drop-while empty? eval even? every? exec first + filter flatten float? fraction gensym group-by identity inc input instance? integer integer? integer-char? interleave interpose islice iterable? iterate iterator? juxt keyword keyword? last list* macroexpand macroexpand-1 map merge-with multicombinations name neg? none? nth diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index cd982d1..1e86304 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -593,3 +593,27 @@ def test_setv_builtins(): def test_lots_of_comment_lines(): # https://github.com/hylang/hy/issues/1313 can_compile(1000 * ";\n") + + +def test_exec_star(): + + code = can_compile('(exec* "print(5)")').body[0] + assert type(code) == (ast.Expr if PY3 else ast.Exec) + if not PY3: + assert code.body.s == "print(5)" + assert code.globals is None + assert code.locals is None + + code = can_compile('(exec* "print(a)" {"a" 3})').body[0] + assert type(code) == (ast.Expr if PY3 else ast.Exec) + if not PY3: + assert code.body.s == "print(a)" + assert code.globals.keys[0].s == "a" + assert code.locals is None + + code = can_compile('(exec* "print(a + b)" {"a" "x"} {"b" "y"})').body[0] + assert type(code) == (ast.Expr if PY3 else ast.Exec) + if not PY3: + assert code.body.s == "print(a + b)" + assert code.globals.keys[0].s == "a" + assert code.locals.keys[0].s == "b" diff --git a/tests/native_tests/core.hy b/tests/native_tests/core.hy index dcef6f8..4cff830 100644 --- a/tests/native_tests/core.hy +++ b/tests/native_tests/core.hy @@ -170,6 +170,49 @@ (assert-false (every? even? [2 4 5])) (assert-true (every? even? []))) +(setv globalvar 1) +(defn test-exec [] + (setv localvar 1) + (setv code " +result['localvar in locals'] = 'localvar' in locals() +result['localvar in globals'] = 'localvar' in globals() +result['globalvar in locals'] = 'globalvar' in locals() +result['globalvar in globals'] = 'globalvar' in globals() +result['x in locals'] = 'x' in locals() +result['x in globals'] = 'x' in globals() +result['y in locals'] = 'y' in locals() +result['y in globals'] = 'y' in globals()") + + (setv result {}) + (exec code) + (assert-true (get result "localvar in locals")) + (assert-false (get result "localvar in globals")) + (assert-false (get result "globalvar in locals")) + (assert-true (get result "globalvar in globals")) + (assert-false (or + (get result "x in locals") (get result "x in globals") + (get result "y in locals") (get result "y in globals"))) + + (setv result {}) + (exec code {"x" 1 "result" result}) + (assert-false (or + (get result "localvar in locals") (get result "localvar in globals") + (get result "globalvar in locals") (get result "globalvar in globals"))) + (assert-true (and + (get result "x in locals") (get result "x in globals"))) + (assert-false (or + (get result "y in locals") (get result "y in globals"))) + + (setv result {}) + (exec code {"x" 1 "result" result} {"y" 1}) + (assert-false (or + (get result "localvar in locals") (get result "localvar in globals") + (get result "globalvar in locals") (get result "globalvar in globals"))) + (assert-false (get result "x in locals")) + (assert-true (get result "x in globals")) + (assert-true (get result "y in locals")) + (assert-false (get result "y in globals"))) + (defn test-filter [] "NATIVE: testing the filter function" (setv res (list (filter pos? [ 1 2 3 -4 5])))