Skip to content

Commit aa12514

Browse files
authored
Add support for referring imported names (#1170)
Fixes #1154
1 parent 879817f commit aa12514

File tree

9 files changed

+325
-46
lines changed

9 files changed

+325
-46
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
* Added support for referring imported Python names as by `from ... import ...` (#1154)
810

911
## [v0.3.8]
1012
### Added
@@ -18,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1820
* Fix an issue where consecutive reader comment forms would not be ignored (#1207)
1921

2022
## [v0.3.7]
21-
### Fixed
23+
### Fixed
2224
* 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)
2325
* Fix a bug where `basilisp.process/exec` could deadlock reading process output if that output exceeded the buffer size (#1202)
2426
* Fix `basilisp boostrap` issue on MS-Windows where the boostrap file loaded too early, before Basilisp was in `sys.path` (#1208)

docs/pyinterop.rst

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,39 @@ Submodules will be available under the full, dot-separated name.
4949

5050
To avoid name clashes from the above, you may alias imports (as in native Python code) using the same syntax as ``require``.
5151
Both top-level modules and submodules may be aliased: ``(import [module.sub :as sm])``.
52-
Note that none of the other convenience features or flags from :lpy:fn:`require` are available, so you will not be able to, say, refer unqualified module members into the current Namespace.
5352

5453
.. code-block::
5554
5655
(import [os.path :as path])
5756
(path/exists "test.txt") ;;=> false
5857
58+
As with Basilisp ``refers`` (and as in Python), it is possible to refer individual module members by name into the current namespace using the ``:refer`` option.
59+
It is also possible to refer all module members into the namespace using ``:refer :all``.
60+
61+
.. code-block::
62+
63+
(import [math :refer [sqrt pi]])
64+
pi ;; 3.141592653589793
65+
66+
(import [statistics :refer :all])
67+
mean ;; <function mean at 0x...>
68+
69+
.. warning::
70+
71+
Basilisp refers names into the current module in different conceptual namespaces and resolves names against those namespaces in order of precedence, preferring Basilisp members first.
72+
Referred Python module members may not resolve if other names take precedence within the current namespace context.
73+
74+
.. code-block::
75+
76+
(import [datetime :as dt :refer :all])
77+
78+
;; This name using the module alias directly will guarantee we are referencing
79+
;; the module member `datetime.time` (a class)
80+
dt/time ;; <class 'datetime.time'>
81+
82+
;; ...whereas this reference prefers the `basilisp.core` function `time`
83+
time ;; <function time at 0x...>
84+
5985
.. note::
6086

6187
Users should generally prefer to use the :lpy:fn:`ns` macro for importing modules into their namespace, rather than using the :lpy:fn:`import` form directly.
@@ -65,14 +91,9 @@ Note that none of the other convenience features or flags from :lpy:fn:`require`
6591
(ns myproject.ns
6692
(:import [os.path :as path]))
6793
68-
.. warning::
69-
70-
Unlike in Python, imported module names and aliases cannot be referred to directly in Basilisp code.
71-
Module and Namespace names are resolved separately from local names and will not resolve as unqualified names.
72-
7394
.. seealso::
7495

75-
:lpy:form:`import`, :lpy:fn:`import`, :lpy:fn:`ns-imports`, :lpy:fn:`ns-map`
96+
:lpy:form:`import`, :lpy:fn:`import`, :lpy:fn:`ns-imports`, :lpy:fn:`ns-import-refers`, :lpy:fn:`ns-map`
7697

7798
.. seealso::
7899

src/basilisp/core.lpy

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4915,6 +4915,11 @@
49154915
[ns]
49164916
(.-imports (the-ns ns)))
49174917

4918+
(defn ^:inline ns-import-refers
4919+
"Return a set of Python module members which are referred in the current namespace."
4920+
[ns]
4921+
(.-import-refers (the-ns ns)))
4922+
49184923
(defn ^:inline ns-interns
49194924
"Return a map of symbols to Vars which are interned in the current namespace."
49204925
[ns]
@@ -4950,14 +4955,26 @@
49504955
(defn ns-map
49514956
"Return a map of all the mapped symbols in the namespace.
49524957

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

49624979
(defn ^:inline ns-resolve
49634980
"Return the Var which will be resolved by the symbol in the given namespace."
@@ -4974,7 +4991,13 @@
49744991
"Import Python modules by name.
49754992

49764993
Modules may be specified either as symbols naming the full module path or as a
4977-
vector taking the form ``[full.module.path :as alias]``\\.
4994+
vector taking the form ``[full.module.path & opts]``\\. The ``opts`` should be
4995+
pairs of a keyword and a value from below, similar to :lpy:fn:`import`:
4996+
4997+
- ``:as name`` which will alias the imported module to the symbol name
4998+
- ``:refer [& syms]`` which will refer module members in the local namespace
4999+
directly
5000+
- ``:refer :all`` which will refer all module members from the namespace directly
49785001

49795002
Note that unlike in Python, ``import`` ed Python module names are always hoisted to
49805003
the current Namespace, so imported names will be available within a Namespace even

src/basilisp/lang/compiler/analyzer.py

Lines changed: 93 additions & 25 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,8 +2573,8 @@ 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 = []
2575-
for f in form.rest:
2576+
aliases, refers, refer_all = [], [], False
2577+
for f in form.rest: # pylint: disable=too-many-nested-blocks
25762578
if isinstance(f, sym.Symbol):
25772579
module_name = f
25782580
module_alias = None
@@ -2588,40 +2590,85 @@ 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
2603-
)
2604-
module_alias_sym = f.val_at(2)
2605-
if not isinstance(module_alias_sym, sym.Symbol):
2608+
"Expected options: ':as alias' or ':refer [...]'", form=f
2609+
) from None
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+
if len(module_refers) == 0:
2651+
raise ctx.AnalyzerException(
2652+
"Must refer at least one name", form=module_refers
2653+
)
2654+
2655+
for refer in module_refers:
2656+
if not isinstance(refer, sym.Symbol):
2657+
raise ctx.AnalyzerException(
2658+
"Python module refer name must be a symbol", form=refer
2659+
)
2660+
refers.append(refer.name)
2661+
2662+
ctx.put_new_symbol(
2663+
refer,
2664+
Binding(
2665+
form=refer,
2666+
name=refer.name,
2667+
local=LocalType.IMPORT,
2668+
env=ctx.get_node_env(),
2669+
),
2670+
symbol_table=ctx.symbol_table.context_boundary,
2671+
)
26252672
else:
26262673
raise ctx.AnalyzerException("symbol or vector expected for import*", form=f)
26272674

@@ -2643,6 +2690,8 @@ def _import_ast(form: ISeq, ctx: AnalyzerContext) -> Import:
26432690
return Import(
26442691
form=form,
26452692
aliases=aliases,
2693+
refers=refers,
2694+
refer_all=refer_all,
26462695
env=ctx.get_node_env(pos=ctx.syntax_position),
26472696
)
26482697

@@ -3738,7 +3787,7 @@ def __resolve_namespaced_symbol( # pylint: disable=too-many-branches
37383787

37393788
def __resolve_bare_symbol(
37403789
ctx: AnalyzerContext, form: sym.Symbol
3741-
) -> Union[Const, MaybeClass, VarRef]:
3790+
) -> Union[Const, HostField, MaybeClass, VarRef]:
37423791
"""Resolve a non-namespaced symbol into a Python name or a local Basilisp Var."""
37433792
assert form.ns is None
37443793

@@ -3767,6 +3816,25 @@ def __resolve_bare_symbol(
37673816
env=ctx.get_node_env(pos=ctx.syntax_position),
37683817
)
37693818

3819+
maybe_import_refer_module = current_ns.get_import_refer(form)
3820+
if maybe_import_refer_module is not None:
3821+
refer_module = current_ns.get_import(maybe_import_refer_module)
3822+
# For referred imports, we want to generate a fully qualified reference
3823+
# to the object, so we don't have to pollute the module with more names.
3824+
# The user won't know the difference.
3825+
return HostField(
3826+
form=form,
3827+
field=munge(form.name),
3828+
target=MaybeClass(
3829+
form=maybe_import_refer_module,
3830+
class_=munge(maybe_import_refer_module.name),
3831+
target=refer_module,
3832+
env=ctx.get_node_env(pos=ctx.syntax_position),
3833+
),
3834+
is_assignable=False,
3835+
env=ctx.get_node_env(pos=ctx.syntax_position),
3836+
)
3837+
37703838
# Allow users to resolve imported module names directly
37713839
maybe_import = current_ns.get_import(form)
37723840
if maybe_import is not None:

src/basilisp/lang/compiler/generator.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2409,6 +2409,62 @@ def _import_to_py_ast(ctx: GeneratorContext, node: Import) -> GeneratedPyAST[ast
24092409
),
24102410
)
24112411
)
2412+
2413+
refers: Optional[ast.expr] = None
2414+
if node.refer_all:
2415+
key, val = genname("k"), genname("v")
2416+
refers = ast.DictComp(
2417+
key=ast.Call(
2418+
func=_NEW_SYM_FN_NAME,
2419+
args=[ast.Name(id=key, ctx=ast.Load())],
2420+
keywords=[],
2421+
),
2422+
value=ast.Name(id=val, ctx=ast.Load()),
2423+
generators=[
2424+
ast.comprehension(
2425+
target=ast.Tuple(
2426+
elts=[
2427+
ast.Name(id=key, ctx=ast.Store()),
2428+
ast.Name(id=val, ctx=ast.Store()),
2429+
],
2430+
ctx=ast.Store(),
2431+
),
2432+
iter=ast.Call(
2433+
func=ast.Attribute(
2434+
value=ast.Call(
2435+
func=ast.Name(id="vars", ctx=ast.Load()),
2436+
args=[ast.Name(id=py_import_alias, ctx=ast.Load())],
2437+
keywords=[],
2438+
),
2439+
attr="items",
2440+
ctx=ast.Load(),
2441+
),
2442+
args=[],
2443+
keywords=[],
2444+
),
2445+
ifs=[],
2446+
is_async=0,
2447+
)
2448+
],
2449+
)
2450+
elif node.refers:
2451+
refer_keys: list[Optional[ast.expr]] = []
2452+
refer_vals: list[ast.expr] = []
2453+
for refer in node.refers:
2454+
refer_keys.append(
2455+
ast.Call(
2456+
func=_NEW_SYM_FN_NAME, args=[ast.Constant(refer)], keywords=[]
2457+
)
2458+
)
2459+
refer_vals.append(
2460+
ast.Attribute(
2461+
value=ast.Name(id=py_import_alias, ctx=ast.Load()),
2462+
attr=refer,
2463+
ctx=ast.Load(),
2464+
)
2465+
)
2466+
refers = ast.Dict(keys=refer_keys, values=refer_vals)
2467+
24122468
last = ast.Name(id=py_import_alias, ctx=ast.Load())
24132469

24142470
deps.append(
@@ -2437,7 +2493,11 @@ def _import_to_py_ast(ctx: GeneratorContext, node: Import) -> GeneratedPyAST[ast
24372493
),
24382494
)
24392495
),
2440-
keywords=[],
2496+
keywords=(
2497+
[ast.keyword(arg="refers", value=refers)]
2498+
if refers is not None
2499+
else []
2500+
),
24412501
)
24422502
)
24432503

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)