Repository: hylang/hy

Issue:
Import from zip files
It would be handy to be able to import .hy files from .zip archives (for those of us who like to deploy stuff in single file bundles).

Right now I'm working around this by doing:

```
BUNDLE=sushy.zip

clean:
    rm *.zip

bundle:
    hyc sushy/*.hy
    zip -r9 $(BUNDLE) . -i *.py *.pyc

test-bundle:
    PYTHONPATH=$(BUNDLE) python -m sushy
```



Discussion:
Not a bad idea -- we should likely implement this as a second PEP302 loader -- after all, it is what they were designed for :)

_bump_

I'm also getting trouble with Hy under PyPy. hy -m foo isn't working for some reason, will investigate that on other issues...

@allison-casey What do you think needs to be documented yet? It seems to just plain not work at the moment.
I believe this was documenting that, because Hy is a python dependency you can only make zip apps via [`shiv`](https://shiv.readthedocs.io/en/latest/) which packages the dependencies with the module unlike Python [`zipapp`](https://docs.python.org/3/library/zipapp.html)
I think that's about the distinct issue #1774. This issue is about import from a ZIP archive, not about self-contained executables. shiv shouldn't be necessary in this case.
ahhh i misread. then we can remove the doc me and leave the feature. 
Adapting [the example from the documentation of `zipimport`](https://docs.python.org/3.11/library/zipimport.html#examples):

```
$ echo '(print "zawinski")' >jwzthreading.hy
$ zip example.zip jwzthreading.hy
  adding: jwzthreading.hy (stored 0%)
$ rm jwzthreading.hy
$ unzip -l example.zip
Archive:  example.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       19  2022-11-05 16:25   jwzthreading.hy
---------                     -------
       19                     1 file
$ python3
Python 3.11.0 (main, Nov  5 2022, 14:58:16) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hy, sys
>>> sys.path.insert(0, 'example.zip')
>>> import jwzthreading
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'jwzthreading'
```


Resolving patch:
diff --git a/hy/importer.py b/hy/importer.py
--- a/hy/importer.py
+++ b/hy/importer.py
@@ -5,10 +5,12 @@
 import pkgutil
 import sys
 import types
+import zipimport
 from contextlib import contextmanager
 from functools import partial
 
 import hy
+from hy._compat import PY3_8
 from hy.compiler import hy_compile
 from hy.reader import read_many
 
@@ -107,10 +109,8 @@ def _get_code_from_file(run_name, fname=None, hy_src_check=lambda x: x.endswith(
 
 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:]
-        )
+        os.path.splitext(filename)[1]
+        not in set(importlib.machinery.SOURCE_SUFFIXES) - {".hy"}
     )
 
 
@@ -128,6 +128,27 @@ def _hy_source_to_code(self, data, path, _optimize=-1):
 
 importlib.machinery.SourceFileLoader.source_to_code = _hy_source_to_code
 
+
+if PY3_8 and (".hy", False, False) not in zipimport._zip_searchorder:
+    zipimport._zip_searchorder += ((".hy", False, False),)
+    _py_compile_source = zipimport._compile_source
+
+    def _hy_compile_source(pathname, source):
+        if not pathname.endswith(".hy"):
+            return _py_compile_source(pathname, source)
+        return compile(
+            hy_compile(
+                read_many(source.decode("UTF-8"), filename=pathname, skip_shebang=True),
+                f"<zip:{pathname}>",
+            ),
+            pathname,
+            "exec",
+            dont_inherit=True,
+        )
+
+    zipimport._compile_source = _hy_compile_source
+
+
 #  This is actually needed; otherwise, pre-created finders assigned to the
 #  current dir (i.e. `''`) in `sys.path` will not catch absolute imports of
 #  directory-local modules!


Test patch:
diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py
--- a/tests/importer/test_importer.py
+++ b/tests/importer/test_importer.py
@@ -8,6 +8,7 @@
 import pytest
 
 import hy
+from hy._compat import PY3_8
 from hy.compiler import hy_compile, hy_eval
 from hy.errors import HyLanguageError, hy_exc_handler
 from hy.importer import HyLoader
@@ -273,3 +274,23 @@ def test_filtered_importlib_frames(capsys):
     captured_w_filtering = capsys.readouterr()[-1].strip()
 
     assert "importlib._" not in captured_w_filtering
+
+
+@pytest.mark.skipif(
+    not PY3_8,
+    reason="Python 3.7's `zipimport` is written in C, it can't be monkey-patched",
+)
+def test_zipimport(tmp_path):
+    from zipfile import ZipFile
+
+    zpath = tmp_path / "archive.zip"
+    with ZipFile(zpath, "w") as o:
+        o.writestr("example.hy", '(setv x "Hy from ZIP")')
+
+    try:
+        sys.path.insert(0, str(zpath))
+        import example
+    finally:
+        sys.path = [p for p in sys.path if p != str(zpath)]
+    assert example.x == "Hy from ZIP"
+    assert example.__file__ == str(zpath / "example.hy")