Skip to content

Commit d002d1a

Browse files
authored
Support reloading namespaces (#1035)
Fixes #1060
1 parent 5511adf commit d002d1a

File tree

6 files changed

+298
-36
lines changed

6 files changed

+298
-36
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Added
99
* Added a compiler metadata flag for suppressing warnings when Var indirection is unavoidable (#1052)
1010
* Added the `--emit-generated-python` CLI argument to control whether generated Python code strings are stored by the runtime for each compiled namespace (#1045)
11+
* Added the ability to reload namespaces using the `:reload` flag on `require` (#1060)
1112

1213
### Changed
1314
* The compiler will issue a warning when adding any alias that might conflict with any other alias (#1045)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ include = ["README.md", "LICENSE"]
3232
[tool.poetry.dependencies]
3333
python = "^3.8"
3434
attrs = ">=22.2.0"
35+
graphlib-backport = { version = "^1.1.0", python = "<3.9" }
3536
immutables = ">=0.20,<1.0.0"
3637
prompt-toolkit = ">=3.0.0,<4.0.0"
3738
pyrsistent = ">=0.18.0,<1.0.0"

src/basilisp/core.lpy

Lines changed: 92 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4944,7 +4944,7 @@
49444944
Basilisp will attempt to load a corresponding namespace starting with 'basilisp.'
49454945
automatically, aliasing the imported namespace as the requested namespace name.
49464946
This should allow for automatically importing Clojure's included core libraries such
4947-
as ``clojure.string``"
4947+
as ``clojure.string``."
49484948
[req]
49494949
(rewrite-clojure-libspec
49504950
(cond
@@ -4955,26 +4955,49 @@
49554955
(ex-info "Invalid libspec for require"
49564956
{:value req})))))
49574957

4958+
(def ^:private require-flags #{:reload :reload-all})
4959+
4960+
(defn ^:private libspecs-and-flags
4961+
"Return a vector of ``[libspecs flags]``."
4962+
[req]
4963+
(let [groups (group-by #(if (contains? require-flags %)
4964+
:flags
4965+
:libspecs)
4966+
req)]
4967+
[(:libspecs groups) (set (:flags groups))]))
4968+
49584969
(defn ^:private require-lib
4959-
"Require the library described by ``libspec`` into the Namespace ``requiring-ns``\\."
4960-
[requiring-ns libspec]
4961-
(let [required-ns-sym (:namespace libspec)]
4962-
;; In order to enable direct linking of Vars as Python variables, required
4963-
;; namespaces must be `require*`ed into the namespace. That's not possible
4964-
;; to do without a macro, so we're using this hacky approach to eval the
4965-
;; code directly (which will effectively add it to the root namespace module).
4966-
(eval (list 'require*
4967-
(if-let [ns-alias (:as libspec)]
4968-
[required-ns-sym :as ns-alias]
4969-
required-ns-sym))
4970-
requiring-ns)
4971-
;; Add a special alias for the original namespace (e.g. `clojure.*`), if we
4972-
;; rewrote the namespace on require.
4973-
(when-let [original-ns (:original-namespace libspec)]
4974-
(.add-alias requiring-ns (the-ns required-ns-sym) original-ns))
4975-
;; If an `:as-alias` is requested, apply that as well.
4976-
(when-let [as-alias (:as-alias libspec)]
4977-
(.add-alias requiring-ns (the-ns required-ns-sym) as-alias))
4970+
"Require the library described by ``libspec`` into the Namespace ``requiring-ns``\\
4971+
subject to the provided ``flags``."
4972+
[requiring-ns libspec flags]
4973+
(let [required-ns-sym (:namespace libspec)
4974+
existing-ns (find-ns required-ns-sym)]
4975+
(cond
4976+
#?@(:lpy39+
4977+
[(and existing-ns (contains? flags :reload-all))
4978+
(.reload-all (the-ns required-ns-sym))])
4979+
4980+
(and existing-ns (contains? flags :reload))
4981+
(.reload (the-ns required-ns-sym))
4982+
4983+
:else
4984+
(do
4985+
;; In order to enable direct linking of Vars as Python variables, required
4986+
;; namespaces must be `require*`ed into the namespace. That's not possible
4987+
;; to do without a macro, so we're using this hacky approach to eval the
4988+
;; code directly (which will effectively add it to the root namespace module).
4989+
(eval (list 'require*
4990+
(if-let [ns-alias (:as libspec)]
4991+
[required-ns-sym :as ns-alias]
4992+
required-ns-sym))
4993+
requiring-ns)
4994+
;; Add a special alias for the original namespace (e.g. `clojure.*`), if we
4995+
;; rewrote the namespace on require.
4996+
(when-let [original-ns (:original-namespace libspec)]
4997+
(.add-alias requiring-ns (the-ns required-ns-sym) original-ns))
4998+
;; If an `:as-alias` is requested, apply that as well.
4999+
(when-let [as-alias (:as-alias libspec)]
5000+
(.add-alias requiring-ns (the-ns required-ns-sym) as-alias))))
49785001
;; Reset the namespace to the requiring namespace, since it was likely changed
49795002
;; during the require process
49805003
(set! *ns* requiring-ns)))
@@ -5032,17 +5055,44 @@
50325055
- ``:refer [& syms]`` which will refer syms in the local namespace directly
50335056
- ``:refer :all`` which will refer all symbols from the namespace directly
50345057

5058+
Arguments may also be flags, which are optional. Flags are keywords. The following
5059+
flags are supported:
5060+
5061+
- ``:reload`` if provided, attempt to reload the namespace
5062+
- ``:reload-all`` if provided, attempt to reload all named namespaces and the
5063+
namespaces loaded by those namespaces as a directed graph
5064+
50355065
Use of ``require`` directly is discouraged in favor of the ``:require`` directive in
5036-
the :lpy:fn:`ns` macro."
5066+
the :lpy:fn:`ns` macro.
5067+
5068+
.. warning::
5069+
5070+
Reloading namespaces has many of the same limitations described for
5071+
:external:py:func:`importlib.reload_module`. Below is a non-exhaustive set of
5072+
limitations of reloading Basilisp namespace:
5073+
5074+
- Vars will be re-``def``'ed based on the current definition of the underlying
5075+
file. If the file has not changed, then the namespace file will be reloaded
5076+
according to the current :ref:`namespace_caching` settings. If a Var is
5077+
removed from the source file, it will not be removed or updated by a reload.
5078+
- References to objects from previous versions of modules (particularly those
5079+
external to the namespace) are not rebound by reloading. In Basilisp code,
5080+
this problem can be limited by disabling :ref:`inlining` and
5081+
:ref:`direct_linking`.
5082+
- Updates to type or record definitions will not be reflected to previously
5083+
instantiated objects of those types."
50375084
[& args]
5038-
(let [current-ns *ns*]
5039-
(doseq [libspec (map require-libspec args)]
5085+
(let [current-ns *ns*
5086+
groups (libspecs-and-flags args)
5087+
libspecs (first groups)
5088+
flags (second groups)]
5089+
(doseq [libspec (map require-libspec libspecs)]
50405090
(if (and (:as-alias libspec) (not (:as libspec)))
50415091
(let [alias-target (or (find-ns (:namespace libspec))
50425092
(create-ns (:namespace libspec)))]
50435093
(.add-alias current-ns alias-target (:as-alias libspec)))
50445094
(do
5045-
(require-lib current-ns libspec)
5095+
(require-lib current-ns libspec flags)
50465096

50475097
;; Add refers
50485098
(let [new-ns (the-ns (:namespace libspec))
@@ -5075,12 +5125,18 @@
50755125
``:require`` directive of the :lpy:fn:`ns` macro or the ``:use`` directive of the
50765126
``ns`` macro."
50775127
[ns-sym & filters]
5078-
(let [current-ns *ns*]
5128+
(let [current-ns *ns*
5129+
groups (group-by #(if (contains? require-flags %)
5130+
:flags
5131+
:filters)
5132+
filters)]
50795133
;; It is necessary to first require the Namespace directly, otherwise
50805134
;; when refer attempts to load the Namespace later, it will fail.
5081-
(->> (require-libspec ns-sym)
5082-
(require-lib current-ns))
5083-
(->> (apply vector ns-sym filters)
5135+
(require-lib current-ns
5136+
(require-libspec ns-sym)
5137+
(set (:flags groups)))
5138+
(->> (:filters groups)
5139+
(apply vector ns-sym)
50845140
(require-libspec)
50855141
(refer-lib current-ns))))
50865142

@@ -5113,12 +5169,16 @@
51135169
Use of ``use`` directly is discouraged in favor of the ``:use`` directive in the
51145170
:lpy:fn:`ns` macro."
51155171
[& args]
5116-
(let [current-ns *ns*]
5117-
(doseq [libspec (map require-libspec args)]
5172+
(let [current-ns *ns*
5173+
groups (libspecs-and-flags args)
5174+
libspecs (first groups)
5175+
flags (second groups)]
5176+
(doseq [libspec (map require-libspec libspecs)]
51185177
;; Remove the :refer key to avoid having require-lib
51195178
;; perform a full :refer, instead we'll use refer-lib
5120-
(->> (dissoc libspec :refer)
5121-
(require-lib current-ns))
5179+
(require-lib current-ns
5180+
(dissoc libspec :refer)
5181+
flags)
51225182
(refer-lib current-ns libspec))
51235183
nil))
51245184

src/basilisp/importer.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
cast,
2020
)
2121

22+
from typing_extensions import TypedDict
23+
2224
from basilisp.lang import compiler as compiler
2325
from basilisp.lang import reader as reader
2426
from basilisp.lang import runtime as runtime
@@ -133,12 +135,17 @@ def _is_namespace_package(path: str) -> bool:
133135
return no_inits and has_basilisp_files
134136

135137

138+
class ImporterCacheEntry(TypedDict, total=False):
139+
spec: ModuleSpec
140+
module: BasilispModule
141+
142+
136143
class BasilispImporter(MetaPathFinder, SourceLoader): # pylint: disable=abstract-method
137144
"""Python import hook to allow directly loading Basilisp code within
138145
Python."""
139146

140147
def __init__(self):
141-
self._cache: MutableMapping[str, dict] = {}
148+
self._cache: MutableMapping[str, ImporterCacheEntry] = {}
142149

143150
def find_spec(
144151
self,
@@ -379,7 +386,11 @@ def exec_module(self, module: types.ModuleType) -> None:
379386
assert isinstance(module, BasilispModule)
380387

381388
fullname = module.__name__
382-
cached = self._cache[fullname]
389+
if (cached := self._cache.get(fullname)) is None:
390+
spec = module.__spec__
391+
assert spec is not None, "Module must have a spec"
392+
cached = {"spec": spec}
393+
self._cache[spec.name] = cached
383394
cached["module"] = module
384395
spec = cached["spec"]
385396
filename = spec.loader_state["filename"]

src/basilisp/lang/runtime.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
from basilisp.lang.util import OBJECT_DUNDER_METHODS, demunge, is_abstract, munge
7272
from basilisp.util import Maybe
7373

74+
if sys.version_info >= (3, 9):
75+
import graphlib
76+
else:
77+
import graphlib # type: ignore
78+
7479
logger = logging.getLogger(__name__)
7580

7681
# Public constants
@@ -670,6 +675,49 @@ def __repr__(self):
670675
def __hash__(self):
671676
return hash(self._name)
672677

678+
def _get_required_namespaces(self) -> vec.PersistentVector["Namespace"]:
679+
"""Return a vector of all required namespaces (loaded via `require`, `use`,
680+
or `refer`).
681+
682+
This vector will include `basilisp.core` unless the namespace was created
683+
manually without requiring it."""
684+
ts: graphlib.TopologicalSorter = graphlib.TopologicalSorter()
685+
686+
def add_nodes(ns: Namespace) -> None:
687+
# basilisp.core does actually create a cycle by requiring namespaces
688+
# that require it, so we cannot add it to the topological sorter here,
689+
# or it will throw a cycle error
690+
if ns.name == CORE_NS:
691+
return
692+
693+
for aliased_ns in ns.aliases.values():
694+
ts.add(ns, aliased_ns)
695+
add_nodes(aliased_ns)
696+
697+
for referred_var in ns.refers.values():
698+
referred_ns = referred_var.ns
699+
ts.add(ns, referred_ns)
700+
add_nodes(referred_ns)
701+
702+
add_nodes(self)
703+
return vec.vector(ts.static_order())
704+
705+
def reload_all(self) -> "Namespace":
706+
"""Reload all dependency namespaces as by `Namespace.reload()`."""
707+
sorted_reload_order = self._get_required_namespaces()
708+
logger.debug(f"Computed :reload-all order: {sorted_reload_order}")
709+
710+
for ns in sorted_reload_order:
711+
ns.reload()
712+
713+
return self
714+
715+
def reload(self) -> "Namespace":
716+
"""Reload code in this namespace by reloading the underlying Python module."""
717+
with self._lock:
718+
importlib.reload(self.module)
719+
return self
720+
673721
def require(self, ns_name: str, *aliases: sym.Symbol) -> BasilispModule:
674722
"""Require the Basilisp Namespace named by `ns_name` and add any aliases given
675723
to this Namespace.
@@ -686,6 +734,7 @@ def require(self, ns_name: str, *aliases: sym.Symbol) -> BasilispModule:
686734
ns_sym = sym.symbol(ns_name)
687735
ns = self.get(ns_sym)
688736
assert ns is not None, "Namespace must exist after being required"
737+
self.add_alias(ns, ns_sym)
689738
if aliases:
690739
self.add_alias(ns, *aliases)
691740
return ns_module

0 commit comments

Comments
 (0)