Skip to content

Commit d7f1f82

Browse files
committed
Add support for referring imported names
1 parent 879817f commit d7f1f82

File tree

5 files changed

+104
-27
lines changed

5 files changed

+104
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
## [v0.3.8]
1010
### Added
1111
* Added `afor` and `awith` macros to support async Python interop (#1179, #1181)
12+
* Added support for referring imported Python names as by `from ... import ...` (#1154)
1213

1314
### Changed
1415
* Function parameter names will not be automatically generated with unique suffixes unless the function meta key `^:safe-py-params` is provided (#1212)
@@ -18,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1819
* Fix an issue where consecutive reader comment forms would not be ignored (#1207)
1920

2021
## [v0.3.7]
21-
### Fixed
22+
### Fixed
2223
* Fix a regression introduced in #1176 where the testrunner couldn't handle relative paths in `sys.path`, causing `basilisp test` to fail when no arugments were provided (#1204)
2324
* Fix a bug where `basilisp.process/exec` could deadlock reading process output if that output exceeded the buffer size (#1202)
2425
* Fix `basilisp boostrap` issue on MS-Windows where the boostrap file loaded too early, before Basilisp was in `sys.path` (#1208)

src/basilisp/core.lpy

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4950,14 +4950,26 @@
49504950
(defn ns-map
49514951
"Return a map of all the mapped symbols in the namespace.
49524952

4953-
Includes the return values of :lpy:fn:`ns-interns` and :lpy:fn:`ns-refers` in one
4954-
map."
4953+
Includes the return values of :lpy:fn:`ns-interns`, :lpy:fn:`ns-refers`, and
4954+
:lpy:fn:`ns-imports` in one map.
4955+
4956+
.. note::
4957+
4958+
It is possible that the same symbol is mapped to an interned name, a referred
4959+
name, an imported name, or an import-referred name. They are stored in separate
4960+
collections in each namespace and Basilisp symbol resolution rules determine the
4961+
precedence. This function returns the combined set of all mapped symbols in a
4962+
single map and therefore symbols defined in multiple maps will only show one
4963+
value. Inspecting the contents of each of the component maps will show all
4964+
mappings, including those that were overwritten by the merge operation in this
4965+
function."
49554966
([] (ns-map *ns*))
49564967
([ns]
49574968
(let [resolved-ns (the-ns ns)]
49584969
(merge
4959-
(ns-interns resolved-ns)
4960-
(ns-refers resolved-ns)))))
4970+
(ns-imports resolved-ns)
4971+
(ns-refers resolved-ns)
4972+
(ns-interns resolved-ns)))))
49614973

49624974
(defn ^:inline ns-resolve
49634975
"Return the Var which will be resolved by the symbol in the given namespace."

src/basilisp/lang/compiler/analyzer.py

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,11 @@
175175
FINALLY = kw.keyword("finally")
176176

177177
# Constants used in analyzing
178+
ALL = kw.keyword("all")
178179
AS = kw.keyword("as")
179180
IMPLEMENTS = kw.keyword("implements")
180181
INTERFACE = kw.keyword("interface")
182+
REFER = kw.keyword("refer")
181183
STAR_STAR = sym.symbol("**")
182184
_DOUBLE_DOT_MACRO_NAME = ".."
183185
_BUILTINS_NS = "python"
@@ -2571,7 +2573,7 @@ def _do_warn_on_import_or_require_name_clash(
25712573
def _import_ast(form: ISeq, ctx: AnalyzerContext) -> Import:
25722574
assert form.first == SpecialForm.IMPORT
25732575

2574-
aliases = []
2576+
aliases, refers, refer_all = [], [], False
25752577
for f in form.rest:
25762578
if isinstance(f, sym.Symbol):
25772579
module_name = f
@@ -2588,40 +2590,80 @@ def _import_ast(form: ISeq, ctx: AnalyzerContext) -> Import:
25882590
symbol_table=ctx.symbol_table.context_boundary,
25892591
)
25902592
elif isinstance(f, vec.PersistentVector):
2591-
if len(f) != 3:
2593+
if len(f) < 1:
25922594
raise ctx.AnalyzerException(
2593-
"import alias must take the form: [module :as alias]", form=f
2595+
"import alias must take the form: [module :as alias :refer [...]]",
2596+
form=f,
25942597
)
25952598
module_name = f.val_at(0) # type: ignore[assignment]
25962599
if not isinstance(module_name, sym.Symbol):
25972600
raise ctx.AnalyzerException(
25982601
"Python module name must be a symbol", form=f
25992602
)
2600-
if not AS == f.val_at(1):
2603+
2604+
try:
2605+
opts = lmap.hash_map(*f[1:])
2606+
except IndexError:
26012607
raise ctx.AnalyzerException(
2602-
"expected :as alias for Python import", form=f
2608+
"Expected options: ':as alias' or ':refer [...]'", form=f
26032609
)
2604-
module_alias_sym = f.val_at(2)
2605-
if not isinstance(module_alias_sym, sym.Symbol):
2610+
2611+
if not {AS, REFER}.issuperset(set(opts.keys())):
26062612
raise ctx.AnalyzerException(
2607-
"Python module alias must be a symbol", form=f
2613+
f"Unexpected import options: {lset.set(opts.keys())}", form=f
26082614
)
2609-
module_alias = module_alias_sym.name
2610-
if "." in module_alias:
2611-
raise ctx.AnalyzerException(
2612-
"Python module alias must not contain '.'", form=f
2615+
2616+
if (module_alias_sym := opts.val_at(AS)) is not None:
2617+
if not isinstance(module_alias_sym, sym.Symbol):
2618+
raise ctx.AnalyzerException(
2619+
"Python module alias must be a symbol", form=f
2620+
)
2621+
module_alias = module_alias_sym.name
2622+
if "." in module_alias:
2623+
raise ctx.AnalyzerException(
2624+
"Python module alias must not contain '.'", form=f
2625+
)
2626+
2627+
ctx.put_new_symbol(
2628+
module_alias_sym,
2629+
Binding(
2630+
form=module_alias_sym,
2631+
name=module_alias,
2632+
local=LocalType.IMPORT,
2633+
env=ctx.get_node_env(),
2634+
),
2635+
symbol_table=ctx.symbol_table.context_boundary,
26132636
)
2637+
else:
2638+
module_alias = module_name.name
26142639

2615-
ctx.put_new_symbol(
2616-
module_alias_sym,
2617-
Binding(
2618-
form=module_alias_sym,
2619-
name=module_alias,
2620-
local=LocalType.IMPORT,
2621-
env=ctx.get_node_env(),
2622-
),
2623-
symbol_table=ctx.symbol_table.context_boundary,
2624-
)
2640+
if (module_refers := opts.val_at(REFER)) is not None:
2641+
if ALL == module_refers:
2642+
refer_all = True
2643+
else:
2644+
if not isinstance(module_refers, vec.PersistentVector):
2645+
raise ctx.AnalyzerException(
2646+
"Python module refers must be a vector of symbols",
2647+
form=module_refers,
2648+
)
2649+
2650+
for refer in module_refers:
2651+
if not isinstance(refer, sym.Symbol):
2652+
raise ctx.AnalyzerException(
2653+
"Python module refer name must be a symbol", form=refer
2654+
)
2655+
refers.append(refer.name)
2656+
2657+
ctx.put_new_symbol(
2658+
refer,
2659+
Binding(
2660+
form=refer,
2661+
name=refer.name,
2662+
local=LocalType.IMPORT,
2663+
env=ctx.get_node_env(),
2664+
),
2665+
symbol_table=ctx.symbol_table.context_boundary,
2666+
)
26252667
else:
26262668
raise ctx.AnalyzerException("symbol or vector expected for import*", form=f)
26272669

@@ -2643,6 +2685,8 @@ def _import_ast(form: ISeq, ctx: AnalyzerContext) -> Import:
26432685
return Import(
26442686
form=form,
26452687
aliases=aliases,
2688+
refers=refers,
2689+
refer_all=refer_all,
26462690
env=ctx.get_node_env(pos=ctx.syntax_position),
26472691
)
26482692

src/basilisp/lang/compiler/generator.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2409,6 +2409,24 @@ def _import_to_py_ast(ctx: GeneratorContext, node: Import) -> GeneratedPyAST[ast
24092409
),
24102410
)
24112411
)
2412+
2413+
if node.refer_all:
2414+
deps.append(
2415+
ast.ImportFrom(
2416+
module=safe_name,
2417+
names=[ast.alias(name="*")],
2418+
level=0,
2419+
)
2420+
)
2421+
elif node.refers:
2422+
deps.append(
2423+
ast.ImportFrom(
2424+
module=safe_name,
2425+
names=[ast.alias(name=munge(name)) for name in node.refers],
2426+
level=0,
2427+
)
2428+
)
2429+
24122430
last = ast.Name(id=py_import_alias, ctx=ast.Load())
24132431

24142432
deps.append(

src/basilisp/lang/compiler/nodes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,8 @@ class If(Node[SpecialForm]):
612612
class Import(Node[SpecialForm]):
613613
form: SpecialForm
614614
aliases: Iterable["ImportAlias"]
615+
refers: Iterable["str"]
616+
refer_all: bool
615617
env: NodeEnv = attr.field(hash=False)
616618
children: Sequence[kw.Keyword] = vec.EMPTY
617619
op: NodeOp = NodeOp.IMPORT

0 commit comments

Comments
 (0)