Skip to content

Commit

Permalink
Support relative requires in hy -m and hy2py
Browse files Browse the repository at this point in the history
This has necessitated changing the semantics of recursive `hy2py` a fair amount: it now expects a module name, not just any old directory.
  • Loading branch information
Kodiologist committed Jun 3, 2023
1 parent 5f88e55 commit 73a6732
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 18 deletions.
5 changes: 5 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Breaking Changes
Forms like `#*word` will attempt to dispatch a macro named `*word`;
to unpack a symbol named `word`, write `#* word` (note the space).
* Reader macro names are no longer mangled.
* `hy2py`'s recursive mode now expects a module name as input, not any
old directory. You must be in the parent directory of the module
directory.

Bug Fixes
------------------------------
Expand All @@ -22,6 +25,8 @@ Bug Fixes
* Fixed some bugs with traceback pointing.
* Fixed some bugs with escaping in bracket f-strings
* The parser no longer looks for shebangs in the REPL or `hy -c`.
* `require` with relative module names should now work correctly with
`hy -m`, as well as `hy2py`'s recursive mode.

New Features
------------------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ for a complete list of options and :py:ref:`Python's documentation
hy2py
-----

``hy2py`` is a program to convert Hy source code into Python source code. Use ``hy2py --help`` for usage instructions. It can take its input from standard input, from a filename, or folder name provided as a command-line argument. If it is a folder, the output parameter (--output/-o) must be provided. When the output parameter is provided, the output will be written into the folder or file, otherwise the result is written to standard output.
``hy2py`` is a program to convert Hy source code into Python source code. Use ``hy2py --help`` for usage instructions. It can take its input from standard input, or from a file or module name provided as a command-line argument. In the case of a module name, the current working directory should be the parent directory of that module, and the output parameter (``--output/-o``) is required. When the output parameter is provided, the output will be written into the folder or file. Otherwise, the result is written to standard output.

.. warning::
``hy2py`` can execute arbitrary code. Don't give it untrusted input.
Expand Down
13 changes: 11 additions & 2 deletions hy/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import platform
import py_compile
import re
import runpy
import sys
from contextlib import nullcontext
Expand Down Expand Up @@ -332,7 +333,9 @@ def hyc_main():


def hy2py_worker(source, options, filename, output_filepath=None):
source_path = None
if isinstance(source, Path):
source_path = source
source = source.read_text(encoding="UTF-8")

if not output_filepath and options.output:
Expand All @@ -358,7 +361,13 @@ def printing_source(hst):
hst.filename = filename

with filtered_hy_exceptions():
_ast = hy_compile(hst, "__main__", filename=filename, source=source)
_ast = hy_compile(
hst,
re.sub(r'\.hy$', '', '.'.join(source_path.parts))
if source_path
else '__main__',
filename=filename,
source=source)

if options.with_source:
print()
Expand All @@ -385,7 +394,7 @@ def hy2py_main():
"FILE",
type=str,
nargs="?",
help='Input Hy code (can be file or directory) (use STDIN if "-" or '
help='Input Hy code (can be file or module) (use STDIN if "-" or '
"not provided)",
)
parser.add_argument(
Expand Down
2 changes: 2 additions & 0 deletions hy/core/result_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,8 @@ def compile_require(compiler, expr, root, entries):
dotted("hy.macros.require"),
String(module_name),
Symbol("None"),
Keyword("target_module_name"),
String(compiler.module.__name__),
Keyword("assignments"),
(
String("EXPORTS")
Expand Down
6 changes: 4 additions & 2 deletions hy/macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def enable_readers(module, reader, names):
reader.reader_macros[name] = namespace["_hy_reader_macros"][name]


def require(source_module, target_module, assignments, prefix=""):
def require(source_module, target_module, assignments, prefix="", target_module_name=None):
"""Load macros from one module into the namespace of another.
This function is called from the macro also named `require`.
Expand All @@ -213,6 +213,7 @@ def require(source_module, target_module, assignments, prefix=""):
prefix (str): 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. Defaults to ""
target_module_name: If true, overrides the apparent name of `target_module`.
Returns:
bool: Whether or not macros were actually transferred.
Expand All @@ -230,7 +231,8 @@ def require(source_module, target_module, assignments, prefix=""):
return False

if not inspect.ismodule(source_module):
source_module = import_module_from_string(source_module, target_module)
source_module = import_module_from_string(source_module,
target_module_name or target_module)

source_macros = source_module.__dict__.setdefault("_hy_macros", {})
source_exports = getattr(
Expand Down
53 changes: 40 additions & 13 deletions tests/test_bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,26 +741,53 @@ def test_assert(tmp_path, monkeypatch):
assert ("bye" in err) == show_msg


def test_hy2py_recursive(tmp_path):
(tmp_path / 'hy').mkdir()
(tmp_path / "hy/first.hy").write_text("""
(import folder.second [a b])
def test_hy2py_recursive(monkeypatch, tmp_path):
(tmp_path / 'foo').mkdir()
(tmp_path / 'foo/__init__.py').touch()
(tmp_path / "foo/first.hy").write_text("""
(import foo.folder.second [a b])
(print a)
(print b)""")
(tmp_path / "hy/folder").mkdir()
(tmp_path / "hy/folder/second.hy").write_text("""
(tmp_path / "foo/folder").mkdir()
(tmp_path / "foo/folder/__init__.py").touch()
(tmp_path / "foo/folder/second.hy").write_text("""
(setv a 1)
(setv b "hello world")""")

_, err = run_cmd(["hy2py", (tmp_path / 'hy')], expect=1)
monkeypatch.chdir(tmp_path)

_, err = run_cmd("hy2py foo", expect=1)
assert "ValueError" in err

run_cmd(["hy2py",
(tmp_path / 'hy'),
"--output", (tmp_path / 'py')])
assert set((tmp_path / 'py').rglob('*')) == {
tmp_path / 'py' / p
run_cmd("hy2py foo --output bar")
assert set((tmp_path / 'bar').rglob('*')) == {
tmp_path / 'bar' / p
for p in ('first.py', 'folder', 'folder/second.py')}

output, _ = run_cmd("python3 first.py", cwd = tmp_path / 'py')
output, _ = run_cmd("python3 first.py", cwd = tmp_path / 'bar')
assert output == "1\nhello world\n"


@pytest.mark.parametrize('case', ['hy -m', 'hy2py'])
def test_relative_require(case, monkeypatch, tmp_path):
# https://github.com/hylang/hy/issues/2204

(tmp_path / 'pkg').mkdir()
(tmp_path / 'pkg' / '__init__.py').touch()
(tmp_path / 'pkg' / 'a.hy').write_text('''
(defmacro m []
'(setv x (.upper "hello")))''')
(tmp_path / 'pkg' / 'b.hy').write_text('''
(require .a [m])
(m)
(print x)''')
monkeypatch.chdir(tmp_path)

if case == 'hy -m':
output, _ = run_cmd('hy -m pkg.b')
elif case == 'hy2py':
run_cmd('hy2py pkg -o out')
(tmp_path / 'out' / '__init__.py').touch()
output, _ = run_cmd('python3 -m out.b')

assert 'HELLO' in output

0 comments on commit 73a6732

Please sign in to comment.