Merge pull request #1682 from brandonwillard/macro-changes
Macro processing updates and fixes
This commit is contained in:
commit
c5abc85a2d
6
NEWS.rst
6
NEWS.rst
@ -12,9 +12,15 @@ New Features
|
|||||||
* Keyword objects (not just literal keywords) can be called, as
|
* Keyword objects (not just literal keywords) can be called, as
|
||||||
shorthand for `(get obj :key)`, and they accept a default value
|
shorthand for `(get obj :key)`, and they accept a default value
|
||||||
as a second argument.
|
as a second argument.
|
||||||
|
* Minimal macro expansion namespacing has been implemented. As a result,
|
||||||
|
external macros no longer have to `require` their own macro dependencies.
|
||||||
|
* Macros and tags now reside in module-level `__macros__` and `__tags__`
|
||||||
|
attributes.
|
||||||
|
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
------------------------------
|
------------------------------
|
||||||
|
* `require` now compiles to Python AST.
|
||||||
|
* Fixed circular `require`s.
|
||||||
* Fixed module reloading.
|
* Fixed module reloading.
|
||||||
* Fixed circular imports.
|
* Fixed circular imports.
|
||||||
* Fixed `__main__` file execution.
|
* Fixed `__main__` file execution.
|
||||||
|
@ -13,6 +13,7 @@ import io
|
|||||||
import importlib
|
import importlib
|
||||||
import py_compile
|
import py_compile
|
||||||
import runpy
|
import runpy
|
||||||
|
import types
|
||||||
|
|
||||||
import astor.code_gen
|
import astor.code_gen
|
||||||
|
|
||||||
@ -47,10 +48,26 @@ builtins.quit = HyQuitter('quit')
|
|||||||
builtins.exit = HyQuitter('exit')
|
builtins.exit = HyQuitter('exit')
|
||||||
|
|
||||||
|
|
||||||
class HyREPL(code.InteractiveConsole):
|
class HyREPL(code.InteractiveConsole, object):
|
||||||
def __init__(self, spy=False, output_fn=None, locals=None,
|
def __init__(self, spy=False, output_fn=None, locals=None,
|
||||||
filename="<input>"):
|
filename="<input>"):
|
||||||
|
|
||||||
|
super(HyREPL, self).__init__(locals=locals,
|
||||||
|
filename=filename)
|
||||||
|
|
||||||
|
# Create a proper module for this REPL so that we can obtain it easily
|
||||||
|
# (e.g. using `importlib.import_module`).
|
||||||
|
# Also, make sure it's properly introduced to `sys.modules` and
|
||||||
|
# consistently use its namespace as `locals` from here on.
|
||||||
|
module_name = self.locals.get('__name__', '__console__')
|
||||||
|
self.module = sys.modules.setdefault(module_name,
|
||||||
|
types.ModuleType(module_name))
|
||||||
|
self.module.__dict__.update(self.locals)
|
||||||
|
self.locals = self.module.__dict__
|
||||||
|
|
||||||
|
# Load cmdline-specific macros.
|
||||||
|
require('hy.cmdline', module_name, assignments='ALL')
|
||||||
|
|
||||||
self.spy = spy
|
self.spy = spy
|
||||||
|
|
||||||
if output_fn is None:
|
if output_fn is None:
|
||||||
@ -65,9 +82,6 @@ class HyREPL(code.InteractiveConsole):
|
|||||||
else:
|
else:
|
||||||
self.output_fn = __builtins__[mangle(output_fn)]
|
self.output_fn = __builtins__[mangle(output_fn)]
|
||||||
|
|
||||||
code.InteractiveConsole.__init__(self, locals=locals,
|
|
||||||
filename=filename)
|
|
||||||
|
|
||||||
# Pre-mangle symbols for repl recent results: *1, *2, *3
|
# Pre-mangle symbols for repl recent results: *1, *2, *3
|
||||||
self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
|
self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
|
||||||
self.locals.update({sym: None for sym in self._repl_results_symbols})
|
self.locals.update({sym: None for sym in self._repl_results_symbols})
|
||||||
@ -102,8 +116,7 @@ class HyREPL(code.InteractiveConsole):
|
|||||||
new_ast = ast.Module(main_ast.body +
|
new_ast = ast.Module(main_ast.body +
|
||||||
[ast.Expr(expr_ast.body)])
|
[ast.Expr(expr_ast.body)])
|
||||||
print(astor.to_source(new_ast))
|
print(astor.to_source(new_ast))
|
||||||
value = hy_eval(do, self.locals, "__console__",
|
value = hy_eval(do, self.locals, self.module, ast_callback)
|
||||||
ast_callback)
|
|
||||||
except HyTypeError as e:
|
except HyTypeError as e:
|
||||||
if e.source is None:
|
if e.source is None:
|
||||||
e.source = source
|
e.source = source
|
||||||
@ -181,8 +194,6 @@ def ideas_macro(ETname):
|
|||||||
|
|
||||||
""")])
|
""")])
|
||||||
|
|
||||||
require("hy.cmdline", "__console__", assignments="ALL")
|
|
||||||
require("hy.cmdline", "__main__", assignments="ALL")
|
|
||||||
|
|
||||||
SIMPLE_TRACEBACKS = True
|
SIMPLE_TRACEBACKS = True
|
||||||
|
|
||||||
@ -199,7 +210,8 @@ def pretty_error(func, *args, **kw):
|
|||||||
|
|
||||||
def run_command(source):
|
def run_command(source):
|
||||||
tree = hy_parse(source)
|
tree = hy_parse(source)
|
||||||
pretty_error(hy_eval, tree, module_name="__main__")
|
require("hy.cmdline", "__main__", assignments="ALL")
|
||||||
|
pretty_error(hy_eval, tree, None, importlib.import_module('__main__'))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@ -208,13 +220,13 @@ def run_repl(hr=None, **kwargs):
|
|||||||
sys.ps1 = "=> "
|
sys.ps1 = "=> "
|
||||||
sys.ps2 = "... "
|
sys.ps2 = "... "
|
||||||
|
|
||||||
namespace = {'__name__': '__console__', '__doc__': ''}
|
if not hr:
|
||||||
|
hr = HyREPL(**kwargs)
|
||||||
|
|
||||||
|
namespace = hr.locals
|
||||||
|
|
||||||
with completion(Completer(namespace)):
|
with completion(Completer(namespace)):
|
||||||
|
|
||||||
if not hr:
|
|
||||||
hr = HyREPL(locals=namespace, **kwargs)
|
|
||||||
|
|
||||||
hr.interact("{appname} {version} using "
|
hr.interact("{appname} {version} using "
|
||||||
"{py}({build}) {pyversion} on {os}".format(
|
"{py}({build}) {pyversion} on {os}".format(
|
||||||
appname=hy.__appname__,
|
appname=hy.__appname__,
|
||||||
@ -409,7 +421,6 @@ def hyc_main():
|
|||||||
# entry point for cmd line script "hy2py"
|
# entry point for cmd line script "hy2py"
|
||||||
def hy2py_main():
|
def hy2py_main():
|
||||||
import platform
|
import platform
|
||||||
module_name = "<STDIN>"
|
|
||||||
|
|
||||||
options = dict(prog="hy2py", usage="%(prog)s [options] [FILE]",
|
options = dict(prog="hy2py", usage="%(prog)s [options] [FILE]",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
@ -448,7 +459,7 @@ def hy2py_main():
|
|||||||
print()
|
print()
|
||||||
print()
|
print()
|
||||||
|
|
||||||
_ast = pretty_error(hy_compile, hst, module_name)
|
_ast = pretty_error(hy_compile, hst, '__main__')
|
||||||
if options.with_ast:
|
if options.with_ast:
|
||||||
if PY3 and platform.system() == "Windows":
|
if PY3 and platform.system() == "Windows":
|
||||||
_print_for_windows(astor.dump_tree(_ast))
|
_print_for_windows(astor.dump_tree(_ast))
|
||||||
|
133
hy/compiler.py
133
hy/compiler.py
@ -13,14 +13,16 @@ from hy.errors import HyCompileError, HyTypeError
|
|||||||
|
|
||||||
from hy.lex import mangle, unmangle
|
from hy.lex import mangle, unmangle
|
||||||
|
|
||||||
import hy.macros
|
from hy._compat import (str_type, string_types, bytes_type, long_type, PY3,
|
||||||
from hy._compat import (
|
PY35, raise_empty)
|
||||||
str_type, bytes_type, long_type, PY3, PY35, raise_empty)
|
from hy.macros import require, load_macros, macroexpand, tag_macroexpand
|
||||||
from hy.macros import require, macroexpand, tag_macroexpand
|
|
||||||
import hy.importer
|
import hy.importer
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
import importlib
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import pkgutil
|
||||||
|
import types
|
||||||
import ast
|
import ast
|
||||||
import sys
|
import sys
|
||||||
import copy
|
import copy
|
||||||
@ -279,32 +281,48 @@ def is_unpack(kind, x):
|
|||||||
and x[0] == "unpack-" + kind)
|
and x[0] == "unpack-" + kind)
|
||||||
|
|
||||||
|
|
||||||
_stdlib = {}
|
|
||||||
|
|
||||||
|
|
||||||
class HyASTCompiler(object):
|
class HyASTCompiler(object):
|
||||||
|
"""A Hy-to-Python AST compiler"""
|
||||||
|
|
||||||
def __init__(self, module_name):
|
def __init__(self, module):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
module: str or types.ModuleType
|
||||||
|
Module in which the Hy tree is evaluated.
|
||||||
|
"""
|
||||||
self.anon_var_count = 0
|
self.anon_var_count = 0
|
||||||
self.imports = defaultdict(set)
|
self.imports = defaultdict(set)
|
||||||
self.module_name = module_name
|
|
||||||
self.temp_if = None
|
self.temp_if = None
|
||||||
|
|
||||||
|
if not inspect.ismodule(module):
|
||||||
|
module = importlib.import_module(module)
|
||||||
|
|
||||||
|
self.module = module
|
||||||
|
self.module_name = module.__name__
|
||||||
|
|
||||||
self.can_use_stdlib = (
|
self.can_use_stdlib = (
|
||||||
not module_name.startswith("hy.core")
|
not self.module_name.startswith("hy.core")
|
||||||
or module_name == "hy.core.macros")
|
or self.module_name == "hy.core.macros")
|
||||||
|
|
||||||
|
# Load stdlib macros into the module namespace.
|
||||||
|
load_macros(self.module)
|
||||||
|
|
||||||
|
self._stdlib = {}
|
||||||
|
|
||||||
# Everything in core needs to be explicit (except for
|
# Everything in core needs to be explicit (except for
|
||||||
# the core macros, which are built with the core functions).
|
# the core macros, which are built with the core functions).
|
||||||
if self.can_use_stdlib and not _stdlib:
|
if self.can_use_stdlib:
|
||||||
# Populate _stdlib.
|
# Populate _stdlib.
|
||||||
import hy.core
|
import hy.core
|
||||||
for module in hy.core.STDLIB:
|
for stdlib_module in hy.core.STDLIB:
|
||||||
mod = importlib.import_module(module)
|
mod = importlib.import_module(stdlib_module)
|
||||||
for e in map(ast_str, mod.EXPORTS):
|
for e in map(ast_str, getattr(mod, 'EXPORTS', [])):
|
||||||
if getattr(mod, e) is not getattr(builtins, e, ''):
|
if getattr(mod, e) is not getattr(builtins, e, ''):
|
||||||
# Don't bother putting a name in _stdlib if it
|
# Don't bother putting a name in _stdlib if it
|
||||||
# points to a builtin with the same name. This
|
# points to a builtin with the same name. This
|
||||||
# prevents pointless imports.
|
# prevents pointless imports.
|
||||||
_stdlib[e] = module
|
self._stdlib[e] = stdlib_module
|
||||||
|
|
||||||
def get_anon_var(self):
|
def get_anon_var(self):
|
||||||
self.anon_var_count += 1
|
self.anon_var_count += 1
|
||||||
@ -1098,11 +1116,6 @@ class HyASTCompiler(object):
|
|||||||
brackets(SYM, sym(":as"), _symn) |
|
brackets(SYM, sym(":as"), _symn) |
|
||||||
brackets(SYM, brackets(many(_symn + maybe(sym(":as") + _symn)))))])
|
brackets(SYM, brackets(many(_symn + maybe(sym(":as") + _symn)))))])
|
||||||
def compile_import_or_require(self, expr, root, entries):
|
def compile_import_or_require(self, expr, root, entries):
|
||||||
"""
|
|
||||||
TODO for `require`: keep track of what we've imported in this run and
|
|
||||||
then "unimport" it after we've completed `thing' so that we don't
|
|
||||||
pollute other envs.
|
|
||||||
"""
|
|
||||||
ret = Result()
|
ret = Result()
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
@ -1128,8 +1141,9 @@ class HyASTCompiler(object):
|
|||||||
else:
|
else:
|
||||||
assignments = [(k, v or k) for k, v in kids]
|
assignments = [(k, v or k) for k, v in kids]
|
||||||
|
|
||||||
|
ast_module = ast_str(module, piecewise=True)
|
||||||
|
|
||||||
if root == "import":
|
if root == "import":
|
||||||
ast_module = ast_str(module, piecewise=True)
|
|
||||||
module = ast_module.lstrip(".")
|
module = ast_module.lstrip(".")
|
||||||
level = len(ast_module) - len(module)
|
level = len(ast_module) - len(module)
|
||||||
if assignments == "ALL" and prefix == "":
|
if assignments == "ALL" and prefix == "":
|
||||||
@ -1150,10 +1164,23 @@ class HyASTCompiler(object):
|
|||||||
for k, v in assignments]
|
for k, v in assignments]
|
||||||
ret += node(
|
ret += node(
|
||||||
expr, module=module or None, names=names, level=level)
|
expr, module=module or None, names=names, level=level)
|
||||||
else: # root == "require"
|
|
||||||
importlib.import_module(module)
|
elif require(ast_module, self.module, assignments=assignments,
|
||||||
require(module, self.module_name,
|
prefix=prefix):
|
||||||
assignments=assignments, prefix=prefix)
|
# Actually calling `require` is necessary for macro expansions
|
||||||
|
# occurring during compilation.
|
||||||
|
self.imports['hy.macros'].update([None])
|
||||||
|
# The `require` we're creating in AST is the same as above, but used at
|
||||||
|
# run-time (e.g. when modules are loaded via bytecode).
|
||||||
|
ret += self.compile(HyExpression([
|
||||||
|
HySymbol('hy.macros.require'),
|
||||||
|
HyString(ast_module),
|
||||||
|
HySymbol('None'),
|
||||||
|
HyKeyword('assignments'),
|
||||||
|
(HyString("ALL") if assignments == "ALL" else
|
||||||
|
[[HyString(k), HyString(v)] for k, v in assignments]),
|
||||||
|
HyKeyword('prefix'),
|
||||||
|
HyString(prefix)]).replace(expr))
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@ -1484,7 +1511,8 @@ class HyASTCompiler(object):
|
|||||||
[x for pair in attrs[0] for x in pair]).replace(attrs)))
|
[x for pair in attrs[0] for x in pair]).replace(attrs)))
|
||||||
|
|
||||||
for e in body:
|
for e in body:
|
||||||
e = self.compile(self._rewire_init(macroexpand(e, self)))
|
e = self.compile(self._rewire_init(
|
||||||
|
macroexpand(e, self.module, self)))
|
||||||
bodyr += e + e.expr_as_stmt()
|
bodyr += e + e.expr_as_stmt()
|
||||||
|
|
||||||
return bases + asty.ClassDef(
|
return bases + asty.ClassDef(
|
||||||
@ -1520,20 +1548,16 @@ class HyASTCompiler(object):
|
|||||||
return self.compile(tag_macroexpand(
|
return self.compile(tag_macroexpand(
|
||||||
HyString(mangle(tag)).replace(tag),
|
HyString(mangle(tag)).replace(tag),
|
||||||
arg,
|
arg,
|
||||||
self))
|
self.module))
|
||||||
|
|
||||||
_namespaces = {}
|
|
||||||
|
|
||||||
@special(["eval-and-compile", "eval-when-compile"], [many(FORM)])
|
@special(["eval-and-compile", "eval-when-compile"], [many(FORM)])
|
||||||
def compile_eval_and_compile(self, expr, root, body):
|
def compile_eval_and_compile(self, expr, root, body):
|
||||||
new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr)
|
new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr)
|
||||||
if self.module_name not in self._namespaces:
|
|
||||||
# Initialize a compile-time namespace for this module.
|
|
||||||
self._namespaces[self.module_name] = {
|
|
||||||
'hy': hy, '__name__': self.module_name}
|
|
||||||
hy.importer.hy_eval(new_expr + body,
|
hy.importer.hy_eval(new_expr + body,
|
||||||
self._namespaces[self.module_name],
|
self.module.__dict__,
|
||||||
self.module_name)
|
self.module)
|
||||||
|
|
||||||
return (self._compile_branch(body)
|
return (self._compile_branch(body)
|
||||||
if ast_str(root) == "eval_and_compile"
|
if ast_str(root) == "eval_and_compile"
|
||||||
else Result())
|
else Result())
|
||||||
@ -1541,7 +1565,7 @@ class HyASTCompiler(object):
|
|||||||
@builds_model(HyExpression)
|
@builds_model(HyExpression)
|
||||||
def compile_expression(self, expr):
|
def compile_expression(self, expr):
|
||||||
# Perform macro expansions
|
# Perform macro expansions
|
||||||
expr = macroexpand(expr, self)
|
expr = macroexpand(expr, self.module, self)
|
||||||
if not isinstance(expr, HyExpression):
|
if not isinstance(expr, HyExpression):
|
||||||
# Go through compile again if the type changed.
|
# Go through compile again if the type changed.
|
||||||
return self.compile(expr)
|
return self.compile(expr)
|
||||||
@ -1665,8 +1689,8 @@ class HyASTCompiler(object):
|
|||||||
attr=ast_str(local),
|
attr=ast_str(local),
|
||||||
ctx=ast.Load())
|
ctx=ast.Load())
|
||||||
|
|
||||||
if self.can_use_stdlib and ast_str(symbol) in _stdlib:
|
if self.can_use_stdlib and ast_str(symbol) in self._stdlib:
|
||||||
self.imports[_stdlib[ast_str(symbol)]].add(ast_str(symbol))
|
self.imports[self._stdlib[ast_str(symbol)]].add(ast_str(symbol))
|
||||||
|
|
||||||
return asty.Name(symbol, id=ast_str(symbol), ctx=ast.Load())
|
return asty.Name(symbol, id=ast_str(symbol), ctx=ast.Load())
|
||||||
|
|
||||||
@ -1699,20 +1723,41 @@ class HyASTCompiler(object):
|
|||||||
return ret + asty.Dict(m, keys=keyvalues[::2], values=keyvalues[1::2])
|
return ret + asty.Dict(m, keys=keyvalues[::2], values=keyvalues[1::2])
|
||||||
|
|
||||||
|
|
||||||
def hy_compile(tree, module_name, root=ast.Module, get_expr=False):
|
def hy_compile(tree, module, root=ast.Module, get_expr=False):
|
||||||
"""
|
"""
|
||||||
Compile a HyObject tree into a Python AST Module.
|
Compile a Hy tree into a Python AST tree.
|
||||||
|
|
||||||
If `get_expr` is True, return a tuple (module, last_expression), where
|
Parameters
|
||||||
`last_expression` is the.
|
----------
|
||||||
|
module: str or types.ModuleType
|
||||||
|
Module, or name of the module, in which the Hy tree is evaluated.
|
||||||
|
|
||||||
|
root: ast object, optional (ast.Module)
|
||||||
|
Root object for the Python AST tree.
|
||||||
|
|
||||||
|
get_expr: bool, optional (False)
|
||||||
|
If true, return a tuple with `(root_obj, last_expression)`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
out : A Python AST tree
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if isinstance(module, string_types):
|
||||||
|
if module.startswith('<') and module.endswith('>'):
|
||||||
|
module = types.ModuleType(module)
|
||||||
|
else:
|
||||||
|
module = importlib.import_module(ast_str(module, piecewise=True))
|
||||||
|
if not inspect.ismodule(module):
|
||||||
|
raise TypeError('Invalid module type: {}'.format(type(module)))
|
||||||
|
|
||||||
|
|
||||||
tree = wrap_value(tree)
|
tree = wrap_value(tree)
|
||||||
if not isinstance(tree, HyObject):
|
if not isinstance(tree, HyObject):
|
||||||
raise HyCompileError("`tree` must be a HyObject or capable of "
|
raise HyCompileError("`tree` must be a HyObject or capable of "
|
||||||
"being promoted to one")
|
"being promoted to one")
|
||||||
|
|
||||||
compiler = HyASTCompiler(module_name)
|
compiler = HyASTCompiler(module)
|
||||||
result = compiler.compile(tree)
|
result = compiler.compile(tree)
|
||||||
expr = result.force_expr
|
expr = result.force_expr
|
||||||
|
|
||||||
|
@ -39,13 +39,15 @@ class Completer(object):
|
|||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
self.path = [hy.compiler._special_form_compilers,
|
self.path = [hy.compiler._special_form_compilers,
|
||||||
builtins.__dict__,
|
builtins.__dict__,
|
||||||
hy.macros._hy_macros[None],
|
|
||||||
namespace]
|
namespace]
|
||||||
self.tag_path = [hy.macros._hy_tag[None]]
|
|
||||||
if '__name__' in namespace:
|
self.tag_path = []
|
||||||
module_name = namespace['__name__']
|
|
||||||
self.path.append(hy.macros._hy_macros[module_name])
|
namespace.setdefault('__macros__', {})
|
||||||
self.tag_path.append(hy.macros._hy_tag[module_name])
|
namespace.setdefault('__tags__', {})
|
||||||
|
|
||||||
|
self.path.append(namespace['__macros__'])
|
||||||
|
self.tag_path.append(namespace['__tags__'])
|
||||||
|
|
||||||
def attr_matches(self, text):
|
def attr_matches(self, text):
|
||||||
# Borrowed from IPython's completer
|
# Borrowed from IPython's completer
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
(import [hy [HyExpression HyDict]]
|
(import [hy [HyExpression HyDict]]
|
||||||
[functools [partial]]
|
[functools [partial]]
|
||||||
|
[importlib [import-module]]
|
||||||
[collections [OrderedDict]]
|
[collections [OrderedDict]]
|
||||||
[hy.macros [macroexpand :as mexpand]]
|
[hy.macros [macroexpand :as mexpand]]
|
||||||
[hy.compiler [HyASTCompiler]])
|
[hy.compiler [HyASTCompiler]])
|
||||||
@ -42,9 +43,11 @@
|
|||||||
|
|
||||||
(defn macroexpand-all [form &optional module-name]
|
(defn macroexpand-all [form &optional module-name]
|
||||||
"Recursively performs all possible macroexpansions in form."
|
"Recursively performs all possible macroexpansions in form."
|
||||||
(setv module-name (or module-name (calling-module-name))
|
(setv module (or (and module-name
|
||||||
|
(import-module module-name))
|
||||||
|
(calling-module))
|
||||||
quote-level [0]
|
quote-level [0]
|
||||||
ast-compiler (HyASTCompiler module-name)) ; TODO: make nonlocal after dropping Python2
|
ast-compiler (HyASTCompiler module)) ; TODO: make nonlocal after dropping Python2
|
||||||
(defn traverse [form]
|
(defn traverse [form]
|
||||||
(walk expand identity form))
|
(walk expand identity form))
|
||||||
(defn expand [form]
|
(defn expand [form]
|
||||||
@ -68,7 +71,7 @@
|
|||||||
[(= (first form) (HySymbol "require"))
|
[(= (first form) (HySymbol "require"))
|
||||||
(ast-compiler.compile form)
|
(ast-compiler.compile form)
|
||||||
(return)]
|
(return)]
|
||||||
[True (traverse (mexpand form ast-compiler))])
|
[True (traverse (mexpand form module ast-compiler))])
|
||||||
(if (coll? form)
|
(if (coll? form)
|
||||||
(traverse form)
|
(traverse form)
|
||||||
form)))
|
form)))
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
(import [hy.models [HySymbol HyKeyword]])
|
(import [hy.models [HySymbol HyKeyword]])
|
||||||
(import [hy.lex [LexException PrematureEndOfInput tokenize mangle unmangle]])
|
(import [hy.lex [LexException PrematureEndOfInput tokenize mangle unmangle]])
|
||||||
(import [hy.compiler [HyASTCompiler]])
|
(import [hy.compiler [HyASTCompiler]])
|
||||||
(import [hy.importer [hy-eval :as eval]])
|
(import [hy.importer [calling-module hy-eval :as eval]])
|
||||||
|
|
||||||
(defn butlast [coll]
|
(defn butlast [coll]
|
||||||
"Return an iterator of all but the last item in `coll`."
|
"Return an iterator of all but the last item in `coll`."
|
||||||
@ -295,12 +295,14 @@ Return series of accumulated sums (or other binary function results)."
|
|||||||
(defn macroexpand [form]
|
(defn macroexpand [form]
|
||||||
"Return the full macro expansion of `form`."
|
"Return the full macro expansion of `form`."
|
||||||
(import hy.macros)
|
(import hy.macros)
|
||||||
(hy.macros.macroexpand form (HyASTCompiler (calling-module-name))))
|
(setv module (calling-module))
|
||||||
|
(hy.macros.macroexpand form module (HyASTCompiler module)))
|
||||||
|
|
||||||
(defn macroexpand-1 [form]
|
(defn macroexpand-1 [form]
|
||||||
"Return the single step macro expansion of `form`."
|
"Return the single step macro expansion of `form`."
|
||||||
(import hy.macros)
|
(import hy.macros)
|
||||||
(hy.macros.macroexpand-1 form (HyASTCompiler (calling-module-name))))
|
(setv module (calling-module))
|
||||||
|
(hy.macros.macroexpand-1 form module (HyASTCompiler module)))
|
||||||
|
|
||||||
(defn merge-with [f &rest maps]
|
(defn merge-with [f &rest maps]
|
||||||
"Return the map of `maps` joined onto the first via the function `f`.
|
"Return the map of `maps` joined onto the first via the function `f`.
|
||||||
@ -467,8 +469,8 @@ Even objects with the __name__ magic will work."
|
|||||||
(or a b)))
|
(or a b)))
|
||||||
|
|
||||||
(setv EXPORTS
|
(setv EXPORTS
|
||||||
'[*map accumulate butlast calling-module-name chain coll? combinations
|
'[*map accumulate butlast calling-module calling-module-name chain coll?
|
||||||
comp complement compress constantly count cycle dec distinct
|
combinations comp complement compress constantly count cycle dec distinct
|
||||||
disassemble drop drop-last drop-while empty? eval even? every? exec first
|
disassemble drop drop-last drop-while empty? eval even? every? exec first
|
||||||
filter flatten float? fraction gensym group-by identity inc input instance?
|
filter flatten float? fraction gensym group-by identity inc input instance?
|
||||||
integer integer? integer-char? interleave interpose islice iterable?
|
integer integer? integer-char? interleave interpose islice iterable?
|
||||||
|
@ -245,34 +245,10 @@ Such 'o!' params are available within `body` as the equivalent 'g!' symbol."
|
|||||||
|
|
||||||
Use ``#doc foo`` instead for help with tag macro ``#foo``.
|
Use ``#doc foo`` instead for help with tag macro ``#foo``.
|
||||||
Use ``(help foo)`` instead for help with runtime objects."
|
Use ``(help foo)`` instead for help with runtime objects."
|
||||||
`(try
|
`(help (.get __macros__ '~symbol None)))
|
||||||
(import [importlib [import-module]])
|
|
||||||
(help (. (import-module "hy")
|
|
||||||
macros
|
|
||||||
_hy_macros
|
|
||||||
[__name__]
|
|
||||||
['~symbol]))
|
|
||||||
(except [KeyError]
|
|
||||||
(help (. (import-module "hy")
|
|
||||||
macros
|
|
||||||
_hy_macros
|
|
||||||
[None]
|
|
||||||
['~symbol])))))
|
|
||||||
|
|
||||||
(deftag doc [symbol]
|
(deftag doc [symbol]
|
||||||
"tag macro documentation
|
"tag macro documentation
|
||||||
|
|
||||||
Gets help for a tag macro function available in this module."
|
Gets help for a tag macro function available in this module."
|
||||||
`(try
|
`(help (.get __tags__ '~symbol None)))
|
||||||
(import [importlib [import-module]])
|
|
||||||
(help (. (import-module "hy")
|
|
||||||
macros
|
|
||||||
_hy_tag
|
|
||||||
[__name__]
|
|
||||||
['~symbol]))
|
|
||||||
(except [KeyError]
|
|
||||||
(help (. (import-module "hy")
|
|
||||||
macros
|
|
||||||
_hy_tag
|
|
||||||
[None]
|
|
||||||
['~symbol])))))
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
(setv _cache (frozenset (map unmangle (+
|
(setv _cache (frozenset (map unmangle (+
|
||||||
hy.core.language.EXPORTS
|
hy.core.language.EXPORTS
|
||||||
hy.core.shadow.EXPORTS
|
hy.core.shadow.EXPORTS
|
||||||
(list (.keys (get hy.macros._hy_macros None)))
|
(list (.keys hy.core.macros.__macros__))
|
||||||
keyword.kwlist
|
keyword.kwlist
|
||||||
(list (.keys hy.compiler._special_form_compilers))
|
(list (.keys hy.compiler._special_form_compilers))
|
||||||
(list hy.compiler._bad_roots)))))))
|
(list hy.compiler._bad_roots)))))))
|
||||||
|
163
hy/importer.py
163
hy/importer.py
@ -17,9 +17,10 @@ import importlib
|
|||||||
import __future__
|
import __future__
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from hy.errors import HyTypeError
|
from hy.errors import HyTypeError
|
||||||
from hy.compiler import hy_compile
|
from hy.compiler import hy_compile, ast_str
|
||||||
from hy.lex import tokenize, LexException
|
from hy.lex import tokenize, LexException
|
||||||
from hy.models import HyExpression, HySymbol
|
from hy.models import HyExpression, HySymbol
|
||||||
from hy._compat import string_types, PY3
|
from hy._compat import string_types, PY3
|
||||||
@ -29,6 +30,36 @@ hy_ast_compile_flags = (__future__.CO_FUTURE_DIVISION |
|
|||||||
__future__.CO_FUTURE_PRINT_FUNCTION)
|
__future__.CO_FUTURE_PRINT_FUNCTION)
|
||||||
|
|
||||||
|
|
||||||
|
def calling_module(n=1):
|
||||||
|
"""Get the module calling, if available.
|
||||||
|
|
||||||
|
As a fallback, this will import a module using the calling frame's
|
||||||
|
globals value of `__name__`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
n: int, optional
|
||||||
|
The number of levels up the stack from this function call.
|
||||||
|
The default is one level up.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
out: types.ModuleType
|
||||||
|
The module at stack level `n + 1` or `None`.
|
||||||
|
"""
|
||||||
|
frame_up = inspect.stack(0)[n + 1][0]
|
||||||
|
module = inspect.getmodule(frame_up)
|
||||||
|
if module is None:
|
||||||
|
# This works for modules like `__main__`
|
||||||
|
module_name = frame_up.f_globals.get('__name__', None)
|
||||||
|
if module_name:
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
def ast_compile(ast, filename, mode):
|
def ast_compile(ast, filename, mode):
|
||||||
"""Compile AST.
|
"""Compile AST.
|
||||||
|
|
||||||
@ -65,13 +96,9 @@ def hy_parse(source):
|
|||||||
return HyExpression([HySymbol("do")] + tokenize(source + "\n"))
|
return HyExpression([HySymbol("do")] + tokenize(source + "\n"))
|
||||||
|
|
||||||
|
|
||||||
def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None):
|
def hy_eval(hytree, locals=None, module=None, ast_callback=None):
|
||||||
"""Evaluates a quoted expression and returns the value.
|
"""Evaluates a quoted expression and returns the value.
|
||||||
|
|
||||||
The optional second and third arguments specify the dictionary of globals
|
|
||||||
to use and the module name. The globals dictionary defaults to ``(local)``
|
|
||||||
and the module name defaults to the name of the current module.
|
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
|
||||||
@ -89,13 +116,15 @@ def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None):
|
|||||||
hytree: a Hy expression tree
|
hytree: a Hy expression tree
|
||||||
Source code to parse.
|
Source code to parse.
|
||||||
|
|
||||||
namespace: dict, optional
|
locals: dict, optional
|
||||||
Namespace in which to evaluate the Hy tree. Defaults to the calling
|
Local environment in which to evaluate the Hy tree. Defaults to the
|
||||||
frame.
|
calling frame.
|
||||||
|
|
||||||
module_name: str, optional
|
module: str or types.ModuleType, optional
|
||||||
Name of the module to which the Hy tree is assigned. Defaults to
|
Module, or name of the module, to which the Hy tree is assigned and
|
||||||
the calling frame's module, if any, and '__eval__' otherwise.
|
the global values are taken.
|
||||||
|
Defaults to the calling frame's module, if any, and '__eval__'
|
||||||
|
otherwise.
|
||||||
|
|
||||||
ast_callback: callable, optional
|
ast_callback: callable, optional
|
||||||
A callback that is passed the Hy compiled tree and resulting
|
A callback that is passed the Hy compiled tree and resulting
|
||||||
@ -105,19 +134,23 @@ def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None):
|
|||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
out : Result of evaluating the Hy compiled tree.
|
out : Result of evaluating the Hy compiled tree.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if namespace is None:
|
if module is None:
|
||||||
|
module = calling_module()
|
||||||
|
|
||||||
|
if isinstance(module, string_types):
|
||||||
|
module = importlib.import_module(ast_str(module, piecewise=True))
|
||||||
|
elif not inspect.ismodule(module):
|
||||||
|
raise TypeError('Invalid module type: {}'.format(type(module)))
|
||||||
|
|
||||||
|
if locals is None:
|
||||||
frame = inspect.stack()[1][0]
|
frame = inspect.stack()[1][0]
|
||||||
namespace = inspect.getargvalues(frame).locals
|
locals = inspect.getargvalues(frame).locals
|
||||||
if module_name is None:
|
|
||||||
m = inspect.getmodule(inspect.stack()[1][0])
|
|
||||||
module_name = '__eval__' if m is None else m.__name__
|
|
||||||
|
|
||||||
if not isinstance(module_name, string_types):
|
if not isinstance(locals, dict):
|
||||||
raise TypeError("Module name must be a string")
|
raise TypeError("Locals must be a dictionary")
|
||||||
|
|
||||||
_ast, expr = hy_compile(hytree, module_name, get_expr=True)
|
_ast, expr = hy_compile(hytree, module, get_expr=True)
|
||||||
|
|
||||||
# Spoof the positions in the generated ast...
|
# Spoof the positions in the generated ast...
|
||||||
for node in ast.walk(_ast):
|
for node in ast.walk(_ast):
|
||||||
@ -131,14 +164,13 @@ def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None):
|
|||||||
if ast_callback:
|
if ast_callback:
|
||||||
ast_callback(_ast, expr)
|
ast_callback(_ast, expr)
|
||||||
|
|
||||||
if not isinstance(namespace, dict):
|
globals = module.__dict__
|
||||||
raise TypeError("Globals must be a dictionary")
|
|
||||||
|
|
||||||
# Two-step eval: eval() the body of the exec call
|
# Two-step eval: eval() the body of the exec call
|
||||||
eval(ast_compile(_ast, "<eval_body>", "exec"), namespace)
|
eval(ast_compile(_ast, "<eval_body>", "exec"), globals, locals)
|
||||||
|
|
||||||
# Then eval the expression context and return that
|
# Then eval the expression context and return that
|
||||||
return eval(ast_compile(expr, "<eval>", "eval"), namespace)
|
return eval(ast_compile(expr, "<eval>", "eval"), globals, locals)
|
||||||
|
|
||||||
|
|
||||||
def cache_from_source(source_path):
|
def cache_from_source(source_path):
|
||||||
@ -167,6 +199,52 @@ def cache_from_source(source_path):
|
|||||||
return os.path.join(d, re.sub(r"(?:\.[^.]+)?\Z", ".pyc", f))
|
return os.path.join(d, re.sub(r"(?:\.[^.]+)?\Z", ".pyc", f))
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def loader_module_obj(loader):
|
||||||
|
"""Use the module object associated with a loader.
|
||||||
|
|
||||||
|
This is intended to be used by a loader object itself, and primarily as a
|
||||||
|
work-around for attempts to get module and/or file code from a loader
|
||||||
|
without actually creating a module object. Since Hy currently needs the
|
||||||
|
module object for macro importing, expansion, and whatnot, using this will
|
||||||
|
reconcile Hy with such attempts.
|
||||||
|
|
||||||
|
For example, if we're first compiling a Hy script starting from
|
||||||
|
`runpy.run_path`, the Hy compiler will need a valid module object in which
|
||||||
|
to run, but, given the way `runpy.run_path` works, there might not be one
|
||||||
|
yet (e.g. `__main__` for a .hy file). We compensate by properly loading
|
||||||
|
the module here.
|
||||||
|
|
||||||
|
The function `inspect.getmodule` has a hidden-ish feature that returns
|
||||||
|
modules using their associated filenames (via `inspect.modulesbyfile`),
|
||||||
|
and, since the Loaders (and their delegate Loaders) carry a filename/path
|
||||||
|
associated with the parent package, we use it as a more robust attempt to
|
||||||
|
obtain an existing module object.
|
||||||
|
|
||||||
|
When no module object is found, a temporary, minimally sufficient module
|
||||||
|
object is created for the duration of the `with` body.
|
||||||
|
"""
|
||||||
|
tmp_mod = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = inspect.getmodule(None, _filename=loader.path)
|
||||||
|
except KeyError:
|
||||||
|
module = None
|
||||||
|
|
||||||
|
if module is None:
|
||||||
|
tmp_mod = True
|
||||||
|
module = sys.modules.setdefault(loader.name,
|
||||||
|
types.ModuleType(loader.name))
|
||||||
|
module.__file__ = loader.path
|
||||||
|
module.__name__ = loader.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield module
|
||||||
|
finally:
|
||||||
|
if tmp_mod:
|
||||||
|
del sys.modules[loader.name]
|
||||||
|
|
||||||
|
|
||||||
def _hy_code_from_file(filename, loader_type=None):
|
def _hy_code_from_file(filename, loader_type=None):
|
||||||
"""Use PEP-302 loader to produce code for a given Hy source file."""
|
"""Use PEP-302 loader to produce code for a given Hy source file."""
|
||||||
full_fname = os.path.abspath(filename)
|
full_fname = os.path.abspath(filename)
|
||||||
@ -226,7 +304,8 @@ if PY3:
|
|||||||
source = data.decode("utf-8")
|
source = data.decode("utf-8")
|
||||||
try:
|
try:
|
||||||
hy_tree = hy_parse(source)
|
hy_tree = hy_parse(source)
|
||||||
data = hy_compile(hy_tree, self.name)
|
with loader_module_obj(self) as module:
|
||||||
|
data = hy_compile(hy_tree, module)
|
||||||
except (HyTypeError, LexException) as e:
|
except (HyTypeError, LexException) as e:
|
||||||
if e.source is None:
|
if e.source is None:
|
||||||
e.source = source
|
e.source = source
|
||||||
@ -276,6 +355,15 @@ else:
|
|||||||
|
|
||||||
super(HyLoader, self).__init__(fullname, fileobj, filename, etc)
|
super(HyLoader, self).__init__(fullname, fileobj, filename, etc)
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
# We add these for Python >= 3.4 Loader interface compatibility.
|
||||||
|
if item == 'path':
|
||||||
|
return self.filename
|
||||||
|
elif item == 'name':
|
||||||
|
return self.fullname
|
||||||
|
else:
|
||||||
|
return super(HyLoader, self).__getattr__(item)
|
||||||
|
|
||||||
def exec_module(self, module, fullname=None):
|
def exec_module(self, module, fullname=None):
|
||||||
fullname = self._fix_name(fullname)
|
fullname = self._fix_name(fullname)
|
||||||
code = self.get_code(fullname)
|
code = self.get_code(fullname)
|
||||||
@ -283,7 +371,7 @@ else:
|
|||||||
|
|
||||||
def load_module(self, fullname=None):
|
def load_module(self, fullname=None):
|
||||||
"""Same as `pkgutil.ImpLoader`, with an extra check for Hy
|
"""Same as `pkgutil.ImpLoader`, with an extra check for Hy
|
||||||
source"""
|
source and the option to not run `self.exec_module`."""
|
||||||
fullname = self._fix_name(fullname)
|
fullname = self._fix_name(fullname)
|
||||||
ext_type = self.etc[0]
|
ext_type = self.etc[0]
|
||||||
mod_type = self.etc[2]
|
mod_type = self.etc[2]
|
||||||
@ -298,7 +386,7 @@ else:
|
|||||||
mod = sys.modules[fullname]
|
mod = sys.modules[fullname]
|
||||||
else:
|
else:
|
||||||
mod = sys.modules.setdefault(
|
mod = sys.modules.setdefault(
|
||||||
fullname, imp.new_module(fullname))
|
fullname, types.ModuleType(fullname))
|
||||||
|
|
||||||
# TODO: Should we set these only when not in `sys.modules`?
|
# TODO: Should we set these only when not in `sys.modules`?
|
||||||
if mod_type == imp.PKG_DIRECTORY:
|
if mod_type == imp.PKG_DIRECTORY:
|
||||||
@ -351,7 +439,8 @@ else:
|
|||||||
try:
|
try:
|
||||||
hy_source = self.get_source(fullname)
|
hy_source = self.get_source(fullname)
|
||||||
hy_tree = hy_parse(hy_source)
|
hy_tree = hy_parse(hy_source)
|
||||||
hy_ast = hy_compile(hy_tree, fullname)
|
with loader_module_obj(self) as module:
|
||||||
|
hy_ast = hy_compile(hy_tree, module)
|
||||||
|
|
||||||
code = compile(hy_ast, self.filename, 'exec',
|
code = compile(hy_ast, self.filename, 'exec',
|
||||||
hy_ast_compile_flags)
|
hy_ast_compile_flags)
|
||||||
@ -363,7 +452,7 @@ else:
|
|||||||
|
|
||||||
if not sys.dont_write_bytecode:
|
if not sys.dont_write_bytecode:
|
||||||
try:
|
try:
|
||||||
hyc_compile(code)
|
hyc_compile(code, module=fullname)
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
return code
|
return code
|
||||||
@ -470,7 +559,8 @@ else:
|
|||||||
|
|
||||||
_py_compile_compile = py_compile.compile
|
_py_compile_compile = py_compile.compile
|
||||||
|
|
||||||
def hyc_compile(file_or_code, cfile=None, dfile=None, doraise=False):
|
def hyc_compile(file_or_code, cfile=None, dfile=None, doraise=False,
|
||||||
|
module=None):
|
||||||
"""Write a Hy file, or code object, to pyc.
|
"""Write a Hy file, or code object, to pyc.
|
||||||
|
|
||||||
This is a patched version of Python 2.7's `py_compile.compile`.
|
This is a patched version of Python 2.7's `py_compile.compile`.
|
||||||
@ -489,6 +579,9 @@ else:
|
|||||||
The filename to use for compile-time errors.
|
The filename to use for compile-time errors.
|
||||||
doraise : bool, default False
|
doraise : bool, default False
|
||||||
If `True` raise compilation exceptions; otherwise, ignore them.
|
If `True` raise compilation exceptions; otherwise, ignore them.
|
||||||
|
module : str or types.ModuleType, optional
|
||||||
|
The module, or module name, in which the Hy tree is expanded.
|
||||||
|
Default is the caller's module.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@ -510,7 +603,13 @@ else:
|
|||||||
flags = None
|
flags = None
|
||||||
if _could_be_hy_src(filename):
|
if _could_be_hy_src(filename):
|
||||||
hy_tree = hy_parse(source_str)
|
hy_tree = hy_parse(source_str)
|
||||||
source = hy_compile(hy_tree, '<hyc_compile>')
|
|
||||||
|
if module is None:
|
||||||
|
module = inspect.getmodule(inspect.stack()[1][0])
|
||||||
|
elif not inspect.ismodule(module):
|
||||||
|
module = importlib.import_module(module)
|
||||||
|
|
||||||
|
source = hy_compile(hy_tree, module)
|
||||||
flags = hy_ast_compile_flags
|
flags = hy_ast_compile_flags
|
||||||
|
|
||||||
codeobject = compile(source, dfile or filename, 'exec', flags)
|
codeobject = compile(source, dfile or filename, 'exec', flags)
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
# Copyright 2018 the authors.
|
|
||||||
# This file is part of Hy, which is free software licensed under the Expat
|
|
||||||
# license. See the LICENSE.
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Check if we have the newer inspect.signature available.
|
|
||||||
# Otherwise fallback to the legacy getargspec.
|
|
||||||
inspect.signature # noqa
|
|
||||||
except AttributeError:
|
|
||||||
def get_arity(fn):
|
|
||||||
return len(inspect.getargspec(fn)[0])
|
|
||||||
|
|
||||||
def has_kwargs(fn):
|
|
||||||
argspec = inspect.getargspec(fn)
|
|
||||||
return argspec.keywords is not None
|
|
||||||
|
|
||||||
def format_args(fn):
|
|
||||||
argspec = inspect.getargspec(fn)
|
|
||||||
return inspect.formatargspec(*argspec)
|
|
||||||
|
|
||||||
else:
|
|
||||||
def get_arity(fn):
|
|
||||||
parameters = inspect.signature(fn).parameters
|
|
||||||
return sum(1 for param in parameters.values()
|
|
||||||
if param.kind == param.POSITIONAL_OR_KEYWORD)
|
|
||||||
|
|
||||||
def has_kwargs(fn):
|
|
||||||
parameters = inspect.signature(fn).parameters
|
|
||||||
return any(param.kind == param.VAR_KEYWORD
|
|
||||||
for param in parameters.values())
|
|
||||||
|
|
||||||
def format_args(fn):
|
|
||||||
return str(inspect.signature(fn))
|
|
333
hy/macros.py
333
hy/macros.py
@ -1,19 +1,39 @@
|
|||||||
# Copyright 2018 the authors.
|
# Copyright 2018 the authors.
|
||||||
# This file is part of Hy, which is free software licensed under the Expat
|
# This file is part of Hy, which is free software licensed under the Expat
|
||||||
# license. See the LICENSE.
|
# license. See the LICENSE.
|
||||||
import pkgutil
|
|
||||||
import importlib
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
from collections import defaultdict
|
from hy._compat import PY3, string_types
|
||||||
|
|
||||||
from hy._compat import PY3
|
|
||||||
import hy.inspect
|
|
||||||
from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
|
from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
|
||||||
from hy.lex import mangle
|
from hy.lex import mangle
|
||||||
from hy._compat import str_type
|
|
||||||
|
|
||||||
from hy.errors import HyTypeError, HyMacroExpansionError
|
from hy.errors import HyTypeError, HyMacroExpansionError
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if we have the newer inspect.signature available.
|
||||||
|
# Otherwise fallback to the legacy getargspec.
|
||||||
|
inspect.signature # noqa
|
||||||
|
except AttributeError:
|
||||||
|
def has_kwargs(fn):
|
||||||
|
argspec = inspect.getargspec(fn)
|
||||||
|
return argspec.keywords is not None
|
||||||
|
|
||||||
|
def format_args(fn):
|
||||||
|
argspec = inspect.getargspec(fn)
|
||||||
|
return inspect.formatargspec(*argspec)
|
||||||
|
|
||||||
|
else:
|
||||||
|
def has_kwargs(fn):
|
||||||
|
parameters = inspect.signature(fn).parameters
|
||||||
|
return any(param.kind == param.VAR_KEYWORD
|
||||||
|
for param in parameters.values())
|
||||||
|
|
||||||
|
def format_args(fn):
|
||||||
|
return str(inspect.signature(fn))
|
||||||
|
|
||||||
|
|
||||||
CORE_MACROS = [
|
CORE_MACROS = [
|
||||||
"hy.core.bootstrap",
|
"hy.core.bootstrap",
|
||||||
]
|
]
|
||||||
@ -22,114 +42,204 @@ EXTRA_MACROS = [
|
|||||||
"hy.core.macros",
|
"hy.core.macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
_hy_macros = defaultdict(dict)
|
|
||||||
_hy_tag = defaultdict(dict)
|
|
||||||
|
|
||||||
|
|
||||||
def macro(name):
|
def macro(name):
|
||||||
"""Decorator to define a macro called `name`.
|
"""Decorator to define a macro called `name`.
|
||||||
|
|
||||||
This stores the macro `name` in the namespace for the module where it is
|
|
||||||
defined.
|
|
||||||
|
|
||||||
If the module where it is defined is in `hy.core`, then the macro is stored
|
|
||||||
in the default `None` namespace.
|
|
||||||
|
|
||||||
This function is called from the `defmacro` special form in the compiler.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
name = mangle(name)
|
name = mangle(name)
|
||||||
def _(fn):
|
def _(fn):
|
||||||
fn.__name__ = '({})'.format(name)
|
fn.__name__ = '({})'.format(name)
|
||||||
try:
|
try:
|
||||||
fn._hy_macro_pass_compiler = hy.inspect.has_kwargs(fn)
|
fn._hy_macro_pass_compiler = has_kwargs(fn)
|
||||||
except Exception:
|
except Exception:
|
||||||
# An exception might be raised if fn has arguments with
|
# An exception might be raised if fn has arguments with
|
||||||
# names that are invalid in Python.
|
# names that are invalid in Python.
|
||||||
fn._hy_macro_pass_compiler = False
|
fn._hy_macro_pass_compiler = False
|
||||||
|
|
||||||
module_name = fn.__module__
|
module = inspect.getmodule(fn)
|
||||||
if module_name.startswith("hy.core"):
|
module_macros = module.__dict__.setdefault('__macros__', {})
|
||||||
module_name = None
|
module_macros[name] = fn
|
||||||
_hy_macros[module_name][name] = fn
|
|
||||||
return fn
|
return fn
|
||||||
return _
|
return _
|
||||||
|
|
||||||
|
|
||||||
def tag(name):
|
def tag(name):
|
||||||
"""Decorator to define a tag macro called `name`.
|
"""Decorator to define a tag macro called `name`.
|
||||||
|
|
||||||
This stores the macro `name` in the namespace for the module where it is
|
|
||||||
defined.
|
|
||||||
|
|
||||||
If the module where it is defined is in `hy.core`, then the macro is stored
|
|
||||||
in the default `None` namespace.
|
|
||||||
|
|
||||||
This function is called from the `deftag` special form in the compiler.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def _(fn):
|
def _(fn):
|
||||||
_name = mangle('#{}'.format(name))
|
_name = mangle('#{}'.format(name))
|
||||||
|
|
||||||
if not PY3:
|
if not PY3:
|
||||||
_name = _name.encode('UTF-8')
|
_name = _name.encode('UTF-8')
|
||||||
|
|
||||||
fn.__name__ = _name
|
fn.__name__ = _name
|
||||||
module_name = fn.__module__
|
|
||||||
|
module = inspect.getmodule(fn)
|
||||||
|
|
||||||
|
module_name = module.__name__
|
||||||
if module_name.startswith("hy.core"):
|
if module_name.startswith("hy.core"):
|
||||||
module_name = None
|
module_name = None
|
||||||
_hy_tag[module_name][mangle(name)] = fn
|
|
||||||
|
module_tags = module.__dict__.setdefault('__tags__', {})
|
||||||
|
module_tags[mangle(name)] = fn
|
||||||
|
|
||||||
return fn
|
return fn
|
||||||
return _
|
return _
|
||||||
|
|
||||||
|
|
||||||
|
def _same_modules(source_module, target_module):
|
||||||
|
"""Compare the filenames associated with the given modules names.
|
||||||
|
|
||||||
|
This tries to not actually load the modules.
|
||||||
|
"""
|
||||||
|
if not (source_module or target_module):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if target_module == source_module:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_filename(module):
|
||||||
|
filename = None
|
||||||
|
try:
|
||||||
|
if not inspect.ismodule(module):
|
||||||
|
loader = pkgutil.get_loader(module)
|
||||||
|
if loader:
|
||||||
|
filename = loader.get_filename()
|
||||||
|
else:
|
||||||
|
filename = inspect.getfile(module)
|
||||||
|
except (TypeError, ImportError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
source_filename = _get_filename(source_module)
|
||||||
|
target_filename = _get_filename(target_module)
|
||||||
|
|
||||||
|
return (source_filename and target_filename and
|
||||||
|
source_filename == target_filename)
|
||||||
|
|
||||||
|
|
||||||
def require(source_module, target_module, assignments, prefix=""):
|
def require(source_module, target_module, assignments, prefix=""):
|
||||||
"""Load macros from `source_module` in the namespace of
|
"""Load macros from one module into the namespace of another.
|
||||||
`target_module`. `assignments` maps old names to new names, or
|
|
||||||
should be the string "ALL". If `prefix` is nonempty, it is
|
|
||||||
prepended to the name of each imported macro. (This means you get
|
|
||||||
macros named things like "mymacromodule.mymacro", which looks like
|
|
||||||
an attribute of a module, although it's actually just a symbol
|
|
||||||
with a period in its name.)
|
|
||||||
|
|
||||||
This function is called from the `require` special form in the compiler.
|
This function is called from the `require` special form in the compiler.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
source_module: str or types.ModuleType
|
||||||
|
The module from which macros are to be imported.
|
||||||
|
|
||||||
|
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 list of macro name and alias pairs.
|
||||||
|
|
||||||
|
prefix: str, optional ("")
|
||||||
|
If nonempty, its value is prepended to the name of each imported macro.
|
||||||
|
This allows one to emulate namespaced macros, like
|
||||||
|
"mymacromodule.mymacro", which looks like an attribute of a module.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
out: boolean
|
||||||
|
Whether or not macros and tags were actually transferred.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
seen_names = set()
|
if target_module is None:
|
||||||
|
parent_frame = inspect.stack()[1][0]
|
||||||
|
target_namespace = parent_frame.f_globals
|
||||||
|
target_module = target_namespace.get('__name__', None)
|
||||||
|
elif isinstance(target_module, string_types):
|
||||||
|
target_module = importlib.import_module(target_module)
|
||||||
|
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)))
|
||||||
|
|
||||||
|
# Let's do a quick check to make sure the source module isn't actually
|
||||||
|
# the module being compiled (e.g. when `runpy` executes a module's code
|
||||||
|
# in `__main__`).
|
||||||
|
# We use the module's underlying filename for this (when they exist), since
|
||||||
|
# it's the most "fixed" attribute.
|
||||||
|
if _same_modules(source_module, target_module):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not inspect.ismodule(source_module):
|
||||||
|
source_module = importlib.import_module(source_module)
|
||||||
|
|
||||||
|
source_macros = source_module.__dict__.setdefault('__macros__', {})
|
||||||
|
source_tags = source_module.__dict__.setdefault('__tags__', {})
|
||||||
|
|
||||||
|
if len(source_module.__macros__) + len(source_module.__tags__) == 0:
|
||||||
|
if assignments != "ALL":
|
||||||
|
raise ImportError('The module {} has no macros or tags'.format(
|
||||||
|
source_module))
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
target_macros = target_namespace.setdefault('__macros__', {})
|
||||||
|
target_tags = target_namespace.setdefault('__tags__', {})
|
||||||
|
|
||||||
if prefix:
|
if prefix:
|
||||||
prefix += "."
|
prefix += "."
|
||||||
if assignments != "ALL":
|
|
||||||
assignments = {mangle(str_type(k)): v for k, v in assignments}
|
|
||||||
|
|
||||||
for d in _hy_macros, _hy_tag:
|
if assignments == "ALL":
|
||||||
for name, macro in d[source_module].items():
|
# Only add macros/tags created in/by the source module.
|
||||||
seen_names.add(name)
|
name_assigns = [(n, n) for n, f in source_macros.items()
|
||||||
if assignments == "ALL":
|
if inspect.getmodule(f) == source_module]
|
||||||
d[target_module][mangle(prefix + name)] = macro
|
name_assigns += [(n, n) for n, f in source_tags.items()
|
||||||
elif name in assignments:
|
if inspect.getmodule(f) == source_module]
|
||||||
d[target_module][mangle(prefix + assignments[name])] = macro
|
else:
|
||||||
|
# If one specifically requests a macro/tag not created in the source
|
||||||
|
# module, I guess we allow it?
|
||||||
|
name_assigns = assignments
|
||||||
|
|
||||||
if assignments != "ALL":
|
for name, alias in name_assigns:
|
||||||
unseen = frozenset(assignments.keys()).difference(seen_names)
|
_name = mangle(name)
|
||||||
if unseen:
|
alias = mangle(prefix + alias)
|
||||||
raise ImportError("cannot require names: " + repr(list(unseen)))
|
if _name in source_module.__macros__:
|
||||||
|
target_macros[alias] = source_macros[_name]
|
||||||
|
elif _name in source_module.__tags__:
|
||||||
|
target_tags[alias] = source_tags[_name]
|
||||||
|
else:
|
||||||
|
raise ImportError('Could not require name {} from {}'.format(
|
||||||
|
_name, source_module))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def load_macros(module_name):
|
def load_macros(module):
|
||||||
"""Load the hy builtin macros for module `module_name`.
|
"""Load the hy builtin macros for module `module_name`.
|
||||||
|
|
||||||
Modules from `hy.core` can only use the macros from CORE_MACROS.
|
Modules from `hy.core` can only use the macros from CORE_MACROS.
|
||||||
Other modules get the macros from CORE_MACROS and EXTRA_MACROS.
|
Other modules get the macros from CORE_MACROS and EXTRA_MACROS.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for module in CORE_MACROS:
|
builtin_macros = CORE_MACROS
|
||||||
importlib.import_module(module)
|
|
||||||
|
|
||||||
if module_name.startswith("hy.core"):
|
if not module.__name__.startswith("hy.core"):
|
||||||
return
|
builtin_macros += EXTRA_MACROS
|
||||||
|
|
||||||
for module in EXTRA_MACROS:
|
module_macros = module.__dict__.setdefault('__macros__', {})
|
||||||
importlib.import_module(module)
|
module_tags = module.__dict__.setdefault('__tags__', {})
|
||||||
|
|
||||||
|
for builtin_mod_name in builtin_macros:
|
||||||
|
builtin_mod = importlib.import_module(builtin_mod_name)
|
||||||
|
|
||||||
|
# Make sure we don't overwrite macros in the module.
|
||||||
|
if hasattr(builtin_mod, '__macros__'):
|
||||||
|
module_macros.update({k: v
|
||||||
|
for k, v in builtin_mod.__macros__.items()
|
||||||
|
if k not in module_macros})
|
||||||
|
if hasattr(builtin_mod, '__tags__'):
|
||||||
|
module_tags.update({k: v
|
||||||
|
for k, v in builtin_mod.__tags__.items()
|
||||||
|
if k not in module_tags})
|
||||||
|
|
||||||
|
|
||||||
def make_empty_fn_copy(fn):
|
def make_empty_fn_copy(fn):
|
||||||
@ -139,7 +249,7 @@ def make_empty_fn_copy(fn):
|
|||||||
# can continue running. Unfortunately, the error message that might get
|
# can continue running. Unfortunately, the error message that might get
|
||||||
# raised later on while expanding a macro might not make sense at all.
|
# raised later on while expanding a macro might not make sense at all.
|
||||||
|
|
||||||
formatted_args = hy.inspect.format_args(fn)
|
formatted_args = format_args(fn)
|
||||||
fn_str = 'lambda {}: None'.format(
|
fn_str = 'lambda {}: None'.format(
|
||||||
formatted_args.lstrip('(').rstrip(')'))
|
formatted_args.lstrip('(').rstrip(')'))
|
||||||
empty_fn = eval(fn_str)
|
empty_fn = eval(fn_str)
|
||||||
@ -152,14 +262,45 @@ def make_empty_fn_copy(fn):
|
|||||||
return empty_fn
|
return empty_fn
|
||||||
|
|
||||||
|
|
||||||
def macroexpand(tree, compiler, once=False):
|
def macroexpand(tree, module, compiler=None, once=False):
|
||||||
"""Expand the toplevel macros for the `tree`.
|
"""Expand the toplevel macros for the given Hy AST tree.
|
||||||
|
|
||||||
Load the macros from the given `compiler.module_name`, then expand the
|
Load the macros from the given `module`, then expand the (top-level) macros
|
||||||
(top-level) macros in `tree` until we no longer can.
|
in `tree` until we no longer can.
|
||||||
|
|
||||||
|
`HyExpression` resulting from macro expansions are assigned the module in
|
||||||
|
which the macro function is defined (determined using `inspect.getmodule`).
|
||||||
|
If the resulting `HyExpression` is itself macro expanded, then the
|
||||||
|
namespace of the assigned module is checked first for a macro corresponding
|
||||||
|
to the expression's head/car symbol. If the head/car symbol of such a
|
||||||
|
`HyExpression` is not found among the macros of its assigned module's
|
||||||
|
namespace, the outer-most namespace--e.g. the one given by the `module`
|
||||||
|
parameter--is used as a fallback.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tree: HyObject or list
|
||||||
|
Hy AST tree.
|
||||||
|
|
||||||
|
module: str or types.ModuleType
|
||||||
|
Module used to determine the local namespace for macros.
|
||||||
|
|
||||||
|
compiler: HyASTCompiler, optional
|
||||||
|
The compiler object passed to expanded macros.
|
||||||
|
|
||||||
|
once: boolean, optional
|
||||||
|
Only expand the first macro in `tree`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
------
|
||||||
|
out: HyObject
|
||||||
|
Returns a mutated tree with macros expanded.
|
||||||
"""
|
"""
|
||||||
load_macros(compiler.module_name)
|
if not inspect.ismodule(module):
|
||||||
|
module = importlib.import_module(module)
|
||||||
|
|
||||||
|
assert not compiler or compiler.module == module
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
if not isinstance(tree, HyExpression) or tree == []:
|
if not isinstance(tree, HyExpression) or tree == []:
|
||||||
@ -170,24 +311,34 @@ def macroexpand(tree, compiler, once=False):
|
|||||||
break
|
break
|
||||||
|
|
||||||
fn = mangle(fn)
|
fn = mangle(fn)
|
||||||
m = _hy_macros[compiler.module_name].get(fn) or _hy_macros[None].get(fn)
|
expr_modules = (([] if not hasattr(tree, 'module') else [tree.module])
|
||||||
|
+ [module])
|
||||||
|
|
||||||
|
# Choose the first namespace with the macro.
|
||||||
|
m = next((mod.__macros__[fn]
|
||||||
|
for mod in expr_modules
|
||||||
|
if fn in mod.__macros__),
|
||||||
|
None)
|
||||||
if not m:
|
if not m:
|
||||||
break
|
break
|
||||||
|
|
||||||
opts = {}
|
opts = {}
|
||||||
if m._hy_macro_pass_compiler:
|
if m._hy_macro_pass_compiler:
|
||||||
|
if compiler is None:
|
||||||
|
from hy.compiler import HyASTCompiler
|
||||||
|
compiler = HyASTCompiler(module)
|
||||||
opts['compiler'] = compiler
|
opts['compiler'] = compiler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
m_copy = make_empty_fn_copy(m)
|
m_copy = make_empty_fn_copy(m)
|
||||||
m_copy(compiler.module_name, *tree[1:], **opts)
|
m_copy(module.__name__, *tree[1:], **opts)
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
msg = "expanding `" + str(tree[0]) + "': "
|
msg = "expanding `" + str(tree[0]) + "': "
|
||||||
msg += str(e).replace("<lambda>()", "", 1).strip()
|
msg += str(e).replace("<lambda>()", "", 1).strip()
|
||||||
raise HyMacroExpansionError(tree, msg)
|
raise HyMacroExpansionError(tree, msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj = m(compiler.module_name, *tree[1:], **opts)
|
obj = m(module.__name__, *tree[1:], **opts)
|
||||||
except HyTypeError as e:
|
except HyTypeError as e:
|
||||||
if e.expression is None:
|
if e.expression is None:
|
||||||
e.expression = tree
|
e.expression = tree
|
||||||
@ -195,6 +346,10 @@ def macroexpand(tree, compiler, once=False):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = "expanding `" + str(tree[0]) + "': " + repr(e)
|
msg = "expanding `" + str(tree[0]) + "': " + repr(e)
|
||||||
raise HyMacroExpansionError(tree, msg)
|
raise HyMacroExpansionError(tree, msg)
|
||||||
|
|
||||||
|
if isinstance(obj, HyExpression):
|
||||||
|
obj.module = inspect.getmodule(m)
|
||||||
|
|
||||||
tree = replace_hy_obj(obj, tree)
|
tree = replace_hy_obj(obj, tree)
|
||||||
|
|
||||||
if once:
|
if once:
|
||||||
@ -203,25 +358,33 @@ def macroexpand(tree, compiler, once=False):
|
|||||||
tree = wrap_value(tree)
|
tree = wrap_value(tree)
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
def macroexpand_1(tree, compiler):
|
|
||||||
|
def macroexpand_1(tree, module, compiler=None):
|
||||||
"""Expand the toplevel macro from `tree` once, in the context of
|
"""Expand the toplevel macro from `tree` once, in the context of
|
||||||
`compiler`."""
|
`compiler`."""
|
||||||
return macroexpand(tree, compiler, once=True)
|
return macroexpand(tree, module, compiler, once=True)
|
||||||
|
|
||||||
|
|
||||||
def tag_macroexpand(tag, tree, compiler):
|
def tag_macroexpand(tag, tree, module):
|
||||||
"""Expand the tag macro "tag" with argument `tree`."""
|
"""Expand the tag macro `tag` with argument `tree`."""
|
||||||
load_macros(compiler.module_name)
|
if not inspect.ismodule(module):
|
||||||
|
module = importlib.import_module(module)
|
||||||
|
|
||||||
|
expr_modules = (([] if not hasattr(tree, 'module') else [tree.module])
|
||||||
|
+ [module])
|
||||||
|
|
||||||
|
# Choose the first namespace with the macro.
|
||||||
|
tag_macro = next((mod.__tags__[tag]
|
||||||
|
for mod in expr_modules
|
||||||
|
if tag in mod.__tags__),
|
||||||
|
None)
|
||||||
|
|
||||||
tag_macro = _hy_tag[compiler.module_name].get(tag)
|
|
||||||
if tag_macro is None:
|
if tag_macro is None:
|
||||||
try:
|
raise HyTypeError(tag, "'{0}' is not a defined tag macro.".format(tag))
|
||||||
tag_macro = _hy_tag[None][tag]
|
|
||||||
except KeyError:
|
|
||||||
raise HyTypeError(
|
|
||||||
tag,
|
|
||||||
"`{0}' is not a defined tag macro.".format(tag)
|
|
||||||
)
|
|
||||||
|
|
||||||
expr = tag_macro(tree)
|
expr = tag_macro(tree)
|
||||||
|
|
||||||
|
if isinstance(expr, HyExpression):
|
||||||
|
expr.module = inspect.getmodule(tag_macro)
|
||||||
|
|
||||||
return replace_hy_obj(expr, tree)
|
return replace_hy_obj(expr, tree)
|
||||||
|
@ -32,11 +32,12 @@ class HyObject(object):
|
|||||||
Generic Hy Object model. This is helpful to inject things into all the
|
Generic Hy Object model. This is helpful to inject things into all the
|
||||||
Hy lexing Objects at once.
|
Hy lexing Objects at once.
|
||||||
"""
|
"""
|
||||||
|
__properties__ = ["module", "start_line", "end_line", "start_column",
|
||||||
|
"end_column"]
|
||||||
|
|
||||||
def replace(self, other, recursive=False):
|
def replace(self, other, recursive=False):
|
||||||
if isinstance(other, HyObject):
|
if isinstance(other, HyObject):
|
||||||
for attr in ["start_line", "end_line",
|
for attr in self.__properties__:
|
||||||
"start_column", "end_column"]:
|
|
||||||
if not hasattr(self, attr) and hasattr(other, attr):
|
if not hasattr(self, attr) and hasattr(other, attr):
|
||||||
setattr(self, attr, getattr(other, attr))
|
setattr(self, attr, getattr(other, attr))
|
||||||
else:
|
else:
|
||||||
|
@ -56,7 +56,7 @@ def test_runpy():
|
|||||||
|
|
||||||
|
|
||||||
def test_stringer():
|
def test_stringer():
|
||||||
_ast = hy_compile(hy_parse("(defn square [x] (* x x))"), '')
|
_ast = hy_compile(hy_parse("(defn square [x] (* x x))"), '__main__')
|
||||||
|
|
||||||
assert type(_ast.body[0]) == ast.FunctionDef
|
assert type(_ast.body[0]) == ast.FunctionDef
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ def test_import_error_reporting():
|
|||||||
|
|
||||||
def _import_error_test():
|
def _import_error_test():
|
||||||
try:
|
try:
|
||||||
_ = hy_compile(hy_parse("(import \"sys\")"), '')
|
_ = hy_compile(hy_parse("(import \"sys\")"), '__main__')
|
||||||
except HyTypeError:
|
except HyTypeError:
|
||||||
return "Error reported"
|
return "Error reported"
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ def tmac(ETname, *tree):
|
|||||||
def test_preprocessor_simple():
|
def test_preprocessor_simple():
|
||||||
""" Test basic macro expansion """
|
""" Test basic macro expansion """
|
||||||
obj = macroexpand(tokenize('(test "one" "two")')[0],
|
obj = macroexpand(tokenize('(test "one" "two")')[0],
|
||||||
|
__name__,
|
||||||
HyASTCompiler(__name__))
|
HyASTCompiler(__name__))
|
||||||
assert obj == HyList(["one", "two"])
|
assert obj == HyList(["one", "two"])
|
||||||
assert type(obj) == HyList
|
assert type(obj) == HyList
|
||||||
@ -30,6 +31,7 @@ def test_preprocessor_simple():
|
|||||||
def test_preprocessor_expression():
|
def test_preprocessor_expression():
|
||||||
""" Test that macro expansion doesn't recurse"""
|
""" Test that macro expansion doesn't recurse"""
|
||||||
obj = macroexpand(tokenize('(test (test "one" "two"))')[0],
|
obj = macroexpand(tokenize('(test (test "one" "two"))')[0],
|
||||||
|
__name__,
|
||||||
HyASTCompiler(__name__))
|
HyASTCompiler(__name__))
|
||||||
|
|
||||||
assert type(obj) == HyList
|
assert type(obj) == HyList
|
||||||
@ -41,13 +43,13 @@ def test_preprocessor_expression():
|
|||||||
|
|
||||||
obj = HyList([HyString("one"), HyString("two")])
|
obj = HyList([HyString("one"), HyString("two")])
|
||||||
obj = tokenize('(shill ["one" "two"])')[0][1]
|
obj = tokenize('(shill ["one" "two"])')[0][1]
|
||||||
assert obj == macroexpand(obj, HyASTCompiler(""))
|
assert obj == macroexpand(obj, __name__, HyASTCompiler(__name__))
|
||||||
|
|
||||||
|
|
||||||
def test_preprocessor_exceptions():
|
def test_preprocessor_exceptions():
|
||||||
""" Test that macro expansion raises appropriate exceptions"""
|
""" Test that macro expansion raises appropriate exceptions"""
|
||||||
with pytest.raises(HyMacroExpansionError) as excinfo:
|
with pytest.raises(HyMacroExpansionError) as excinfo:
|
||||||
macroexpand(tokenize('(defn)')[0], HyASTCompiler(__name__))
|
macroexpand(tokenize('(defn)')[0], __name__, HyASTCompiler(__name__))
|
||||||
assert "_hy_anon_fn_" not in excinfo.value.message
|
assert "_hy_anon_fn_" not in excinfo.value.message
|
||||||
assert "TypeError" not in excinfo.value.message
|
assert "TypeError" not in excinfo.value.message
|
||||||
|
|
||||||
@ -56,6 +58,6 @@ def test_macroexpand_nan():
|
|||||||
# https://github.com/hylang/hy/issues/1574
|
# https://github.com/hylang/hy/issues/1574
|
||||||
import math
|
import math
|
||||||
NaN = float('nan')
|
NaN = float('nan')
|
||||||
x = macroexpand(HyFloat(NaN), HyASTCompiler(__name__))
|
x = macroexpand(HyFloat(NaN), __name__, HyASTCompiler(__name__))
|
||||||
assert type(x) is HyFloat
|
assert type(x) is HyFloat
|
||||||
assert math.isnan(x)
|
assert math.isnan(x)
|
||||||
|
@ -11,6 +11,7 @@ def test_tag_macro_error():
|
|||||||
"""Check if we get correct error with wrong dispatch character"""
|
"""Check if we get correct error with wrong dispatch character"""
|
||||||
try:
|
try:
|
||||||
macroexpand(tokenize("(dispatch_tag_macro '- '())")[0],
|
macroexpand(tokenize("(dispatch_tag_macro '- '())")[0],
|
||||||
|
__name__,
|
||||||
HyASTCompiler(__name__))
|
HyASTCompiler(__name__))
|
||||||
except HyTypeError as e:
|
except HyTypeError as e:
|
||||||
assert "with the character `-`" in str(e)
|
assert "with the character `-`" in str(e)
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
;; This file is part of Hy, which is free software licensed under the Expat
|
;; This file is part of Hy, which is free software licensed under the Expat
|
||||||
;; license. See the LICENSE.
|
;; license. See the LICENSE.
|
||||||
|
|
||||||
(import [hy.errors [HyTypeError]])
|
(import pytest
|
||||||
|
[hy.errors [HyTypeError]])
|
||||||
|
|
||||||
(defmacro rev [&rest body]
|
(defmacro rev [&rest body]
|
||||||
"Execute the `body` statements in reverse"
|
"Execute the `body` statements in reverse"
|
||||||
@ -329,3 +330,124 @@
|
|||||||
(except [e SystemExit]
|
(except [e SystemExit]
|
||||||
(assert (= (str e) "42"))))
|
(assert (= (str e) "42"))))
|
||||||
(setv --name-- oldname))
|
(setv --name-- oldname))
|
||||||
|
|
||||||
|
(defn test-macro-namespace-resolution []
|
||||||
|
"Confirm that local versions of macro-macro dependencies do not shadow the
|
||||||
|
versions from the macro's own module, but do resolve unbound macro references
|
||||||
|
in expansions."
|
||||||
|
|
||||||
|
;; `nonlocal-test-macro` is a macro used within
|
||||||
|
;; `tests.resources.macro-with-require.test-module-macro`.
|
||||||
|
;; Here, we introduce an equivalently named version in local scope that, when
|
||||||
|
;; used, will expand to a different output string.
|
||||||
|
(defmacro nonlocal-test-macro [x]
|
||||||
|
(print "this is the local version of `nonlocal-test-macro`!"))
|
||||||
|
|
||||||
|
;; Was the above macro created properly?
|
||||||
|
(assert (in "nonlocal_test_macro" __macros__))
|
||||||
|
|
||||||
|
(setv nonlocal-test-macro (get __macros__ "nonlocal_test_macro"))
|
||||||
|
|
||||||
|
(require [tests.resources.macro-with-require [*]])
|
||||||
|
|
||||||
|
;; Make sure our local version wasn't overwritten by a faulty `require` of the
|
||||||
|
;; one in tests.resources.macro-with-require.
|
||||||
|
(assert (= nonlocal-test-macro (get __macros__ "nonlocal_test_macro")))
|
||||||
|
|
||||||
|
(setv module-name-var "tests.native_tests.native_macros.test-macro-namespace-resolution")
|
||||||
|
(assert (= (+ "This macro was created in tests.resources.macros, "
|
||||||
|
"expanded in tests.native_tests.native_macros.test-macro-namespace-resolution "
|
||||||
|
"and passed the value 2.")
|
||||||
|
(test-module-macro 2)))
|
||||||
|
(assert (= (+ "This macro was created in tests.resources.macros, "
|
||||||
|
"expanded in tests.native_tests.native_macros.test-macro-namespace-resolution "
|
||||||
|
"and passed the value 2.")
|
||||||
|
#test-module-tag 2))
|
||||||
|
|
||||||
|
;; Now, let's use a `require`d macro that depends on another macro defined only
|
||||||
|
;; in this scope.
|
||||||
|
(defmacro local-test-macro [x]
|
||||||
|
(.format "This is the local version of `nonlocal-test-macro` returning {}!" x))
|
||||||
|
|
||||||
|
(assert (= "This is the local version of `nonlocal-test-macro` returning 3!"
|
||||||
|
(test-module-macro-2 3)))
|
||||||
|
(assert (= "This is the local version of `nonlocal-test-macro` returning 3!"
|
||||||
|
#test-module-tag-2 3)))
|
||||||
|
|
||||||
|
(defn test-macro-from-module []
|
||||||
|
"Macros loaded from an external module, which itself `require`s macros, should
|
||||||
|
work without having to `require` the module's macro dependencies (due to
|
||||||
|
[minimal] macro namespace resolution).
|
||||||
|
|
||||||
|
In doing so we also confirm that a module's `__macros__` attribute is correctly
|
||||||
|
loaded and used.
|
||||||
|
|
||||||
|
Additionally, we confirm that `require` statements are executed via loaded bytecode."
|
||||||
|
|
||||||
|
(import os sys marshal types)
|
||||||
|
(import [hy.importer [cache-from-source]])
|
||||||
|
|
||||||
|
(setv pyc-file (cache-from-source
|
||||||
|
(os.path.realpath
|
||||||
|
(os.path.join
|
||||||
|
"tests" "resources" "macro_with_require.hy"))))
|
||||||
|
|
||||||
|
;; Remove any cached byte-code, so that this runs from source and
|
||||||
|
;; gets evaluated in this module.
|
||||||
|
(when (os.path.isfile pyc-file)
|
||||||
|
(os.unlink pyc-file)
|
||||||
|
(.clear sys.path_importer_cache)
|
||||||
|
(when (in "tests.resources.macro_with_require" sys.modules)
|
||||||
|
(del (get sys.modules "tests.resources.macro_with_require"))
|
||||||
|
(__macros__.clear)
|
||||||
|
(__tags__.clear)))
|
||||||
|
|
||||||
|
;; Ensure that bytecode isn't present when we require this module.
|
||||||
|
(assert (not (os.path.isfile pyc-file)))
|
||||||
|
|
||||||
|
(defn test-requires-and-macros []
|
||||||
|
(require [tests.resources.macro-with-require
|
||||||
|
[test-module-macro]])
|
||||||
|
|
||||||
|
;; Make sure that `require` didn't add any of its `require`s
|
||||||
|
(assert (not (in "nonlocal-test-macro" __macros__)))
|
||||||
|
;; and that it didn't add its tags.
|
||||||
|
(assert (not (in "test_module_tag" __tags__)))
|
||||||
|
|
||||||
|
;; Now, require everything.
|
||||||
|
(require [tests.resources.macro-with-require [*]])
|
||||||
|
|
||||||
|
;; Again, make sure it didn't add its required macros and/or tags.
|
||||||
|
(assert (not (in "nonlocal-test-macro" __macros__)))
|
||||||
|
|
||||||
|
;; Its tag(s) should be here now.
|
||||||
|
(assert (in "test_module_tag" __tags__))
|
||||||
|
|
||||||
|
;; The test macro expands to include this symbol.
|
||||||
|
(setv module-name-var "tests.native_tests.native_macros")
|
||||||
|
(assert (= (+ "This macro was created in tests.resources.macros, "
|
||||||
|
"expanded in tests.native_tests.native_macros "
|
||||||
|
"and passed the value 1.")
|
||||||
|
(test-module-macro 1)))
|
||||||
|
|
||||||
|
(assert (= (+ "This macro was created in tests.resources.macros, "
|
||||||
|
"expanded in tests.native_tests.native_macros "
|
||||||
|
"and passed the value 1.")
|
||||||
|
#test-module-tag 1)))
|
||||||
|
|
||||||
|
(test-requires-and-macros)
|
||||||
|
|
||||||
|
;; Now that bytecode is present, reload the module, clear the `require`d
|
||||||
|
;; macros and tags, and rerun the tests.
|
||||||
|
(assert (os.path.isfile pyc-file))
|
||||||
|
|
||||||
|
;; Reload the module and clear the local macro context.
|
||||||
|
(.clear sys.path_importer_cache)
|
||||||
|
(del (get sys.modules "tests.resources.macro_with_require"))
|
||||||
|
(.clear __macros__)
|
||||||
|
(.clear __tags__)
|
||||||
|
|
||||||
|
;; XXX: There doesn't seem to be a way--via standard import mechanisms--to
|
||||||
|
;; ensure that an imported module used the cached bytecode. We'll simply have
|
||||||
|
;; to trust that the .pyc loading convention was followed.
|
||||||
|
(test-requires-and-macros))
|
||||||
|
8
tests/resources/bin/circular_macro_require.hy
Normal file
8
tests/resources/bin/circular_macro_require.hy
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
(defmacro bar [expr]
|
||||||
|
`(print ~expr))
|
||||||
|
|
||||||
|
(defmacro foo [expr]
|
||||||
|
`(do (require [tests.resources.bin.circular-macro-require [bar]])
|
||||||
|
(bar ~expr)))
|
||||||
|
|
||||||
|
(foo 42)
|
3
tests/resources/bin/require_and_eval.hy
Normal file
3
tests/resources/bin/require_and_eval.hy
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
(require [hy.extra.anaphoric [ap-if]])
|
||||||
|
|
||||||
|
(print (eval '(ap-if (+ "a" "b") (+ it "c"))))
|
25
tests/resources/macro_with_require.hy
Normal file
25
tests/resources/macro_with_require.hy
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
;; Require all the macros and make sure they don't pollute namespaces/modules
|
||||||
|
;; that require `*` from this.
|
||||||
|
(require [tests.resources.macros [*]])
|
||||||
|
|
||||||
|
(defmacro test-module-macro [a]
|
||||||
|
"The variable `macro-level-var' here should not bind to the same-named symbol
|
||||||
|
in the expansion of `nonlocal-test-macro'."
|
||||||
|
(setv macro-level-var "tests.resources.macros.macro-with-require")
|
||||||
|
`(nonlocal-test-macro ~a))
|
||||||
|
|
||||||
|
(deftag test-module-tag [a]
|
||||||
|
"The variable `macro-level-var' here should not bind to the same-named symbol
|
||||||
|
in the expansion of `nonlocal-test-macro'."
|
||||||
|
(setv macro-level-var "tests.resources.macros.macro-with-require")
|
||||||
|
`(nonlocal-test-macro ~a))
|
||||||
|
|
||||||
|
(defmacro test-module-macro-2 [a]
|
||||||
|
"The macro `local-test-macro` isn't in this module's namespace, so it better
|
||||||
|
be in the expansion's!"
|
||||||
|
`(local-test-macro ~a))
|
||||||
|
|
||||||
|
(deftag test-module-tag-2 [a]
|
||||||
|
"The macro `local-test-macro` isn't in this module's namespace, so it better
|
||||||
|
be in the expansion's!"
|
||||||
|
`(local-test-macro ~a))
|
@ -1,3 +1,5 @@
|
|||||||
|
(setv module-name-var "tests.resources.macros")
|
||||||
|
|
||||||
(defmacro thread-set-ab []
|
(defmacro thread-set-ab []
|
||||||
(defn f [&rest args] (.join "" (+ (, "a") args)))
|
(defn f [&rest args] (.join "" (+ (, "a") args)))
|
||||||
(setv variable (HySymbol (-> "b" (f))))
|
(setv variable (HySymbol (-> "b" (f))))
|
||||||
@ -10,3 +12,11 @@
|
|||||||
|
|
||||||
(defmacro test-macro []
|
(defmacro test-macro []
|
||||||
'(setv blah 1))
|
'(setv blah 1))
|
||||||
|
|
||||||
|
(defmacro nonlocal-test-macro [x]
|
||||||
|
"When called from `macro-with-require`'s macro(s), the first instance of
|
||||||
|
`module-name-var` should resolve to the value in the module where this is
|
||||||
|
defined, then the expansion namespace/module"
|
||||||
|
`(.format (+ "This macro was created in {}, expanded in {} "
|
||||||
|
"and passed the value {}.")
|
||||||
|
~module-name-var module-name-var ~x))
|
@ -388,3 +388,38 @@ def test_bin_hy_file_no_extension():
|
|||||||
"""Confirm that a file with no extension is processed as Hy source"""
|
"""Confirm that a file with no extension is processed as Hy source"""
|
||||||
output, _ = run_cmd("hy tests/resources/no_extension")
|
output, _ = run_cmd("hy tests/resources/no_extension")
|
||||||
assert "This Should Still Work" in output
|
assert "This Should Still Work" in output
|
||||||
|
|
||||||
|
|
||||||
|
def test_bin_hy_circular_macro_require():
|
||||||
|
"""Confirm that macros can require themselves during expansion and when
|
||||||
|
run from the command line."""
|
||||||
|
|
||||||
|
# First, with no bytecode
|
||||||
|
test_file = "tests/resources/bin/circular_macro_require.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 "42" == output.strip()
|
||||||
|
|
||||||
|
# Now, with bytecode
|
||||||
|
assert os.path.exists(cache_from_source(test_file))
|
||||||
|
output, _ = run_cmd("hy {}".format(test_file))
|
||||||
|
assert "42" == output.strip()
|
||||||
|
|
||||||
|
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()
|
||||||
|
Loading…
Reference in New Issue
Block a user