Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 a compiler metadata flag for suppressing warnings when Var indirection is unavoidable (#1052)
* Added the `--emit-generated-python` CLI argument to control whether generated Python code strings are stored by the runtime for each compiled namespace (#1045)
* Added the ability to reload namespaces using the `:reload` flag on `require` (#???)

### Changed
* The compiler will issue a warning when adding any alias that might conflict with any other alias (#1045)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ include = ["README.md", "LICENSE"]
[tool.poetry.dependencies]
python = "^3.8"
attrs = ">=22.2.0"
graphlib-backport = { version = "^1.1.0", python = "<3.9" }
immutables = ">=0.20,<1.0.0"
prompt-toolkit = ">=3.0.0,<4.0.0"
pyrsistent = ">=0.18.0,<1.0.0"
Expand Down
124 changes: 92 additions & 32 deletions src/basilisp/core.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -4944,7 +4944,7 @@
Basilisp will attempt to load a corresponding namespace starting with 'basilisp.'
automatically, aliasing the imported namespace as the requested namespace name.
This should allow for automatically importing Clojure's included core libraries such
as ``clojure.string``"
as ``clojure.string``."
[req]
(rewrite-clojure-libspec
(cond
Expand All @@ -4955,26 +4955,49 @@
(ex-info "Invalid libspec for require"
{:value req})))))

(def ^:private require-flags #{:reload :reload-all})

(defn ^:private libspecs-and-flags
"Return a vector of ``[libspecs flags]``."
[req]
(let [groups (group-by #(if (contains? require-flags %)
:flags
:libspecs)
req)]
[(:libspecs groups) (set (:flags groups))]))

(defn ^:private require-lib
"Require the library described by ``libspec`` into the Namespace ``requiring-ns``\\."
[requiring-ns libspec]
(let [required-ns-sym (:namespace libspec)]
;; In order to enable direct linking of Vars as Python variables, required
;; namespaces must be `require*`ed into the namespace. That's not possible
;; to do without a macro, so we're using this hacky approach to eval the
;; code directly (which will effectively add it to the root namespace module).
(eval (list 'require*
(if-let [ns-alias (:as libspec)]
[required-ns-sym :as ns-alias]
required-ns-sym))
requiring-ns)
;; Add a special alias for the original namespace (e.g. `clojure.*`), if we
;; rewrote the namespace on require.
(when-let [original-ns (:original-namespace libspec)]
(.add-alias requiring-ns (the-ns required-ns-sym) original-ns))
;; If an `:as-alias` is requested, apply that as well.
(when-let [as-alias (:as-alias libspec)]
(.add-alias requiring-ns (the-ns required-ns-sym) as-alias))
"Require the library described by ``libspec`` into the Namespace ``requiring-ns``\\
subject to the provided ``flags``."
[requiring-ns libspec flags]
(let [required-ns-sym (:namespace libspec)
existing-ns (find-ns required-ns-sym)]
(cond
#?@(:lpy39+
[(and existing-ns (contains? flags :reload-all))
(.reload-all (the-ns required-ns-sym))])

(and existing-ns (contains? flags :reload))
(.reload (the-ns required-ns-sym))

:else
(do
;; In order to enable direct linking of Vars as Python variables, required
;; namespaces must be `require*`ed into the namespace. That's not possible
;; to do without a macro, so we're using this hacky approach to eval the
;; code directly (which will effectively add it to the root namespace module).
(eval (list 'require*
(if-let [ns-alias (:as libspec)]
[required-ns-sym :as ns-alias]
required-ns-sym))
requiring-ns)
;; Add a special alias for the original namespace (e.g. `clojure.*`), if we
;; rewrote the namespace on require.
(when-let [original-ns (:original-namespace libspec)]
(.add-alias requiring-ns (the-ns required-ns-sym) original-ns))
;; If an `:as-alias` is requested, apply that as well.
(when-let [as-alias (:as-alias libspec)]
(.add-alias requiring-ns (the-ns required-ns-sym) as-alias))))
;; Reset the namespace to the requiring namespace, since it was likely changed
;; during the require process
(set! *ns* requiring-ns)))
Expand Down Expand Up @@ -5032,17 +5055,44 @@
- ``:refer [& syms]`` which will refer syms in the local namespace directly
- ``:refer :all`` which will refer all symbols from the namespace directly

Arguments may also be flags, which are optional. Flags are keywords. The following
flags are supported:

- ``:reload`` if provided, attempt to reload the namespace
- ``:reload-all`` if provided, attempt to reload all named namespaces and the
namespaces loaded by those namespaces as a directed graph

Use of ``require`` directly is discouraged in favor of the ``:require`` directive in
the :lpy:fn:`ns` macro."
the :lpy:fn:`ns` macro.

.. warning::

Reloading namespaces has many of the same limitations described for
:external:py:func:`importlib.reload_module`. Below is a non-exhaustive set of
limitations of reloading Basilisp namespace:

- Vars will be re-``def``'ed based on the current definition of the underlying
file. If the file has not changed, then the namespace file will be reloaded
according to the current :ref:`namespace_caching` settings. If a Var is
removed from the source file, it will not be removed or updated by a reload.
- References to objects from previous versions of modules (particularly those
external to the namespace) are not rebound by reloading. In Basilisp code,
this problem can be limited by disabling :ref:`inlining` and
:ref:`direct_linking`.
- Updates to type or record definitions will not be reflected to previously
instantiated objects of those types."
[& args]
(let [current-ns *ns*]
(doseq [libspec (map require-libspec args)]
(let [current-ns *ns*
groups (libspecs-and-flags args)
libspecs (first groups)
flags (second groups)]
(doseq [libspec (map require-libspec libspecs)]
(if (and (:as-alias libspec) (not (:as libspec)))
(let [alias-target (or (find-ns (:namespace libspec))
(create-ns (:namespace libspec)))]
(.add-alias current-ns alias-target (:as-alias libspec)))
(do
(require-lib current-ns libspec)
(require-lib current-ns libspec flags)

;; Add refers
(let [new-ns (the-ns (:namespace libspec))
Expand Down Expand Up @@ -5075,12 +5125,18 @@
``:require`` directive of the :lpy:fn:`ns` macro or the ``:use`` directive of the
``ns`` macro."
[ns-sym & filters]
(let [current-ns *ns*]
(let [current-ns *ns*
groups (group-by #(if (contains? require-flags %)
:flags
:filters)
filters)]
;; It is necessary to first require the Namespace directly, otherwise
;; when refer attempts to load the Namespace later, it will fail.
(->> (require-libspec ns-sym)
(require-lib current-ns))
(->> (apply vector ns-sym filters)
(require-lib current-ns
(require-libspec ns-sym)
(set (:flags groups)))
(->> (:filters groups)
(apply vector ns-sym)
(require-libspec)
(refer-lib current-ns))))

Expand Down Expand Up @@ -5113,12 +5169,16 @@
Use of ``use`` directly is discouraged in favor of the ``:use`` directive in the
:lpy:fn:`ns` macro."
[& args]
(let [current-ns *ns*]
(doseq [libspec (map require-libspec args)]
(let [current-ns *ns*
groups (libspecs-and-flags args)
libspecs (first groups)
flags (second groups)]
(doseq [libspec (map require-libspec libspecs)]
;; Remove the :refer key to avoid having require-lib
;; perform a full :refer, instead we'll use refer-lib
(->> (dissoc libspec :refer)
(require-lib current-ns))
(require-lib current-ns
(dissoc libspec :refer)
flags)
(refer-lib current-ns libspec))
nil))

Expand Down
15 changes: 13 additions & 2 deletions src/basilisp/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
cast,
)

from typing_extensions import TypedDict

from basilisp.lang import compiler as compiler
from basilisp.lang import reader as reader
from basilisp.lang import runtime as runtime
Expand Down Expand Up @@ -133,12 +135,17 @@ def _is_namespace_package(path: str) -> bool:
return no_inits and has_basilisp_files


class ImporterCacheEntry(TypedDict, total=False):
spec: ModuleSpec
module: BasilispModule


class BasilispImporter(MetaPathFinder, SourceLoader): # pylint: disable=abstract-method
"""Python import hook to allow directly loading Basilisp code within
Python."""

def __init__(self):
self._cache: MutableMapping[str, dict] = {}
self._cache: MutableMapping[str, ImporterCacheEntry] = {}

def find_spec(
self,
Expand Down Expand Up @@ -379,7 +386,11 @@ def exec_module(self, module: types.ModuleType) -> None:
assert isinstance(module, BasilispModule)

fullname = module.__name__
cached = self._cache[fullname]
if (cached := self._cache.get(fullname)) is None:
spec = module.__spec__
assert spec is not None, "Module must have a spec"
cached = {"spec": spec}
self._cache[spec.name] = cached
Comment on lines +389 to +393
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt this ever happens except in testing, but I was finding that calling monkeypatch.syspath_prepend that monkeypatch calls importlib.invalidate_caches() which was causing issues trying to access values from the cache without any guard. Now we just reconstruct the cache from the module if that does happen.

cached["module"] = module
spec = cached["spec"]
filename = spec.loader_state["filename"]
Expand Down
49 changes: 49 additions & 0 deletions src/basilisp/lang/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
from basilisp.lang.util import OBJECT_DUNDER_METHODS, demunge, is_abstract, munge
from basilisp.util import Maybe

if sys.version_info >= (3, 9):
import graphlib
else:
import graphlib # type: ignore

logger = logging.getLogger(__name__)

# Public constants
Expand Down Expand Up @@ -670,6 +675,49 @@ def __repr__(self):
def __hash__(self):
return hash(self._name)

def _get_required_namespaces(self) -> vec.PersistentVector["Namespace"]:
"""Return a vector of all required namespaces (loaded via `require`, `use`,
or `refer`).

This vector will include `basilisp.core` unless the namespace was created
manually without requiring it."""
ts: graphlib.TopologicalSorter = graphlib.TopologicalSorter()

def add_nodes(ns: Namespace) -> None:
# basilisp.core does actually create a cycle by requiring namespaces
# that require it, so we cannot add it to the topological sorter here,
# or it will throw a cycle error
if ns.name == CORE_NS:
return

for aliased_ns in ns.aliases.values():
ts.add(ns, aliased_ns)
add_nodes(aliased_ns)

for referred_var in ns.refers.values():
referred_ns = referred_var.ns
ts.add(ns, referred_ns)
add_nodes(referred_ns)

add_nodes(self)
return vec.vector(ts.static_order())

def reload_all(self) -> "Namespace":
"""Reload all dependency namespaces as by `Namespace.reload()`."""
sorted_reload_order = self._get_required_namespaces()
logger.debug(f"Computed :reload-all order: {sorted_reload_order}")

for ns in sorted_reload_order:
ns.reload()

return self

def reload(self) -> "Namespace":
"""Reload code in this namespace by reloading the underlying Python module."""
with self._lock:
importlib.reload(self.module)
return self

def require(self, ns_name: str, *aliases: sym.Symbol) -> BasilispModule:
"""Require the Basilisp Namespace named by `ns_name` and add any aliases given
to this Namespace.
Expand All @@ -686,6 +734,7 @@ def require(self, ns_name: str, *aliases: sym.Symbol) -> BasilispModule:
ns_sym = sym.symbol(ns_name)
ns = self.get(ns_sym)
assert ns is not None, "Namespace must exist after being required"
self.add_alias(ns, ns_sym)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was always my intent to add the fully-qualified name as an alias to the namespace, but it never mattered before now.

if aliases:
self.add_alias(ns, *aliases)
return ns_module
Expand Down
Loading
Loading