Skip to content

Commit

Permalink
Add importing-resolve function to dynamically import Python names (#1072
Browse files Browse the repository at this point in the history
)

Closes #1065 
Closes #1070
  • Loading branch information
chrisrink10 authored Sep 22, 2024
1 parent 1fe7330 commit f0514ed
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
* Added functions to `basilisp.test` for using and combining test fixtures (#980)
* Added the `importing-resolve` function for dynamically importing and resolving a Python name (#1065, #1070)

### Fixed
* Fix a bug where the reader was double counting the CRLF newline seq in metadata (#1063)
Expand Down
31 changes: 30 additions & 1 deletion src/basilisp/core.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -4886,7 +4886,13 @@
if the ``import`` itself occurs within a function or method.
Use of ``import`` directly is discouraged in favor of the ``:import`` directive in
the :lpy:fn:`ns` macro."
the :lpy:fn:`ns` macro.
Importing and attempting to reference an import within a top level form other than
a :lpy:form:`do` is a compile-time error because the compiler cannot verify that
the imported name exists. In cases where you may need to import and reference an
imported symbol within a single top-level form, you can use
:lpy:fn:`importing-resolve`."
[& modules]
;; We need to use eval here, because there could have been an ns
;; switch inside an eval earlier, and although *ns* is correctly set
Expand Down Expand Up @@ -5065,6 +5071,12 @@
Use of ``require`` directly is discouraged in favor of the ``:require`` directive in
the :lpy:fn:`ns` macro.
Requiring and attempting to reference a required namespace within a top level form
other than a :lpy:form:`do` is a compile-time error because the compiler cannot
verify that the required name exists. In cases where you may need to require and
reference a required symbol within a single top-level form, you can use
:lpy:fn:`requiring-resolve`.
.. warning::
Reloading namespaces has many of the same limitations described for
Expand Down Expand Up @@ -5265,6 +5277,23 @@
~uses
~@imports)))

(defn importing-resolve
"Resolve the namespaced symbol ``sym`` as a Python module member. If resolution fails,
attempts to import ``sym`` 's namespace (as by :lpy:fn:`import`\\) before resolving
again."
[sym]
(if (qualified-symbol? sym)
(let [mod-name (basilisp.lang.util/munge (namespace sym))
obj-name (basilisp.lang.util/munge (name sym))
py-resolve #(some-> (get sys/modules mod-name)
(python/getattr obj-name nil))]
(or (py-resolve)
(do (eval `(import* ~(symbol (namespace sym))) *ns*)
(py-resolve))))
(throw
(ex-info "Cannot resolve an unqualified symbol"
{:sym sym}))))

(defn requiring-resolve
"Resolve the namespaced symbol ``sym`` as by :lpy:fn:`resolve`\\. If resolution fails,
attempts to require ``sym`` 's namespace (as by :lpy:fn:`require`\\) before resolving
Expand Down
12 changes: 12 additions & 0 deletions tests/basilisp/test_core_fns.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -1748,6 +1748,18 @@
;; a valid symbol in that namespace
(is (= (resolve 'basilisp.shell/sh) (requiring-resolve 'basilisp.shell/sh))))

(deftest importing-resolve-test
(is (thrown? basilisp.lang.exception/ExceptionInfo (importing-resolve 'a-symbol)))
(is (thrown? basilisp.lang.exception/ExceptionInfo (importing-resolve :a-keyword)))
(is (thrown? basilisp.lang.exception/ExceptionInfo (importing-resolve :ns.qualified/kw)))
(is (thrown? python/ImportError (importing-resolve 'not.a.real.ns/sym)))
(is (nil? (importing-resolve 'logging/fakeGetLogger)))
;; this check should succeed (in spite of the missing top-level require) because
;; the previous assertion will import 'basilisp.shell even though 'sholl is not
;; a valid symbol in that namespace
(is (= (python/getattr (get sys/modules "logging") "getLogger")
(importing-resolve 'logging/getLogger))))

(deftest ns-resolve-test
(is (= #'basilisp.core/apply (ns-resolve *ns* 'apply)))
(is (nil? (ns-resolve *ns* 'xyz)))
Expand Down

0 comments on commit f0514ed

Please sign in to comment.