Skip to content

Commit

Permalink
Make require more flexible about target module
Browse files Browse the repository at this point in the history
This change allows `require` to be called without a target module, in which case
it will use the caller's globals.  The compiler now produces Python AST that
calls `require` without a target module, with the intention that macros are to
be loaded into the namespace in which its AST is `eval`ed.

Additionally, a test was added that targets this functionality.
  • Loading branch information
brandonwillard committed Oct 7, 2018
1 parent 0d996fd commit 67e9d6a
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 22 deletions.
36 changes: 22 additions & 14 deletions hy/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1190,24 +1190,32 @@ def compile_import_or_require(self, expr, root, entries):

ast_prefix = ast_str(prefix) if prefix else prefix
ast_prefix = asty.Str(expr, s=ast_prefix)
call = asty.Call(expr,
func=asty.Name(expr, id='require', ctx=ast.Load()),
args=[asty.Str(expr, s=ast_module),
asty.Str(expr, s=self.module_name)],
keywords=[
asty.keyword(expr,
arg='assignments',
value=names),
asty.keyword(expr,
arg='prefix',
value=ast_prefix)])
call = asty.Call(
expr,
func=asty.Name(expr, id='require', ctx=ast.Load()),
args=[
asty.Str(expr, s=ast_module),
# Don't be too specific about the destination of
# the macros from `ast_module`; wherever this
# compiled AST is run is where the macros will be
# added!
asty.Name(expr, id='None', ctx=ast.Load())
# This work in Python 3.x, but not 2.7
# asty.NameConstant(expr, value=None)
],
keywords=[
asty.keyword(expr,
arg='assignments',
value=names),
asty.keyword(expr,
arg='prefix',
value=ast_prefix)])

require_expr = asty.Expr(expr, value=call)
ret += require_expr

if self.module:
require(ast_module, self.module_name,
assignments=assignments, prefix=prefix)
require(ast_module, self.module,
assignments=assignments, prefix=prefix)

return ret

Expand Down
4 changes: 2 additions & 2 deletions hy/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def __getattr__(self, item):
elif item == 'name':
return self.fullname
else:
return self[item]
super(HyLoader, self).__getattr__(item)

def exec_module(self, module, fullname=None):
fullname = self._fix_name(fullname)
Expand Down Expand Up @@ -478,7 +478,7 @@ def get_code(self, fullname=None):
self.code = self.byte_compile_hy(fullname)

if self.code is None:
super(HyLoader, self).get_code(fullname=fullname)
super(HyLoader, self).get_code(fullname=fullname)

return self.code

Expand Down
24 changes: 18 additions & 6 deletions hy/macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import importlib
import inspect

from hy._compat import PY3
from hy._compat import PY3, string_types
import hy.inspect
from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
from hy.lex import mangle
Expand Down Expand Up @@ -77,8 +77,10 @@ def require(source_module, target_module, assignments, prefix=""):
source_module: str or types.ModuleType
The module from which macros are to be imported.
target_module: str or types.ModuleType
The module into which the macros will be loaded.
target_module: str, types.ModuleType or None
The module into which the macros will be loaded. If `None`, then
the caller's namespace.
The latter is useful during evaluation of generated AST/bytecode.
assignments: str or list of tuples of strs
The string "ALL" or a dict of macro names to aliases.
Expand All @@ -102,10 +104,20 @@ def require(source_module, target_module, assignments, prefix=""):
raise ImportError('Module {} does not contain any macros or tags'.format(
source_module))

if not inspect.ismodule(target_module):
if target_module is None:
parent_frame = inspect.stack()[1][0]
target_namespace = parent_frame.f_globals
elif isinstance(target_module, string_types):
target_module = importlib.import_module(target_module)
target_macros = target_module.__dict__.setdefault('__macros__', {})
target_tags = target_module.__dict__.setdefault('__tags__', {})
target_namespace = target_module.__dict__
elif inspect.ismodule(target_module):
target_namespace = target_module.__dict__
else:
raise TypeError('`target_module` is not a recognized type: {}'.format(
type(target_module)))

target_macros = target_namespace.setdefault('__macros__', {})
target_tags = target_namespace.setdefault('__tags__', {})

if prefix:
prefix += "."
Expand Down
3 changes: 3 additions & 0 deletions tests/resources/bin/require_and_eval.hy
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(require [hy.extra.anaphoric [ap-if]])

(print (eval '(ap-if (+ "a" "b") (+ it "c"))))
19 changes: 19 additions & 0 deletions tests/test_bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,22 @@ def test_bin_hy_file_no_extension():
"""Confirm that a file with no extension is processed as Hy source"""
output, _ = run_cmd("hy tests/resources/no_extension")
assert "This Should Still Work" in output


def test_bin_hy_macro_require():
"""Confirm that a `require` will load macros into the non-module namespace
(i.e. `exec(code, locals)`) used by `runpy.run_path`.
In other words, this confirms that the AST generated for a `require` will
load macros into the unnamed namespace its run in."""

# First, with no bytecode
test_file = "tests/resources/bin/require_and_eval.hy"
rm(cache_from_source(test_file))
assert not os.path.exists(cache_from_source(test_file))
output, _ = run_cmd("hy {}".format(test_file))
assert "abc" == output.strip()

# Now, with bytecode
assert os.path.exists(cache_from_source(test_file))
output, _ = run_cmd("hy {}".format(test_file))
assert "abc" == output.strip()

0 comments on commit 67e9d6a

Please sign in to comment.