-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
import.r
201 lines (183 loc) · 8.35 KB
/
import.r
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#' Import a module into the current scope
#'
#' \code{module = import('module')} imports a specified module and makes its
#' code available via the environment-like object it returns.
#'
#' @param module an identifier specifying the full module path
#' @param attach if \code{TRUE}, attach the newly loaded module to the object
#' search path (see \code{Details})
#' @param attach_operators if \code{TRUE}, attach operators of module to the
#' object search path, even if \code{attach} is \code{FALSE}
#' @return the loaded module environment (invisible)
#'
#' @details Modules are loaded in an isolated environment which is returned, and
#' optionally attached to the object search path of the current scope (if
#' argument \code{attach} is \code{TRUE}).
#' \code{attach} defaults to \code{FALSE}. However, in interactive code it is
#' often helpful to attach packages by default. Therefore, in interactive code
#' invoked directly from the terminal only (i.e. not within modules),
#' \code{attach} defaults to the value of \code{options('import.attach')}, which
#' can be set to \code{TRUE} or \code{FALSE} depending on the user’s preference.
#'
#' \code{attach_operators} causes \emph{operators} to be attached by default,
#' because operators can only be invoked in R if they re found in the search
#' path. Not attaching them therefore drastically limits a module’s usefulness.
#'
#' Modules are searched in the module search path \code{options('import.path')}.
#' This is a vector of paths to consider, from the highest to the lowest
#' priority. The current directory is \emph{always} considered first. That is,
#' if a file \code{a.r} exists both in the current directory and in a module
#' search path, the local file \code{./a.r} will be loaded.
#'
#' Module names can be fully qualified to refer to nested paths. See
#' \code{Examples}.
#'
#' @note Unlike for packages, attaching happens \emph{locally}: if
#' \code{import} is executed in the global environment, the effect is the same.
#' Otherwise, the imported module is inserted as the parent of the current
#' \code{environment()}. When used (globally) \emph{inside} a module, the newly
#' imported module is only available inside the module’s search path, not
#' outside it (nor in other modules which might be loaded).
#'
#' @examples
#' # `a.r` is a file in the local directory containing a function `f`.
#' a = import('a')
#' a$f()
#'
#' # b/c.r is a file in path `b`, containing a function `g`.
#' import('b/c', attach = TRUE)
#' g() # No module name qualification necessary
#'
#' @seealso \code{unload}
#' @seealso \code{reload}
#' @seealso \code{module_name}
#' @export
import = function (module, attach, attach_operators = TRUE) {
module = substitute(module)
stopifnot(inherits(module, 'name'))
if (missing(attach)) {
attach = if (interactive() && is.null(module_name()))
getOption('import.attach', FALSE)
else
FALSE
}
stopifnot(class(attach) == 'logical' && length(attach) == 1)
module_path = try(find_module(module), silent = TRUE)
if (inherits(module_path, 'try-error'))
stop(attr(module_path, 'condition')$message)
containing_modules = module_init_files(module, module_path)
mapply(do_import, names(containing_modules), containing_modules)
mod_ns = do_import(as.character(module), module_path)
module_parent = parent.frame()
mod_env = exhibit_namespace(mod_ns, as.character(module), module_parent)
if (attach) {
if (identical(module_parent, .GlobalEnv))
attach(mod_env, name = environmentName(mod_env))
else
parent.env(module_parent) = mod_env
}
else if (attach_operators)
export_operators(mod_ns, module_parent)
invisible(mod_env)
}
do_import = function (module_name, module_path) {
if (is_module_loaded(module_path))
return(get_loaded_module(module_path))
# The namespace contains a module’s content. This schema is very much like
# R package organisation.
# A good resource for this is:
# <http://obeautifulcode.com/R/How-R-Searches-And-Finds-Stuff/>
namespace = structure(new.env(parent = .BaseNamespaceEnv),
name = paste('namespace', module_name, sep = ':'),
path = module_path,
class = c('namespace', 'environment'))
local(source(attr(environment(), 'path'), chdir = TRUE, local = TRUE),
envir = namespace)
cache_module(namespace)
namespace
}
exhibit_namespace = function (namespace, name, parent) {
exported_functions = lsf.str(namespace)
# Skip one parent environment because this module is hooked into the chain
# between the calling environment and its ancestor, thus sitting in its
# local object search path.
structure(list2env(sapply(exported_functions, get, envir = namespace),
parent = parent.env(parent)),
name = paste('module', name, sep = ':'),
path = module_path(namespace),
class = c('module', 'environment'))
}
export_operators = function (namespace, parent) {
# `$` cannot be overwritten, but it is generic so S3 variants of it can be
# defined. We therefore test it as well.
ops = c('+', '-', '*', '/', '^', '**', '&', '|', ':', '::', ':::', '$', '=',
'<-', '<<-', '==', '<', '<=', '>', '>=', '!=', '~', '&&', '||')
is_predefined = function (f) f %in% ops
is_op = function (f) {
prefix = strsplit(f, '\\.')[[1]][1]
is_predefined(prefix) || grepl('^%.*%$', prefix)
}
operators = Filter(is_op, lsf.str(namespace))
name = module_name(namespace)
# Skip one parent environment because this module is hooked into the chain
# between the calling environment and its ancestor, thus sitting in its
# local object search path.
op_env = structure(list2env(sapply(operators, get, envir = namespace),
parent = parent.env(parent)),
name = paste('operators', name, sep = ':'),
path = module_path(namespace),
class = c('module', 'environment'))
if (identical(parent, .GlobalEnv))
attach(op_env, name = environmentName(op_env))
else
parent.env(parent) = op_env
}
#' Unload a given module
#'
#' Unset the module variable that is being passed as a parameter, and remove the
#' loaded module from cache.
#' @param module reference to the module which should be unloaded
#' @note Any other references to the loaded modules remain unchanged, and will
#' still work. However, subsequently importing the module again will reload its
#' source files, which would not have happened without \code{unload}.
#' Unloading modules is primarily useful for testing during development, and
#' should not be used in production code.
#'
#' \code{unload} does not currently detach environments.
#' @seealso \code{import}
#' @seealso \code{reload}
#' @export
unload = function (module) {
stopifnot(inherits(module, 'module'))
module_ref = as.character(substitute(module))
rm(list = module_path(module), envir = .loaded_modules)
# unset the module reference in its scope, i.e. the caller’s environment or
# some parent thereof.
rm(list = module_ref, envir = parent.frame(), inherits = TRUE)
}
#' Reload a given module
#'
#' Remove the loaded module from the cache, forcing a reload. The newly reloaded
#' module is assigned to the module reference in the calling scope.
#' @param module reference to the module which should be unloaded
#' @note Any other references to the loaded modules remain unchanged, and will
#' still work. Reloading modules is primarily useful for testing during
#' development, and should not be used in production code.
#'
#' \code{reload} does not work correctly with attached environments.
#' @seealso \code{import}
#' @seealso \code{unload}
#' @export
reload = function (module) {
stopifnot(inherits(module, 'module'))
module_ref = as.character(substitute(module))
module_path = module_path(module)
module_name = module_name(module)
rm(list = module_path, envir = .loaded_modules)
#' @TODO Once we have `attach`, need also to take care of the search path
#' and whatnot.
mod_ns = do_import(module_name, module_path)
module_parent = parent.frame()
mod_env = exhibit_namespace(mod_ns, module_ref, module_parent)
assign(module_ref, mod_env, envir = module_parent, inherits = TRUE)
}