Skip to content

Commit 2a4b6f0

Browse files
authored
Allow Python imports to be aliased like a Basilisp require (#319)
* Allow Python imports to be aliased like a Basilisp require * Docstrings * Formatting
1 parent 6299191 commit 2a4b6f0

File tree

4 files changed

+118
-21
lines changed

4 files changed

+118
-21
lines changed

src/basilisp/core/__init__.lpy

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,22 @@
11591159
(finally
11601160
(. (var ~vvar) ~'pop-bindings)))))
11611161

1162+
(import* [time :as py-time])
1163+
1164+
(defn ^:private perf-counter
1165+
[]
1166+
(py-time/perf-counter))
1167+
1168+
(defmacro time
1169+
"Time the execution of expr. Return the result of expr and print the
1170+
time execution took in milliseconds."
1171+
[expr]
1172+
`(let [start (perf-counter)]
1173+
(try
1174+
~expr
1175+
(finally
1176+
(println (* 1000 (- (perf-counter) start)) "msecs")))))
1177+
11621178
;;;;;;;;;;;;;;;;;;;;;;
11631179
;; Threading Macros ;;
11641180
;;;;;;;;;;;;;;;;;;;;;;

src/basilisp/lang/compiler.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,8 @@ def quoted(self):
328328
yield
329329
self._is_quoted.pop()
330330

331-
def add_import(self, imp: sym.Symbol, mod: types.ModuleType):
332-
self.current_ns.add_import(imp, mod)
331+
def add_import(self, imp: sym.Symbol, mod: types.ModuleType, *aliases: sym.Symbol):
332+
self.current_ns.add_import(imp, mod, *aliases)
333333

334334
@property
335335
def imports(self) -> lmap.Map:
@@ -1272,35 +1272,52 @@ def _if_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
12721272
def _import_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
12731273
"""Append Import statements into the compiler context nodes."""
12741274
assert form.first == _IMPORT
1275-
assert all([isinstance(f, sym.Symbol) for f in form.rest])
12761275

12771276
last = None
1278-
for s in form.rest:
1277+
for f in form.rest:
1278+
if isinstance(f, sym.Symbol):
1279+
module_name = f
1280+
module_alias = module_name.name.split(".", maxsplit=1)[0]
1281+
elif isinstance(f, vec.Vector):
1282+
module_name = f.entry(0)
1283+
assert isinstance(
1284+
module_name, sym.Symbol
1285+
), "Python module name must be a symbol"
1286+
assert kw.keyword("as") == f.entry(1)
1287+
module_alias = f.entry(2).name
1288+
else:
1289+
raise CompilerException("Symbol or vector expected for import*")
1290+
12791291
try:
1280-
module = importlib.import_module(s.name)
1281-
ctx.add_import(s, module)
1292+
module = importlib.import_module(module_name.name)
1293+
if module_name.name != module_alias:
1294+
ctx.add_import(module_name, module, sym.symbol(module_alias))
1295+
else:
1296+
ctx.add_import(module_name, module)
12821297
except ModuleNotFoundError:
1283-
raise ImportError(f"Module '{s.name}' not found")
1298+
raise ImportError(f"Module '{module_name.name}' not found")
12841299

12851300
with ctx.quoted():
1286-
module_name = s.name.split(".", maxsplit=1)[0]
1287-
yield _dependency(ast.Global(names=[module_name]))
1301+
module_alias = munge(module_alias)
1302+
yield _dependency(ast.Global(names=[module_alias]))
12881303
yield _dependency(
12891304
ast.Assign(
1290-
targets=[ast.Name(id=module_name, ctx=ast.Store())],
1305+
targets=[ast.Name(id=module_alias, ctx=ast.Store())],
12911306
value=ast.Call(
12921307
func=_load_attr("builtins.__import__"),
1293-
args=[ast.Str(s.name)],
1308+
args=[ast.Str(module_name.name)],
12941309
keywords=[],
12951310
),
12961311
)
12971312
)
1298-
last = ast.Name(id=module_name, ctx=ast.Load())
1313+
last = ast.Name(id=module_alias, ctx=ast.Load())
12991314
yield _dependency(
13001315
ast.Call(
13011316
func=_load_attr(f"{_NS_VAR_VALUE}.add_import"),
13021317
args=list(
1303-
chain(_unwrap_nodes(_to_ast(ctx, s)), [last]) # type: ignore
1318+
chain( # type: ignore
1319+
_unwrap_nodes(_to_ast(ctx, module_name)), [last]
1320+
)
13041321
),
13051322
keywords=[],
13061323
)
@@ -2046,17 +2063,31 @@ def _resolve_sym(ctx: CompilerContext, form: sym.Symbol) -> Optional[str]: # no
20462063
if v is not None:
20472064
return _resolve_sym_var(ctx, v)
20482065
ns_sym = sym.symbol(form.ns)
2049-
if ns_sym in ctx.current_ns.imports:
2066+
if ns_sym in ctx.current_ns.imports or ns_sym in ctx.current_ns.import_aliases:
20502067
# We still import Basilisp code, so we'll want to make sure
20512068
# that the symbol isn't referring to a Basilisp Var first
20522069
v = Var.find(form)
20532070
if v is not None:
20542071
return _resolve_sym_var(ctx, v)
20552072

2073+
# Python modules imported using `import*` may be imported
2074+
# with an alias, which sets the local name of the alias to the
2075+
# alias value (much like Python's `from module import member`
2076+
# statement). In this case, we'll want to check for the module
2077+
# using its non-aliased name, but then generate code to access
2078+
# the member using the alias.
2079+
if ns_sym in ctx.current_ns.import_aliases:
2080+
module_name: sym.Symbol = ctx.current_ns.import_aliases[ns_sym]
2081+
else:
2082+
module_name = ns_sym
2083+
20562084
# Otherwise, try to direct-link it like a Python variable
2085+
safe_module_name = munge(module_name.name)
2086+
assert (
2087+
safe_module_name in sys.modules
2088+
), f"Module '{safe_module_name}' is not imported"
2089+
ns_module = sys.modules[safe_module_name]
20572090
safe_ns = munge(form.ns)
2058-
assert safe_ns in sys.modules, f"Module '{safe_ns}' is not imported"
2059-
ns_module = sys.modules[safe_ns]
20602091

20612092
# Try without allowing builtins first
20622093
safe_name = munge(form.name)

src/basilisp/lang/runtime.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,15 @@ class Namespace:
305305

306306
_NAMESPACES = atom.Atom(lmap.Map.empty())
307307

308-
__slots__ = ("_name", "_module", "_interns", "_refers", "_aliases", "_imports")
308+
__slots__ = (
309+
"_name",
310+
"_module",
311+
"_interns",
312+
"_refers",
313+
"_aliases",
314+
"_imports",
315+
"_import_aliases",
316+
)
309317

310318
def __init__(self, name: sym.Symbol, module: types.ModuleType = None) -> None:
311319
self._name = name
@@ -319,6 +327,7 @@ def __init__(self, name: sym.Symbol, module: types.ModuleType = None) -> None:
319327
.to_dict()
320328
)
321329
)
330+
self._import_aliases: atom.Atom = atom.Atom(lmap.Map.empty())
322331
self._interns: atom.Atom = atom.Atom(lmap.Map.empty())
323332
self._refers: atom.Atom = atom.Atom(lmap.Map.empty())
324333

@@ -360,6 +369,11 @@ def imports(self) -> lmap.Map:
360369
namespace."""
361370
return self._imports.deref()
362371

372+
@property
373+
def import_aliases(self) -> lmap.Map:
374+
"""A mapping of a symbolic alias and a Python module name."""
375+
return self._import_aliases.deref()
376+
363377
@property
364378
def interns(self) -> lmap.Map:
365379
"""A mapping between a symbolic name and a Var. The Var may point to
@@ -415,14 +429,32 @@ def find(self, sym: sym.Symbol) -> Optional[Var]:
415429
return self.refers.entry(sym, None)
416430
return v
417431

418-
def add_import(self, sym: sym.Symbol, module: types.ModuleType) -> None:
419-
"""Add the Symbol as an imported Symbol in this Namespace."""
432+
def add_import(
433+
self, sym: sym.Symbol, module: types.ModuleType, *aliases: sym.Symbol
434+
) -> None:
435+
"""Add the Symbol as an imported Symbol in this Namespace. If aliases are given,
436+
the aliases will be applied to the """
420437
self._imports.swap(lambda m: m.assoc(sym, module))
438+
if aliases:
439+
self._import_aliases.swap(
440+
lambda m: m.assoc(
441+
*itertools.chain.from_iterable([(alias, sym) for alias in aliases])
442+
)
443+
)
421444

422445
def get_import(self, sym: sym.Symbol) -> Optional[types.ModuleType]:
423446
"""Return the module if a moduled named by sym has been imported into
424-
this Namespace, None otherwise."""
425-
return self.imports.entry(sym, None)
447+
this Namespace, None otherwise.
448+
449+
First try to resolve a module directly with the given name. If no module
450+
can be resolved, attempt to resolve the module using import aliases."""
451+
mod = self.imports.entry(sym, None)
452+
if mod is None:
453+
alias = self.import_aliases.get(sym, None)
454+
if alias is None:
455+
return None
456+
return self.imports.entry(alias, None)
457+
return mod
426458

427459
def add_refer(self, sym: sym.Symbol, var: Var) -> None:
428460
"""Refer var in this namespace under the name sym."""

tests/basilisp/compiler_test.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,24 @@ def test_truthiness(ns: runtime.Namespace):
593593
assert kw.keyword("a") == lcompile("(if #{true} :a :b)")
594594

595595

596+
def test_import(ns: runtime.Namespace):
597+
with pytest.raises(compiler.CompilerException):
598+
lcompile("(import* :time)")
599+
600+
with pytest.raises(compiler.CompilerException):
601+
lcompile('(import* "time")')
602+
603+
with pytest.raises(ImportError):
604+
lcompile("(import* real.fake.module)")
605+
606+
import time
607+
608+
assert time.perf_counter == lcompile("(import* time) time/perf-counter")
609+
assert time.perf_counter == lcompile(
610+
"(import* [time :as py-time]) py-time/perf-counter"
611+
)
612+
613+
596614
def test_interop_new(ns: runtime.Namespace):
597615
assert "hi" == lcompile('(builtins.str. "hi")')
598616
assert "1" == lcompile("(builtins.str. 1)")

0 commit comments

Comments
 (0)