import argparse
import ast
import builtins
import code
import codeop
import hashlib
import importlib
import io
import linecache
import os
import py_compile
import runpy
import sys
import time
import traceback
import types
from contextlib import contextmanager
import hy
from hy._compat import PY3_9
from hy.compiler import HyASTCompiler, hy_ast_compile_flags, hy_compile, hy_eval
from hy.completer import Completer, completion
from hy.errors import (
HyLanguageError,
HyMacroExpansionError,
HyRequireError,
filtered_hy_exceptions,
hy_exc_handler,
)
from hy.importer import HyLoader, runhy
from hy.macros import enable_readers, require, require_reader
from hy.reader import mangle, read_many
from hy.reader.exceptions import PrematureEndOfInput
from hy.reader.hy_reader import HyReader
sys.last_type = None
sys.last_value = None
sys.last_traceback = None
class HyQuitter:
def __init__(self, name):
self.name = name
def __repr__(self):
return "Use (%s) or Ctrl-D (i.e. EOF) to exit" % (self.name)
__str__ = __repr__
def __call__(self, code=None):
try:
sys.stdin.close()
except:
pass
raise SystemExit(code)
class HyHelper:
def __repr__(self):
return (
"Use (help) for interactive help, or (help object) for help "
"about object."
)
def __call__(self, *args, **kwds):
import pydoc
return pydoc.help(*args, **kwds)
@contextmanager
def extend_linecache(add_cmdline_cache):
_linecache_checkcache = linecache.checkcache
def _cmdline_checkcache(*args):
_linecache_checkcache(*args)
linecache.cache.update(add_cmdline_cache)
linecache.checkcache = _cmdline_checkcache
yield
linecache.checkcache = _linecache_checkcache
_codeop_maybe_compile = codeop._maybe_compile
def _hy_maybe_compile(compiler, source, filename, symbol):
"""The `codeop` version of this will compile the same source multiple
times, and, since we have macros and things like `eval-and-compile`, we
can't allow that.
"""
if not isinstance(compiler, HyCompile):
return _codeop_maybe_compile(compiler, source, filename, symbol)
for line in source.split("\n"):
line = line.strip()
if line and line[0] != ";":
# Leave it alone (could do more with Hy syntax)
break
else:
if symbol != "eval":
# Replace it with a 'pass' statement (i.e. tell the compiler to do
# nothing)
source = "pass"
return compiler(source, filename, symbol)
codeop._maybe_compile = _hy_maybe_compile
class HyCompile(codeop.Compile):
"""This compiler uses `linecache` like
`IPython.core.compilerop.CachingCompiler`.
"""
def __init__(
self, module, locals, ast_callback=None, hy_compiler=None, cmdline_cache={}
):
self.module = module
self.locals = locals
self.ast_callback = ast_callback
self.hy_compiler = hy_compiler
self.reader = HyReader()
super().__init__()
if hasattr(self.module, "__reader_macros__"):
enable_readers(
self.module, self.reader, self.module.__reader_macros__.keys()
)
self.flags |= hy_ast_compile_flags
self.cmdline_cache = cmdline_cache
def _cache(self, source, name):
entry = (
len(source),
time.time(),
[line + "\n" for line in source.splitlines()],
name,
)
linecache.cache[name] = entry
self.cmdline_cache[name] = entry
def _update_exc_info(self):
self.locals["_hy_last_type"] = sys.last_type
self.locals["_hy_last_value"] = sys.last_value
# Skip our frame.
sys.last_traceback = getattr(sys.last_traceback, "tb_next", sys.last_traceback)
self.locals["_hy_last_traceback"] = sys.last_traceback
def __call__(self, source, filename="<input>", symbol="single"):
if source == "pass":
# We need to return a no-op to signal that no more input is needed.
return (compile(source, filename, symbol),) * 2
hash_digest = hashlib.sha1(source.encode("utf-8").strip()).hexdigest()
name = "{}-{}".format(filename.strip("<>"), hash_digest)
self._cache(source, name)
try:
root_ast = ast.Interactive if symbol == "single" else ast.Module
# Our compiler doesn't correspond to a real, fixed source file, so
# we need to [re]set these.
self.hy_compiler.filename = name
self.hy_compiler.source = source
hy_ast = read_many(
source, filename=name, reader=self.reader, skip_shebang=True
)
exec_ast, eval_ast = hy_compile(
hy_ast,
self.module,
root=root_ast,
get_expr=True,
compiler=self.hy_compiler,
filename=name,
source=source,
import_stdlib=False,
)
if self.ast_callback:
self.ast_callback(exec_ast, eval_ast)
exec_code = super().__call__(exec_ast, name, symbol)
eval_code = super().__call__(eval_ast, name, "eval")
except Exception as e:
# Capture and save the error before we handle further
sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
self._update_exc_info()
if isinstance(e, (PrematureEndOfInput, SyntaxError)):
raise
else:
# Hy will raise exceptions during compile-time that Python would
# raise during run-time (e.g. import errors for `require`). In
# order to work gracefully with the Python world, we convert such
# Hy errors to code that purposefully reraises those exceptions in
# the places where Python code expects them.
# Capture a traceback without the compiler/REPL frames.
exec_code = super(HyCompile, self).__call__(
"raise _hy_last_value.with_traceback(_hy_last_traceback)",
name,
symbol,
)
eval_code = super(HyCompile, self).__call__("None", name, "eval")
return exec_code, eval_code
class HyCommandCompiler(codeop.CommandCompiler):
def __init__(self, *args, **kwargs):
self.compiler = HyCompile(*args, **kwargs)
def __call__(self, *args, **kwargs):
try:
return super().__call__(*args, **kwargs)
except PrematureEndOfInput:
# We have to do this here, because `codeop._maybe_compile` won't
# take `None` for a return value (at least not in Python 2.7) and
# this exception type is also a `SyntaxError`, so it will be caught
# by `code.InteractiveConsole` base methods before it reaches our
# `runsource`.
return None
[docs]class HyREPL(code.InteractiveConsole):
"A subclass of :class:`code.InteractiveConsole` for Hy."
def __init__(self, spy=False, output_fn=None, locals=None, filename="<stdin>"):
# Create a proper module for this REPL so that we can obtain it easily
# (e.g. using `importlib.import_module`).
# We let `InteractiveConsole` initialize `self.locals` when it's
# `None`.
super().__init__(locals=locals, filename=filename)
module_name = self.locals.get("__name__", "__console__")
# Make sure our newly created module is properly introduced to
# `sys.modules`, and consistently use its namespace as `self.locals`
# from here on.
self.module = sys.modules.setdefault(module_name, types.ModuleType(module_name))
self.module.__dict__.update(self.locals)
self.locals = self.module.__dict__
if os.environ.get("HYSTARTUP"):
try:
loader = HyLoader("__hystartup__", os.environ.get("HYSTARTUP"))
spec = importlib.util.spec_from_loader(loader.name, loader)
mod = importlib.util.module_from_spec(spec)
sys.modules.setdefault(mod.__name__, mod)
loader.exec_module(mod)
imports = mod.__dict__.get(
"__all__",
[name for name in mod.__dict__ if not name.startswith("_")],
)
imports = {name: mod.__dict__[name] for name in imports}
spy = spy or imports.get("repl_spy")
output_fn = output_fn or imports.get("repl_output_fn")
# Load imports and defs
self.locals.update(imports)
# load module macros
require(mod, self.module, assignments="ALL")
require_reader(mod, self.module, assignments="ALL")
except Exception as e:
print(e)
self.hy_compiler = HyASTCompiler(self.module, module_name)
self.cmdline_cache = {}
self.compile = HyCommandCompiler(
self.module,
self.locals,
ast_callback=self.ast_callback,
hy_compiler=self.hy_compiler,
cmdline_cache=self.cmdline_cache,
)
self.spy = spy
self.last_value = None
self.print_last_value = True
if output_fn is None:
self.output_fn = hy.repr
elif callable(output_fn):
self.output_fn = output_fn
elif "." in output_fn:
parts = [mangle(x) for x in output_fn.split(".")]
module, f = ".".join(parts[:-1]), parts[-1]
self.output_fn = getattr(importlib.import_module(module), f)
else:
self.output_fn = getattr(builtins, mangle(output_fn))
# Pre-mangle symbols for repl recent results: *1, *2, *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})
# Allow access to the running REPL instance
self.locals["_hy_repl"] = self
# Compile an empty statement to load the standard prelude
exec_ast = hy_compile(
read_many(""), self.module, compiler=self.hy_compiler, import_stdlib=True
)
if self.ast_callback:
self.ast_callback(exec_ast, None)
def ast_callback(self, exec_ast, eval_ast):
if self.spy:
try:
# Mush the two AST chunks into a single module for
# conversion into Python.
new_ast = ast.Module(
exec_ast.body
+ ([] if eval_ast is None else [ast.Expr(eval_ast.body)]),
type_ignores=[],
)
print(ast.unparse(new_ast))
except Exception:
msg = "Exception in AST callback:\n{}\n".format(traceback.format_exc())
self.write(msg)
def _error_wrap(self, error_fn, exc_info_override=False, *args, **kwargs):
sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
if exc_info_override:
# Use a traceback that doesn't have the REPL frames.
sys.last_type = self.locals.get("_hy_last_type", sys.last_type)
sys.last_value = self.locals.get("_hy_last_value", sys.last_value)
sys.last_traceback = self.locals.get(
"_hy_last_traceback", sys.last_traceback
)
sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback)
self.locals[mangle("*e")] = sys.last_value
def showsyntaxerror(self, filename=None):
if filename is None:
filename = self.filename
self.print_last_value = False
self._error_wrap(
super().showsyntaxerror, exc_info_override=True, filename=filename
)
def showtraceback(self):
self._error_wrap(super().showtraceback)
def runcode(self, code):
try:
eval(code[0], self.locals)
self.last_value = eval(code[1], self.locals)
# Don't print `None` values.
self.print_last_value = self.last_value is not None
except SystemExit:
raise
except Exception as e:
# Set this to avoid a print-out of the last value on errors.
self.print_last_value = False
self.showtraceback()
def runsource(self, source, filename="<stdin>", symbol="exec"):
try:
res = super().runsource(source, filename, symbol)
except (HyMacroExpansionError, HyRequireError):
# We need to handle these exceptions ourselves, because the base
# method only handles `OverflowError`, `SyntaxError` and
# `ValueError`.
self.showsyntaxerror(filename)
return False
except (HyLanguageError):
# Our compiler will also raise `TypeError`s
self.showtraceback()
return False
# Shift exisitng REPL results
if not res:
next_result = self.last_value
for sym in self._repl_results_symbols:
self.locals[sym], next_result = next_result, self.locals[sym]
# Print the value.
if self.print_last_value:
try:
output = self.output_fn(self.last_value)
except Exception:
self.showtraceback()
return False
print(output)
return res
[docs] def run(self):
"Start running the REPL. Return 0 when done."
import platform
import colorama
sys.ps1 = "=> "
sys.ps2 = "... "
builtins.quit = HyQuitter("quit")
builtins.exit = HyQuitter("exit")
builtins.help = HyHelper()
colorama.init()
namespace = self.locals
with filtered_hy_exceptions(), extend_linecache(self.cmdline_cache), completion(
Completer(namespace)
):
self.interact(
"Hy {version} using "
"{py}({build}) {pyversion} on {os}".format(
version=hy.__version__,
py=platform.python_implementation(),
build=platform.python_build()[0],
pyversion=platform.python_version(),
os=platform.system(),
)
)
return 0
def set_path(filename):
"""Emulate Python cmdline behavior by setting `sys.path` relative
to the executed file's location."""
path = os.path.realpath(os.path.dirname(filename))
if sys.path[0] == "":
sys.path[0] = path
else:
sys.path.insert(0, path)
def run_command(source, filename=None):
__main__ = importlib.import_module("__main__")
require("hy.cmdline", __main__, assignments="ALL")
with filtered_hy_exceptions():
try:
hy_eval(
read_many(source, filename=filename, skip_shebang=True),
__main__.__dict__,
__main__,
filename=filename,
source=source,
)
except HyLanguageError:
hy_exc_handler(*sys.exc_info())
return 1
return 0
def run_icommand(source, **kwargs):
if os.path.exists(source):
filename = source
set_path(source)
with open(source, "r", encoding="utf-8") as f:
source = f.read()
else:
filename = "<string>"
hr = HyREPL(**kwargs)
with filtered_hy_exceptions():
res = hr.runsource(source, filename=filename)
# If the command was prematurely ended, show an error (just like Python
# does).
if res:
hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback)
return hr.run()
USAGE = "hy [-h | -v | -i CMD | -c CMD | -m MODULE | FILE | -] [ARG]..."
VERSION = "hy " + hy.__version__
EPILOG = """
FILE
program read from script
-
program read from stdin
[ARG]...
arguments passed to program in sys.argv[1:]
"""
class HyArgError(Exception):
pass
def cmdline_handler(scriptname, argv):
# We need to terminate interpretation of options after certain
# options, such as `-c`. So, we can't use `argparse`.
defs = [
dict(
name=["-B"],
action="store_true",
help="don't write .py[co] files on import; also PYTHONDONTWRITEBYTECODE=x",
),
dict(
name=["-c"],
dest="command",
terminate=True,
help="program passed in as string",
),
dict(
name=["-E"],
action="store_true",
help="ignore PYTHON* environment variables (such as PYTHONPATH)",
),
dict(
name=["-h", "--help"],
action="help",
help="print this help message and exit",
),
dict(
name=["-i"],
dest="icommand",
terminate=True,
help="program passed in as string, then stay in REPL",
),
dict(
name=["-m"],
dest="mod",
terminate=True,
help="run library module as a script",
),
dict(
name=["--repl-output-fn"],
dest="repl_output_fn",
help="function for printing REPL output (e.g., repr)",
),
dict(
name=["--spy"],
action="store_true",
help="print equivalent Python code before executing",
),
dict(
name=["-u", "--unbuffered"],
action="store_true",
help="force the stdout and stderr streams to be unbuffered; this option has no effect on stdin; also PYTHONUNBUFFERED=x",
),
dict(
name=["-v", "--version"],
action="version",
help="print the Hy version number and exit",
),
]
# Get the path of the Hy cmdline executable and swap it with
# `sys.executable` (saving the original, just in case).
# The `__main__` module will also have `__file__` set to the
# entry-point script. Currently, I don't see an immediate problem, but
# that's not how the Python cmdline works.
hy.executable = argv[0]
hy.sys_executable = sys.executable
sys.executable = hy.executable
program = argv[0]
argv = list(argv[1:])
options = {}
def err(fmt, *args):
raise HyArgError("hy: " + fmt.format(*args))
def proc_opt(opt, arg=None, item=None, i=None):
matches = [o for o in defs if opt in o["name"]]
if not matches:
err("unrecognized option: {}", opt)
[match] = matches
if "dest" in match:
if arg:
pass
elif i is not None and i + 1 < len(item):
arg = item[i + 1 + (item[i + 1] == "=") :]
elif argv:
arg = argv.pop(0)
else:
err("option {}: expected one argument", opt)
options[match["dest"]] = arg
else:
options[match["name"][-1].lstrip("-")] = True
if "terminate" in match:
return "terminate"
return "dest" in match
# Collect options.
while argv:
item = argv.pop(0)
if item == "--":
break
elif item.startswith("--"):
# One double-hyphen option.
opt, _, arg = item.partition("=")
if proc_opt(opt, arg=arg) == "terminate":
break
elif item.startswith("-") and item != "-":
# One or more single-hyphen options.
for i in range(1, len(item)):
x = proc_opt("-" + item[i], item=item, i=i)
if x:
break
if x == "terminate":
break
else:
# We're done with options. Add the item back.
argv.insert(0, item)
break
if "E" in options:
_remove_python_envs()
if "B" in options:
sys.dont_write_bytecode = True
if "unbuffered" in options:
for k in "stdout", "stderr":
setattr(
sys,
k,
io.TextIOWrapper(
open(getattr(sys, k).fileno(), "wb", 0), write_through=True
),
)
if "help" in options:
print("usage:", USAGE)
print("")
print("optional arguments:")
for o in defs:
print(
", ".join(o["name"]) + ("=" + o["dest"].upper() if "dest" in o else "")
)
print(
" "
+ o["help"]
+ (" (terminates option list)" if o.get("terminate") else "")
)
print(EPILOG)
return 0
if "version" in options:
print(VERSION)
return 0
if "command" in options:
sys.argv = ["-c"] + argv
return run_command(options["command"], filename="<string>")
if "mod" in options:
set_path("")
sys.argv = [program] + argv
runpy.run_module(hy.mangle(options["mod"]), run_name="__main__", alter_sys=True)
return 0
if "icommand" in options:
return run_icommand(
options["icommand"],
spy=options.get("spy"),
output_fn=options.get("repl_output_fn"),
)
if argv:
if argv[0] == "-":
# Read the program from stdin
return run_command(sys.stdin.read(), filename="<stdin>")
else:
# User did "hy <filename>"
filename = argv[0]
set_path(filename)
try:
sys.argv = argv
with filtered_hy_exceptions():
runhy.run_path(filename, run_name="__main__")
return 0
except FileNotFoundError as e:
print(
"hy: Can't open file '{}': [Errno {}] {}".format(
e.filename, e.errno, e.strerror
),
file=sys.stderr,
)
sys.exit(e.errno)
except HyLanguageError:
hy_exc_handler(*sys.exc_info())
sys.exit(1)
return HyREPL(spy=options.get("spy"), output_fn=options.get("repl_output_fn")).run()
# entry point for cmd line script "hy"
def hy_main():
sys.path.insert(0, "")
try:
sys.exit(cmdline_handler("hy", sys.argv))
except HyArgError as e:
print(e)
exit(1)
def hyc_main():
parser = argparse.ArgumentParser(prog="hyc")
parser.add_argument(
"files",
metavar="FILE",
nargs="*",
help=("File(s) to compile (use STDIN if only" ' "-" or nothing is provided)'),
)
parser.add_argument("-v", action="version", version=VERSION)
options = parser.parse_args(sys.argv[1:])
rv = 0
if len(options.files) == 0 or (len(options.files) == 1 and options.files[0] == "-"):
while True:
filename = sys.stdin.readline()
if not filename:
break
filename = filename.rstrip("\n")
set_path(filename)
try:
py_compile.compile(filename, doraise=True)
except py_compile.PyCompileError as error:
rv = 1
sys.stderr.write("%s\n" % error.msg)
except OSError as error:
rv = 1
sys.stderr.write("%s\n" % error)
sys.path.pop(0)
else:
for filename in options.files:
set_path(filename)
try:
print("Compiling %s" % filename)
py_compile.compile(filename, doraise=True)
except py_compile.PyCompileError as error:
# return value to indicate at least one failure
rv = 1
sys.stderr.write("%s\n" % error.msg)
sys.path.pop(0)
return rv
# entry point for cmd line script "hy2py"
def hy2py_main():
options = dict(
prog="hy2py",
usage="%(prog)s [options] [FILE]",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser = argparse.ArgumentParser(**options)
parser.add_argument(
"FILE",
type=str,
nargs="?",
help='Input Hy code (use STDIN if "-" or ' "not provided)",
)
parser.add_argument(
"--with-source",
"-s",
action="store_true",
help="Show the parsed source structure",
)
parser.add_argument(
"--with-ast", "-a", action="store_true", help="Show the generated AST"
)
parser.add_argument(
"--without-python",
"-np",
action="store_true",
help=("Do not show the Python code generated " "from the AST"),
)
options = parser.parse_args(sys.argv[1:])
if options.FILE is None or options.FILE == "-":
sys.path.insert(0, "")
filename = "<stdin>"
source = sys.stdin.read()
else:
filename = options.FILE
set_path(filename)
with open(options.FILE, "r", encoding="utf-8") as source_file:
source = source_file.read()
def printing_source(hst):
for node in hst:
if options.with_source:
print(node)
yield node
hst = hy.models.Lazy(
printing_source(read_many(source, filename, skip_shebang=True))
)
hst.source = source
hst.filename = filename
with filtered_hy_exceptions():
_ast = hy_compile(hst, "__main__", filename=filename, source=source)
if options.with_source:
print()
print()
if options.with_ast:
print(ast.dump(_ast, **(dict(indent=2) if PY3_9 else {})))
print()
print()
if not options.without_python:
print(ast.unparse(_ast))
parser.exit(0)
# remove PYTHON* environment variables,
# such as "PYTHONPATH"
def _remove_python_envs():
for key in list(os.environ.keys()):
if key.startswith("PYTHON"):
os.environ.pop(key)