Skip to content

Commit ac3f691

Browse files
committed
First draft of auto-reload function
1 parent 97afc35 commit ac3f691

2 files changed

Lines changed: 143 additions & 1 deletion

File tree

R/autoreload.r

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#' Auto-reloading of modules on change
2+
#'
3+
#' @usage \special{box::enable_autoreload(..., include, exclude, on_access = FALSE)}
4+
#' @param ... ignored; present to force naming arguments
5+
#' @param include vector of unevaluated, qualified module names to auto-reload
6+
#' (optional)
7+
#' @param exclude vector of unevaluates, qualified module names to auto-reload
8+
#' (optional)
9+
#' @param on_access logical value specifying whether to reload modules every
10+
#' time they are used, or only when they are being loaded via \code{box::use}.
11+
#' @return \code{enable_autoreload} is called for its side effect and does not
12+
#' return a value.
13+
#' @details
14+
#' \code{include} and \code{exclude}, when given, are either single,
15+
#' unevaluated, qualified module names (e.g. \code{./a}; \code{my/mod}) or
16+
#' vectors of such module names (e.g. \code{c(./a, my/mod)}).
17+
#' @name auto-reload
18+
#' @export
19+
enable_autoreload = function (..., include, exclude, on_access = FALSE) {
20+
autoreload$init(on_access)
21+
includes = spec_list(substitute(include))
22+
excludes = spec_list(substitute(exclude))
23+
caller = parent.frame()
24+
map(autoreload$add_include, includes, list(caller))
25+
map(autoreload$add_exclude, excludes, list(caller))
26+
invisible()
27+
}
28+
29+
#' @rdname auto-reload
30+
#' @export
31+
disable_autoreload = function () {
32+
autoreload$reset()
33+
invisible()
34+
}
35+
36+
#' @rdname auto-reload
37+
#' @export
38+
autoreload_include = function (...) {
39+
caller = parent.frame()
40+
includes = match.call(expand.dots = FALSE)$...
41+
map(autoreload$add_include, includes, list(caller))
42+
invisible()
43+
}
44+
45+
#' @rdname auto-reload
46+
#' @export
47+
autoreload_exclude = function (...) {
48+
caller = parent.frame()
49+
excludes = match.call(expand.dots = FALSE)$...
50+
map(autoreload$add_exclude, excludes, list(caller))
51+
invisible()
52+
}
53+
54+
spec_list = function (specs) {
55+
if (identical(specs, quote(expr =))) {
56+
list()
57+
} else if (is.call(specs) && identical(specs[[1L]], quote(c))) {
58+
specs[-1L]
59+
} else {
60+
list(specs)
61+
}
62+
}
63+
64+
autoreload = local({
65+
self = environment()
66+
67+
init = function (on_access) {
68+
reset()
69+
if (on_access) {
70+
throw('Not yet implemented')
71+
} else {
72+
self$is_mod_loaded = is_mod_loaded_reload
73+
}
74+
}
75+
76+
reset = function () {
77+
self$includes = character()
78+
self$excludes = character()
79+
self$is_mod_loaded = is_mod_loaded_basic
80+
}
81+
82+
add_include = function (spec, caller) {
83+
spec = parse_spec(spec, '')
84+
info = find_mod(spec, caller)
85+
self$excludes = setdiff(self$excludes, info$source_path)
86+
self$includes = c(self$includes, info$source_path)
87+
}
88+
89+
add_exclude = function (spec, caller) {
90+
spec = parse_spec(spec, '')
91+
info = find_mod(spec, caller)
92+
self$includes = setdiff(self$includes, info$source_path)
93+
self$excludes = c(self$excludes, info$source_path)
94+
}
95+
96+
included = function (info) {
97+
path = info$source_path
98+
99+
if (length(includes) == 0L) {
100+
! path %in% excludes
101+
} else {
102+
path %in% includes
103+
}
104+
}
105+
106+
is_mod_loaded_basic = function (info) {
107+
info$source_path %in% names(loaded_mods)
108+
}
109+
110+
is_mod_loaded_reload = function (info) {
111+
is_mod_loaded_basic(info) && ! needs_reloading(info)
112+
}
113+
114+
needs_reloading = function (info) {
115+
included(info) && is_file_modified(info)
116+
}
117+
118+
reset()
119+
120+
self
121+
})
122+
123+
add_timestamp = function (info) {
124+
timestamp = file.mtime(info$source_path)
125+
mod_timestamps[[info$source_path]] = timestamp
126+
}
127+
128+
remove_timestamp = function (info) {
129+
rm(list = info$source_path, envir = mod_timestamps)
130+
}
131+
132+
is_file_modified = function (info) {
133+
prev = mod_timestamps[[info$source_path]]
134+
is.null(prev) || file.mtime(info$source_path) > prev
135+
}
136+
137+
mod_timestamps = new.env(parent = emptyenv())

R/loaded.r

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,18 @@ loaded_mods = new.env(parent = emptyenv())
2929
#' @param info the mod info of a module
3030
#' @rdname loaded
3131
is_mod_loaded = function (info) {
32-
info$source_path %in% names(loaded_mods)
32+
autoreload$is_mod_loaded(info)
3333
}
3434

3535
#' @param mod_ns module namespace environment
3636
#' @rdname loaded
3737
register_mod = function (info, mod_ns) {
3838
loaded_mods[[info$source_path]] = mod_ns
39+
# The timestamp is saved *before* the source file is loaded to prevent race
40+
# conditions in the presence of concurrent file modifications.
41+
# At worst, this means loading the module redundantly in auto-reload mode.
42+
# Doing it the other way round might cause file changes not to be noticed.
43+
add_timestamp(info)
3944
attr(loaded_mods[[info$source_path]], 'loading') = TRUE
4045
}
4146

0 commit comments

Comments
 (0)