1
- import importlib .machinery
2
1
import importlib .util
3
2
import logging
4
3
import marshal
5
4
import os
6
5
import os .path
7
6
import sys
8
7
import types
8
+ from functools import lru_cache
9
9
from importlib .abc import MetaPathFinder , SourceLoader
10
+ from importlib .machinery import ModuleSpec
10
11
from typing import List , Mapping , MutableMapping , Optional
11
12
12
13
import basilisp .lang .compiler as compiler
@@ -87,6 +88,37 @@ def _cache_from_source(path: str) -> str:
87
88
return os .path .join (cache_path , filename + ".lpyc" )
88
89
89
90
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
+
90
122
class BasilispImporter (MetaPathFinder , SourceLoader ):
91
123
"""Python import hook to allow directly loading Basilisp code within
92
124
Python."""
@@ -99,24 +131,25 @@ def find_spec(
99
131
fullname : str ,
100
132
path , # Optional[List[str]] # MyPy complains this is incompatible with supertype
101
133
target : types .ModuleType = None ,
102
- ) -> Optional [importlib . machinery . ModuleSpec ]:
134
+ ) -> Optional [ModuleSpec ]:
103
135
"""Find the ModuleSpec for the specified Basilisp module.
104
136
105
137
Returns None if the module is not a Basilisp module to allow import processing to continue."""
106
138
package_components = fullname .split ("." )
107
- if path is None :
139
+ if not path :
108
140
path = sys .path
109
141
module_name = package_components
110
142
else :
111
143
module_name = [package_components [- 1 ]]
112
144
113
145
for entry in path :
146
+ root_path = os .path .join (entry , * module_name )
114
147
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" ,
117
150
]
118
151
for filename in filenames :
119
- if os .path .exists (filename ):
152
+ if os .path .isfile (filename ):
120
153
state = {
121
154
"fullname" : fullname ,
122
155
"filename" : filename ,
@@ -127,9 +160,29 @@ def find_spec(
127
160
logger .debug (
128
161
f"Found potential Basilisp module '{ fullname } ' in file '{ filename } '"
129
162
)
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 ,
132
172
)
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 )
133
186
return None
134
187
135
188
def invalidate_caches (self ):
@@ -154,15 +207,15 @@ def set_data(self, path, data):
154
207
with open (path , mode = "w+b" ) as f :
155
208
f .write (data )
156
209
157
- def get_filename (self , fullname : str ) -> str :
210
+ def get_filename (self , fullname : str ) -> str : # pragma: no cover
158
211
try :
159
212
cached = self ._cache [fullname ]
160
213
except KeyError :
161
214
raise ImportError (f"Could not import module '{ fullname } '" )
162
215
spec = cached ["spec" ]
163
216
return spec .loader_state .filename
164
217
165
- def create_module (self , spec : importlib . machinery . ModuleSpec ):
218
+ def create_module (self , spec : ModuleSpec ):
166
219
logger .debug (f"Creating Basilisp module '{ spec .name } ''" )
167
220
mod = types .ModuleType (spec .name )
168
221
mod .__file__ = spec .loader_state ["filename" ]
0 commit comments