diff --git a/CHANGELOG.md b/CHANGELOG.md index cffc8259..a486b3bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index c378eef8..885934ca 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -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 @@ -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 @@ -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 diff --git a/tests/basilisp/test_core_fns.lpy b/tests/basilisp/test_core_fns.lpy index 903688f3..c064b622 100644 --- a/tests/basilisp/test_core_fns.lpy +++ b/tests/basilisp/test_core_fns.lpy @@ -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)))