Repository: hylang/hy

Issue:
`hy2py` and relative require
here is my module
```
.
├── __init__.py
├── args.hy
├── config.hy
├── log.hy
├── macros.hy
├── main.hy
├── type_helpers.hy
└── utils.hy
```

when i try to compile `hy2py args.hy` it raises:
```
hy.errors.HyMacroExpansionError:
  File "args.hy", line 10
    (require .macros *)
    ^-----------------^
expanding macro require
  TypeError: the 'package' argument is required to perform a relative import for '.macros'
```

if i remove dot
```hy
(require macros *)
```
then it works with `hy2py` but breaks running module with entry point

Discussion:

Patch:
diff --git a/NEWS.rst b/NEWS.rst
index eb5c3b3f..1136f216 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -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
 ------------------------------
@@ -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
 ------------------------------
diff --git a/docs/cli.rst b/docs/cli.rst
index f40c18f5..0f8227a0 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -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.
diff --git a/hy/cmdline.py b/hy/cmdline.py
index dca3febb..73e02eac 100644
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -5,6 +5,7 @@ import io
 import os
 import platform
 import py_compile
+import re
 import runpy
 import sys
 from contextlib import nullcontext
@@ -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:
@@ -358,7 +361,13 @@ def hy2py_worker(source, options, filename, output_filepath=None):
         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()
@@ -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(
diff --git a/hy/core/result_macros.py b/hy/core/result_macros.py
index eb296ffd..4d42a003 100644
--- a/hy/core/result_macros.py
+++ b/hy/core/result_macros.py
@@ -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")
diff --git a/hy/macros.py b/hy/macros.py
index 3f159a21..d684310f 100644
--- a/hy/macros.py
+++ b/hy/macros.py
@@ -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`.
@@ -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.
@@ -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(


Test patch:
diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py
index 6cb4c421..4a743e23 100644
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -1,5 +1,3 @@
-# fmt: off
-
 import ast
 from textwrap import dedent
 
diff --git a/tests/test_bin.py b/tests/test_bin.py
index 32e3c84f..b3168243 100644
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-# fmt: off
 
 import builtins
 import os
@@ -742,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