Skip to content

Commit d9c7e03

Browse files
committed
Add support for referring imported names
1 parent 88cfb0b commit d9c7e03

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
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88
### Added
99
* Added `afor` and `awith` macros to support async Python interop (#1179, #1181)
10+
* Added support for referring imported Python names as by `from ... import ...` (#1154)
1011

1112
### Changed
1213
* Single arity functions can be tagged with `^:allow-unsafe-names` to preserve their parameter names (#1212)
@@ -16,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1617
* Fix an issue where consecutive reader comment forms would not be ignored (#1207)
1718

1819
## [v0.3.7]
19-
### Fixed
20+
### Fixed
2021
* 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)
2122
* Fix a bug where `basilisp.process/exec` could deadlock reading process output if that output exceeded the buffer size (#1202)
2223
* 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
@@ -174,9 +174,11 @@
174174
FINALLY = kw.keyword("finally")
175175

176176
# Constants used in analyzing
177+
ALL = kw.keyword("all")
177178
AS = kw.keyword("as")
178179
IMPLEMENTS = kw.keyword("implements")
179180
INTERFACE = kw.keyword("interface")
181+
REFER = kw.keyword("refer")
180182
STAR_STAR = sym.symbol("**")
181183
_DOUBLE_DOT_MACRO_NAME = ".."
182184
_BUILTINS_NS = "python"
@@ -2569,7 +2571,7 @@ def _do_warn_on_import_or_require_name_clash(
25692571
def _import_ast(form: ISeq, ctx: AnalyzerContext) -> Import:
25702572
assert form.first == SpecialForm.IMPORT
25712573

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

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

@@ -2641,6 +2683,8 @@ def _import_ast(form: ISeq, ctx: AnalyzerContext) -> Import:
26412683
return Import(
26422684
form=form,
26432685
aliases=aliases,
2686+
refers=refers,
2687+
refer_all=refer_all,
26442688
env=ctx.get_node_env(pos=ctx.syntax_position),
26452689
)
26462690

src/basilisp/lang/compiler/generator.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2402,6 +2402,24 @@ def _import_to_py_ast(ctx: GeneratorContext, node: Import) -> GeneratedPyAST[ast
24022402
),
24032403
)
24042404
)
2405+
2406+
if node.refer_all:
2407+
deps.append(
2408+
ast.ImportFrom(
2409+
module=safe_name,
2410+
names=[ast.alias(name="*")],
2411+
level=0,
2412+
)
2413+
)
2414+
elif node.refers:
2415+
deps.append(
2416+
ast.ImportFrom(
2417+
module=safe_name,
2418+
names=[ast.alias(name=munge(name)) for name in node.refers],
2419+
level=0,
2420+
)
2421+
)
2422+
24052423
last = ast.Name(id=py_import_alias, ctx=ast.Load())
24062424

24072425
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)