Merge pull request #1678 from brandonwillard/run-ambiguous-files-as-hy

Allow `runpy` to consider non-standard Hy source extensions
This commit is contained in:
Kodi Arfer 2018-09-24 16:36:36 -04:00 committed by GitHub
commit 1707f602f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 73 additions and 28 deletions

View File

@ -19,7 +19,7 @@ import astor.code_gen
import hy import hy
from hy.lex import LexException, PrematureEndOfInput, mangle from hy.lex import LexException, PrematureEndOfInput, mangle
from hy.compiler import HyTypeError, hy_compile from hy.compiler import HyTypeError, hy_compile
from hy.importer import hy_eval, hy_parse from hy.importer import hy_eval, hy_parse, runhy
from hy.completer import completion, Completer from hy.completer import completion, Completer
from hy.macros import macro, require from hy.macros import macro, require
from hy.models import HyExpression, HyString, HySymbol from hy.models import HyExpression, HyString, HySymbol
@ -352,7 +352,7 @@ def cmdline_handler(scriptname, argv):
try: try:
sys.argv = options.args sys.argv = options.args
runpy.run_path(filename, run_name='__main__') runhy.run_path(filename, run_name='__main__')
return 0 return 0
except FileNotFoundError as e: except FileNotFoundError as e:
print("hy: Can't open file '{0}': [Errno {1}] {2}".format( print("hy: Can't open file '{0}': [Errno {1}] {2}".format(

View File

@ -11,12 +11,13 @@ import inspect
import pkgutil import pkgutil
import re import re
import io import io
import runpy
import types import types
import tempfile import tempfile
import importlib import importlib
import __future__ import __future__
from functools import partial
from hy.errors import HyTypeError from hy.errors import HyTypeError
from hy.compiler import hy_compile from hy.compiler import hy_compile
from hy.lex import tokenize, LexException from hy.lex import tokenize, LexException
@ -166,46 +167,62 @@ 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))
def _get_code_from_file(run_name, fname=None): def _hy_code_from_file(filename, loader_type=None):
"""A patch of `runpy._get_code_from_file` that will also compile Hy """Use PEP-302 loader to produce code for a given Hy source file."""
code. full_fname = os.path.abspath(filename)
This version will read and cache bytecode for Hy files. It operates
normally otherwise.
"""
if fname is None and run_name is not None:
fname = run_name
if fname.endswith('.hy'):
full_fname = os.path.abspath(fname)
fname_path, fname_file = os.path.split(full_fname) fname_path, fname_file = os.path.split(full_fname)
modname = os.path.splitext(fname_file)[0] modname = os.path.splitext(fname_file)[0]
sys.path.insert(0, fname_path) sys.path.insert(0, fname_path)
try: try:
if loader_type is None:
loader = pkgutil.get_loader(modname) loader = pkgutil.get_loader(modname)
else:
loader = loader_type(modname, full_fname)
code = loader.get_code(modname) code = loader.get_code(modname)
finally: finally:
sys.path.pop(0) sys.path.pop(0)
else:
return code
def _get_code_from_file(run_name, fname=None,
hy_src_check=lambda x: x.endswith('.hy')):
"""A patch of `runpy._get_code_from_file` that will also run and cache Hy
code.
"""
if fname is None and run_name is not None:
fname = run_name
# Check for bytecode first. (This is what the `runpy` version does!)
with open(fname, "rb") as f: with open(fname, "rb") as f:
code = pkgutil.read_code(f) code = pkgutil.read_code(f)
if code is None: if code is None:
if hy_src_check(fname):
code = _hy_code_from_file(fname, loader_type=HyLoader)
else:
# Try normal source
with open(fname, "rb") as f: with open(fname, "rb") as f:
# This code differs from `runpy`'s only in that we
# force decoding into UTF-8.
source = f.read().decode('utf-8') source = f.read().decode('utf-8')
code = compile(source, fname, 'exec') code = compile(source, fname, 'exec')
return (code, fname) if PY3 else code return (code, fname) if PY3 else code
_runpy_get_code_from_file = runpy._get_code_from_file
runpy._get_code_from_file = _get_code_from_file
if PY3: if PY3:
importlib.machinery.SOURCE_SUFFIXES.insert(0, '.hy') importlib.machinery.SOURCE_SUFFIXES.insert(0, '.hy')
_py_source_to_code = importlib.machinery.SourceFileLoader.source_to_code _py_source_to_code = importlib.machinery.SourceFileLoader.source_to_code
def _could_be_hy_src(filename):
return (os.path.isfile(filename) and
(filename.endswith('.hy') or
not any(filename.endswith(ext)
for ext in importlib.machinery.SOURCE_SUFFIXES[1:])))
def _hy_source_to_code(self, data, path, _optimize=-1): def _hy_source_to_code(self, data, path, _optimize=-1):
if os.path.isfile(path) and path.endswith('.hy'): if _could_be_hy_src(path):
source = data.decode("utf-8") source = data.decode("utf-8")
try: try:
hy_tree = hy_parse(source) hy_tree = hy_parse(source)
@ -242,12 +259,17 @@ else:
from pkgutil import ImpImporter, ImpLoader from pkgutil import ImpImporter, ImpLoader
def _could_be_hy_src(filename):
return (filename.endswith('.hy') or
(os.path.isfile(filename) and
not any(filename.endswith(s[0]) for s in imp.get_suffixes())))
class HyLoader(ImpLoader, object): class HyLoader(ImpLoader, object):
def __init__(self, fullname, filename, fileobj=None, etc=None): def __init__(self, fullname, filename, fileobj=None, etc=None):
"""This constructor is designed for some compatibility with """This constructor is designed for some compatibility with
SourceFileLoader.""" SourceFileLoader."""
if etc is None and filename is not None: if etc is None and filename is not None:
if filename.endswith('.hy'): if _could_be_hy_src(filename):
etc = ('.hy', 'U', imp.PY_SOURCE) etc = ('.hy', 'U', imp.PY_SOURCE)
if fileobj is None: if fileobj is None:
fileobj = io.open(filename, 'rU', encoding='utf-8') fileobj = io.open(filename, 'rU', encoding='utf-8')
@ -477,7 +499,7 @@ else:
try: try:
flags = None flags = None
if filename.endswith('.hy'): 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>') source = hy_compile(hy_tree, '<hyc_compile>')
flags = hy_ast_compile_flags flags = hy_ast_compile_flags
@ -530,3 +552,18 @@ else:
return cfile return cfile
py_compile.compile = hyc_compile py_compile.compile = hyc_compile
# We create a separate version of runpy, "runhy", that prefers Hy source over
# Python.
runhy = importlib.import_module('runpy')
runhy._get_code_from_file = partial(_get_code_from_file,
hy_src_check=_could_be_hy_src)
del sys.modules['runpy']
runpy = importlib.import_module('runpy')
_runpy_get_code_from_file = runpy._get_code_from_file
runpy._get_code_from_file = _get_code_from_file

View File

@ -0,0 +1,2 @@
#!/usr/bin/env hy
(print "This Should Still Work")

View File

@ -382,3 +382,9 @@ def test_bin_hy_module_no_main():
def test_bin_hy_sys_executable(): def test_bin_hy_sys_executable():
output, _ = run_cmd("hy -c '(do (import sys) (print sys.executable))'") output, _ = run_cmd("hy -c '(do (import sys) (print sys.executable))'")
assert output.strip().endswith('/hy') assert output.strip().endswith('/hy')
def test_bin_hy_file_no_extension():
"""Confirm that a file with no extension is processed as Hy source"""
output, _ = run_cmd("hy tests/resources/no_extension")
assert "This Should Still Work" in output