Repository: hylang/hy

Issue:
`hy2py` cannot import local module during compile time
`eggs.hy`:
```hy
(print 42)
```
`spam.hy`:
```hy
(eval-when-compile (import eggs))
```

`hy2py` fails to compile `spam.hy`:
```bash
$ > hy2py spam.hy 
Traceback (most recent call last):
  File "/usr/bin/hy2py", line 12, in <module>
    sys.exit(hy2py_main())
  File "spam.hy", line 2, in <module>
    (import eggs))
hy.errors.HyEvalError: 
  File "[HyExpression([
  HySymbol('import'),
  HySymbol('eggs')])]", line 1
    (eval-when-compile
    ^----------------^
No module named 'eggs'
```
But `hy` itself runs fine:
```bash
$ > hy spam.hy 
42
```
Note: this seems to be  unaffected by whether `pyc` files are present for any of the files.


Discussion:

Resolving patch:
diff --git a/hy/cmdline.py b/hy/cmdline.py
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -443,6 +443,16 @@ def ideas_macro(ETname):
 """)])
 
 
+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")
@@ -484,13 +494,7 @@ def run_repl(hr=None, **kwargs):
 
 def run_icommand(source, **kwargs):
     if os.path.exists(source):
-        # Emulate Python cmdline behavior by setting `sys.path` relative
-        # to the executed file's location.
-        if sys.path[0] == '':
-            sys.path[0] = os.path.realpath(os.path.split(source)[0])
-        else:
-            sys.path.insert(0, os.path.split(source)[0])
-
+        set_path(source)
         with io.open(source, "r", encoding='utf-8') as f:
             source = f.read()
         filename = source
@@ -636,6 +640,7 @@ def proc_opt(opt, arg=None, item=None, i=None):
         return run_command(options['command'], filename='<string>')
 
     if 'mod' in options:
+        set_path('')
         sys.argv = [program] + argv
         runpy.run_module(options['mod'], run_name='__main__', alter_sys=True)
         return 0
@@ -653,13 +658,7 @@ def proc_opt(opt, arg=None, item=None, i=None):
         else:
             # User did "hy <filename>"
             filename = argv[0]
-
-            # Emulate Python cmdline behavior by setting `sys.path` relative
-            # to the executed file's location.
-            if sys.path[0] == '':
-                sys.path[0] = os.path.realpath(os.path.split(filename)[0])
-            else:
-                sys.path.insert(0, os.path.split(filename)[0])
+            set_path(filename)
 
             try:
                 sys.argv = argv
@@ -706,6 +705,7 @@ def hyc_main():
             if not filename:
                 break
             filename = filename.rstrip('\n')
+            set_path(filename)
             try:
                 py_compile.compile(filename, doraise=True)
             except py_compile.PyCompileError as error:
@@ -714,8 +714,10 @@ def hyc_main():
             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)
@@ -723,6 +725,7 @@ def hyc_main():
                 # return value to indicate at least one failure
                 rv = 1
                 sys.stderr.write("%s\n" % error.msg)
+            sys.path.pop(0)
     return rv
 
 
@@ -745,10 +748,12 @@ def hy2py_main():
     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 io.open(options.FILE, 'r', encoding='utf-8') as source_file:
             source = source_file.read()
 


Test patch:
diff --git a/tests/resources/relative_import_compile_time.hy b/tests/resources/relative_import_compile_time.hy
new file mode 100644
--- /dev/null
+++ b/tests/resources/relative_import_compile_time.hy
@@ -0,0 +1,4 @@
+(eval-when-compile
+  (import bin.printenv)
+  (import sys)
+  (print sys.path))
diff --git a/tests/test_bin.py b/tests/test_bin.py
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -442,6 +442,22 @@ def test_bin_hy_file_sys_path():
     output, _ = run_cmd("hy tests/resources/relative_import.hy")
     assert file_relative_path in output
 
+def test_bin_hyc_file_sys_path():
+    # similar to test_bin_hy_file_sys_path, test hyc and hy2py to make sure
+    # they can find the relative import at compile time
+    # https://github.com/hylang/hy/issues/2021
+
+    test_file = 'tests/resources/relative_import_compile_time.hy'
+    file_relative_path = os.path.realpath(os.path.dirname(test_file))
+
+    for binary in ("hy", "hyc", "hy2py"):
+        # Ensure we hit the compiler
+        rm(cache_from_source(test_file))
+        assert not os.path.exists(cache_from_source(file_relative_path))
+
+        output, _ = run_cmd(f"{binary} {test_file}")
+        assert file_relative_path in output
+
 
 def test_bin_hy_module_main_args():
     output, _ = run_cmd("hy -m tests.resources.bin.main test 123 -B")