Repository: pytest-dev/apipkg

Issue:
apipkg.initpkg should preserve __spec__
Apipkg.initpkg should preserve the `__spec__` attribute of the original module. This attribute is used by importlib, and in Python 3.7 `importlib.util.find_spec('name')` will fail when 'name' is already in sys.modules and doesn't have an attribute named `__spec__`

See also <https://bugs.python.org/issue35791>

Discussion:

Patch:
diff --git a/src/apipkg/__init__.py b/src/apipkg/__init__.py
index 5f52de3..7981008 100644
--- a/src/apipkg/__init__.py
+++ b/src/apipkg/__init__.py
@@ -56,6 +56,7 @@ def initpkg(pkgname, exportdefs, attr=None, eager=False):
         d["__package__"] = oldmod.__package__
     if "__doc__" not in exportdefs and getattr(oldmod, "__doc__", None):
         d["__doc__"] = oldmod.__doc__
+    d["__spec__"] = getattr(oldmod, "__spec__", None)
     d.update(attr)
     if hasattr(oldmod, "__dict__"):
         oldmod.__dict__.update(d)
@@ -66,6 +67,7 @@ def initpkg(pkgname, exportdefs, attr=None, eager=False):
         for module in list(sys.modules.values()):
             if isinstance(module, ApiModule):
                 module.__dict__
+    return mod
 
 
 def importobj(modpath, attrname):


Test patch:
diff --git a/test_apipkg.py b/test_apipkg.py
index c957ec3..72ec775 100644
--- a/test_apipkg.py
+++ b/test_apipkg.py
@@ -668,3 +668,45 @@ def test_eagerload_on_bython(monkeypatch):
         apipkg.initpkg(
             "apipkg.testmodule.example.lazy", {"test": "apipkg.does_not_exist"}
         )
+
+
+@pytest.fixture
+def find_spec():
+    try:
+        from importlib.util import find_spec
+    except ImportError:
+        pytest.xfail("no importlib")
+    return find_spec
+
+
+def test_importlib_find_spec_fake_module(find_spec):
+    mod = apipkg.initpkg("apipkg.testmodule.example.missing", {})
+    with pytest.raises(ValueError, match=mod.__name__ + r"\.__spec__ is None"):
+        find_spec(mod.__name__)
+
+
+def test_importlib_find_spec_aliasmodule(find_spec):
+    am = apipkg.AliasModule("apipkg.testmodule.example.email_spec", "email")
+    spec = find_spec(am.__name__)
+    assert spec is am.__spec__
+
+
+def test_importlib_find_spec_initpkg(find_spec, tmpdir, monkeypatch):
+
+    modname = "apipkg_test_example_initpkg_findspec"
+
+    pkgdir = tmpdir.mkdir("apipkg_test_example_initpkg_findspec")
+    pkgdir.ensure("__init__.py").write(
+        textwrap.dedent(
+            """
+        import apipkg
+        apipkg.initpkg(__name__, {
+            'email': 'email',
+            })
+    """
+        )
+    )
+
+    monkeypatch.syspath_prepend(tmpdir)
+    find_spec(modname)
+    find_spec(modname + ".email")