From c0c5c9c6993cdd3533fb3be8b7b5c80dd0d16af5 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Tue, 4 Sep 2018 22:19:40 -0500 Subject: [PATCH] Make cmdline Hy process unknown filetypes as Hy source This change a Hy-preferring `runhy` that is used by cmdline Hy. Standard `runpy` is still patched so that it can run `.hy` files, but the default behaviour for unknown filetypes is preserved (i.e. assume they are Python source). Closes hylang/hy#1677. --- hy/cmdline.py | 4 +- hy/importer.py | 89 +++++++++++++++++++++++++----------- tests/resources/no_extension | 2 + tests/test_bin.py | 6 +++ 4 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 tests/resources/no_extension diff --git a/hy/cmdline.py b/hy/cmdline.py index 08f6c0b..f38355b 100644 --- a/hy/cmdline.py +++ b/hy/cmdline.py @@ -19,7 +19,7 @@ import astor.code_gen import hy from hy.lex import LexException, PrematureEndOfInput, mangle 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.macros import macro, require from hy.models import HyExpression, HyString, HySymbol @@ -352,7 +352,7 @@ def cmdline_handler(scriptname, argv): try: sys.argv = options.args - runpy.run_path(filename, run_name='__main__') + runhy.run_path(filename, run_name='__main__') return 0 except FileNotFoundError as e: print("hy: Can't open file '{0}': [Errno {1}] {2}".format( diff --git a/hy/importer.py b/hy/importer.py index 1ed6e7e..1bc8573 100644 --- a/hy/importer.py +++ b/hy/importer.py @@ -11,12 +11,13 @@ import inspect import pkgutil import re import io -import runpy import types import tempfile import importlib import __future__ +from functools import partial + from hy.errors import HyTypeError from hy.compiler import hy_compile 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)) -def _get_code_from_file(run_name, fname=None): - """A patch of `runpy._get_code_from_file` that will also compile Hy - code. +def _hy_code_from_file(filename, loader_type=None): + """Use PEP-302 loader to produce code for a given Hy source file.""" + full_fname = os.path.abspath(filename) + fname_path, fname_file = os.path.split(full_fname) + modname = os.path.splitext(fname_file)[0] + sys.path.insert(0, fname_path) + try: + if loader_type is None: + loader = pkgutil.get_loader(modname) + else: + loader = loader_type(modname, full_fname) + code = loader.get_code(modname) + finally: + sys.path.pop(0) - This version will read and cache bytecode for Hy files. It operates - normally otherwise. + 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 - if fname.endswith('.hy'): - full_fname = os.path.abspath(fname) - fname_path, fname_file = os.path.split(full_fname) - modname = os.path.splitext(fname_file)[0] - sys.path.insert(0, fname_path) - try: - loader = pkgutil.get_loader(modname) - code = loader.get_code(modname) - finally: - sys.path.pop(0) - else: - with open(fname, "rb") as f: - code = pkgutil.read_code(f) - if code is None: + # Check for bytecode first. (This is what the `runpy` version does!) + with open(fname, "rb") as f: + code = pkgutil.read_code(f) + + 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: + # This code differs from `runpy`'s only in that we + # force decoding into UTF-8. source = f.read().decode('utf-8') code = compile(source, fname, 'exec') 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: importlib.machinery.SOURCE_SUFFIXES.insert(0, '.hy') _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): - if os.path.isfile(path) and path.endswith('.hy'): + if _could_be_hy_src(path): source = data.decode("utf-8") try: hy_tree = hy_parse(source) @@ -242,12 +259,17 @@ else: 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): def __init__(self, fullname, filename, fileobj=None, etc=None): """This constructor is designed for some compatibility with SourceFileLoader.""" 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) if fileobj is None: fileobj = io.open(filename, 'rU', encoding='utf-8') @@ -477,7 +499,7 @@ else: try: flags = None - if filename.endswith('.hy'): + if _could_be_hy_src(filename): hy_tree = hy_parse(source_str) source = hy_compile(hy_tree, '') flags = hy_ast_compile_flags @@ -530,3 +552,18 @@ else: return cfile 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 diff --git a/tests/resources/no_extension b/tests/resources/no_extension new file mode 100644 index 0000000..3d52066 --- /dev/null +++ b/tests/resources/no_extension @@ -0,0 +1,2 @@ +#!/usr/bin/env hy +(print "This Should Still Work") \ No newline at end of file diff --git a/tests/test_bin.py b/tests/test_bin.py index e642f53..58d1448 100644 --- a/tests/test_bin.py +++ b/tests/test_bin.py @@ -382,3 +382,9 @@ def test_bin_hy_module_no_main(): def test_bin_hy_sys_executable(): output, _ = run_cmd("hy -c '(do (import sys) (print sys.executable))'") 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