Skip to content

Commit

Permalink
Merge pull request #2354 from Kodiologist/zipimport
Browse files Browse the repository at this point in the history
Implement import from ZIP archives
  • Loading branch information
Kodiologist committed Nov 10, 2022
2 parents 04866e5 + d50bf77 commit 2a737d7
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 4 deletions.
10 changes: 10 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
.. default-role:: code

Unreleased
=============================

New Features
------------------------------
* On Pythons ≥ 3.7, Hy modules can now be imported from ZIP
archives in the same way as Python modules, via `zipimport`_.

.. _zipimport: https://docs.python.org/3.11/library/zipimport.html

0.25.0 (released 2022-11-08)
==============================

Expand Down
29 changes: 25 additions & 4 deletions hy/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"}
)


Expand All @@ -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!
Expand Down
21 changes: 21 additions & 0 deletions tests/importer/test_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")

0 comments on commit 2a737d7

Please sign in to comment.