Skip to content

Commit 0aec10a

Browse files
authored
Imports in the time of __init__ (#393)
* Imports in the time of __init__ * Tests * Allow Python submodules to Basilisp modules * Better branch coverage
1 parent 0b9c763 commit 0aec10a

File tree

3 files changed

+382
-14
lines changed

3 files changed

+382
-14
lines changed
File renamed without changes.

src/basilisp/importer.py

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import importlib.machinery
21
import importlib.util
32
import logging
43
import marshal
54
import os
65
import os.path
76
import sys
87
import types
8+
from functools import lru_cache
99
from importlib.abc import MetaPathFinder, SourceLoader
10+
from importlib.machinery import ModuleSpec
1011
from typing import List, Mapping, MutableMapping, Optional
1112

1213
import basilisp.lang.compiler as compiler
@@ -87,6 +88,37 @@ def _cache_from_source(path: str) -> str:
8788
return os.path.join(cache_path, filename + ".lpyc")
8889

8990

91+
@lru_cache()
92+
def _is_package(path: str) -> bool:
93+
"""Return True if path should be considered a Basilisp (and consequently
94+
a Python) package.
95+
96+
A path would be considered a package if it contains at least one Basilisp
97+
or Python code file."""
98+
for _, _, files in os.walk(path):
99+
for file in files:
100+
if file.endswith(".lpy") or file.endswith(".py"):
101+
return True
102+
return False
103+
104+
105+
@lru_cache()
106+
def _is_namespace_package(path: str) -> bool:
107+
"""Return True if the current directory is a namespace Basilisp package.
108+
109+
Basilisp namespace packages are directories containing no __init__.py or
110+
__init__.lpy files and at least one other Basilisp code file."""
111+
no_inits = True
112+
has_basilisp_files = False
113+
_, _, files = next(os.walk(path))
114+
for file in files:
115+
if file in {"__init__.lpy", "__init__.py"}:
116+
no_inits = False
117+
elif file.endswith(".lpy"):
118+
has_basilisp_files = True
119+
return no_inits and has_basilisp_files
120+
121+
90122
class BasilispImporter(MetaPathFinder, SourceLoader):
91123
"""Python import hook to allow directly loading Basilisp code within
92124
Python."""
@@ -99,24 +131,25 @@ def find_spec(
99131
fullname: str,
100132
path, # Optional[List[str]] # MyPy complains this is incompatible with supertype
101133
target: types.ModuleType = None,
102-
) -> Optional[importlib.machinery.ModuleSpec]:
134+
) -> Optional[ModuleSpec]:
103135
"""Find the ModuleSpec for the specified Basilisp module.
104136
105137
Returns None if the module is not a Basilisp module to allow import processing to continue."""
106138
package_components = fullname.split(".")
107-
if path is None:
139+
if not path:
108140
path = sys.path
109141
module_name = package_components
110142
else:
111143
module_name = [package_components[-1]]
112144

113145
for entry in path:
146+
root_path = os.path.join(entry, *module_name)
114147
filenames = [
115-
f"{os.path.join(entry, *module_name, '__init__')}.lpy",
116-
f"{os.path.join(entry, *module_name)}.lpy",
148+
f"{os.path.join(root_path, '__init__')}.lpy",
149+
f"{root_path}.lpy",
117150
]
118151
for filename in filenames:
119-
if os.path.exists(filename):
152+
if os.path.isfile(filename):
120153
state = {
121154
"fullname": fullname,
122155
"filename": filename,
@@ -127,9 +160,29 @@ def find_spec(
127160
logger.debug(
128161
f"Found potential Basilisp module '{fullname}' in file '{filename}'"
129162
)
130-
return importlib.machinery.ModuleSpec(
131-
fullname, self, origin=filename, loader_state=state
163+
is_package = filename.endswith("__init__.lpy") or _is_package(
164+
root_path
165+
)
166+
spec = ModuleSpec(
167+
fullname,
168+
self,
169+
origin=filename,
170+
loader_state=state,
171+
is_package=is_package,
132172
)
173+
# The Basilisp loader can find packages regardless of
174+
# submodule_search_locations, but the Python loader cannot.
175+
# Set this to the root path to allow the Python loader to
176+
# load submodules of Basilisp "packages".
177+
if is_package:
178+
assert (
179+
spec.submodule_search_locations is not None
180+
), "Package module spec must have submodule_search_locations list"
181+
spec.submodule_search_locations.append(root_path)
182+
return spec
183+
if os.path.isdir(root_path):
184+
if _is_namespace_package(root_path):
185+
return ModuleSpec(fullname, None, is_package=True)
133186
return None
134187

135188
def invalidate_caches(self):
@@ -154,15 +207,15 @@ def set_data(self, path, data):
154207
with open(path, mode="w+b") as f:
155208
f.write(data)
156209

157-
def get_filename(self, fullname: str) -> str:
210+
def get_filename(self, fullname: str) -> str: # pragma: no cover
158211
try:
159212
cached = self._cache[fullname]
160213
except KeyError:
161214
raise ImportError(f"Could not import module '{fullname}'")
162215
spec = cached["spec"]
163216
return spec.loader_state.filename
164217

165-
def create_module(self, spec: importlib.machinery.ModuleSpec):
218+
def create_module(self, spec: ModuleSpec):
166219
logger.debug(f"Creating Basilisp module '{spec.name}''")
167220
mod = types.ModuleType(spec.name)
168221
mod.__file__ = spec.loader_state["filename"]

0 commit comments

Comments
 (0)