Skip to content

Commit 9ca238d

Browse files
committed
Remove usage of forbidden function
CRAN finally started enforcing the removal of internal functions that the R core team does not consider parts of its public API. This affects unlocking environments, even though this would be “benign” in our case. However, there is strictly speaking no need to unlock anything, so the code was rewritten (though at the cost of added complexity). This squashed commit also contains the following changes: * Test environment locking more thoroughly * Silence annoying message in test * Update outdated URL * Update roxygen2 version * Document changes
1 parent 97e0a9a commit 9ca238d

11 files changed

Lines changed: 88 additions & 44 deletions

File tree

DESCRIPTION

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: box
22
Title: Write Reusable, Composable and Modular R Code
3-
Version: 1.2.0.9000
3+
Version: 1.2.1
44
Authors@R: c(
55
person(
66
'Konrad', 'Rudolph',
@@ -39,4 +39,4 @@ Suggests:
3939
Enhances:
4040
rstudioapi
4141
VignetteBuilder: knitr
42-
RoxygenNote: 7.2.1
42+
RoxygenNote: 7.3.2

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
R_HOME ?= $(shell Rscript --vanilla -e 'cat(Sys.getenv("R_HOME"))')
1+
R_HOME ?= $(shell Rscript --vanilla -e 'cat(R.home())')
22
rscript = ${R_HOME}/bin/Rscript --no-save --no-restore
33
r = ${R_HOME}/bin/R --no-save --no-restore
44

NEWS.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
# box (development version)
1+
# box 1.2.1
22

33
## Bug fixes
44

55
* Suppress a spurious internal warning upon reloading a module, caused by a dependent module being imported more than once (#363).
66

77

8+
## Miscellaneous
9+
10+
* Changed the way environments are locked, since environment unlocking is no longer allowed by the CRAN policies.
11+
12+
813
# box 1.2.0
914

1015
## Breaking changes

R/env.r

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,6 @@ strict_extract = function (e1, e2) {
189189
invisible(x)
190190
}
191191

192-
unlock_environment = function (env) {
193-
invisible(.Call(c_unlock_env, env))
194-
}
195-
196192
find_import_env = function (x, spec, info, mod_ns) {
197193
UseMethod('find_import_env')
198194
}

R/use.r

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
#'
9898
#' A module which has not declared any exports is treated as a \emph{legacy
9999
#' module} and exports \emph{all} default-visible names (that is, all names that
100-
#' do not start with a dot (\code{.}). This usage is present only for backwards
100+
#' do not start with a dot (\code{.})). This usage is present only for backwards
101101
#' compatibility with plain \R scripts, and its usage is \emph{not recommended}
102102
#' when writing new modules.
103103
#'
@@ -314,12 +314,15 @@ use_one = function (declaration, alias, caller, use_call) {
314314
#' @param info the physical module information
315315
#' @rdname importing
316316
load_and_register = function (spec, info, caller) {
317-
mod_ns = load_mod(info)
317+
ret = load_mod(info)
318+
mod_ns = ret$mod_ns
319+
is_apex = ret$is_apex
320+
on.exit(lock_all_environments(is_apex))
318321
register_as_import(spec, info, mod_ns, caller)
319322

320323
if (is_mod_still_loading(info)) {
321324
# Module was cached but hasn’t fully loaded yet. This happens for
322-
# cyclic imports (1. A -> 2. B -> 3. A)) in step (3). To proceed, we
325+
# cyclic imports (1. A -> 2. B -> 3. A) in step (3). To proceed, we
323326
# take note of the issue and wait until we bounce back to step (1) to
324327
# perform deferred finalization.
325328

@@ -374,7 +377,7 @@ export_and_attach = function (spec, info, mod_ns, caller) {
374377
finalize_deferred(info)
375378

376379
mod_exports = mod_exports(info, spec, mod_ns)
377-
lockEnvironment(mod_exports, bindings = TRUE)
380+
defer_locking(mod_exports)
378381

379382
assign_alias(spec, mod_exports, caller)
380383
attach_to_caller(spec, info, mod_exports, mod_ns, caller)
@@ -398,38 +401,50 @@ load_from_source = function (info, mod_ns) {
398401
make_S3_methods_known(mod_ns)
399402
}
400403

401-
#' @return \code{load_mod} returns the module or package namespace environment
402-
#' of the specified module or package info.
404+
#' @return \code{load_mod} returns a named \code{list(mod_ns, is_apex)}
405+
#' containing the module or package namespace environment of the specified
406+
#' module or package info, as well as a flag specifying whether the loaded
407+
#' module is an “apex” module, i.e. not loaded from within another module. This
408+
#' information is then used to lock all environments that were created during
409+
#' the loading. — This must happen after nested modules are fully loaded, since
410+
#' deferred finalization otherwise cannot modify these environments during
411+
#' cyclic imports.
403412
#' @rdname importing
404413
load_mod = function (info) {
405414
UseMethod('load_mod')
406415
}
407416

408417
`load_mod.box$mod_info` = function (info) {
409-
if (is_mod_loaded(info)) return(loaded_mod(info))
418+
if (is_mod_loaded(info)) {
419+
return(list(mod_ns = loaded_mod(info), is_apex = is_at_apex()))
420+
}
410421

411422
# Load module/package and dependencies; register the module now, to allow
412423
# cyclic imports without recursing indefinitely — but deregister upon
413424
# failure to load.
414-
on.exit(deregister_mod(info))
425+
426+
on.exit({
427+
deregister_mod(info)
428+
lock_all_environments(is_apex)
429+
})
415430

416431
mod_ns = make_namespace(info)
432+
is_apex = defer_locking(mod_ns)
417433
register_mod(info, mod_ns)
418434
load_from_source(info, mod_ns)
419435
mod_loading_finished(info, mod_ns)
420436

421437
# Call `.on_load` hook just after loading is finished but before exporting
422438
# symbols, so that `.on_load` can modify these symbols.
423439
call_hook(mod_ns, '.on_load', mod_ns)
424-
lockEnvironment(mod_ns, bindings = TRUE)
425440

426441
on.exit()
427-
mod_ns
442+
list(mod_ns = mod_ns, is_apex = is_apex)
428443
}
429444

430445
`load_mod.box$pkg_info` = function (info) {
431446
pkg = info$name
432-
base::.getNamespace(pkg) %||% loadNamespace(pkg)
447+
list(mod_ns = base::.getNamespace(pkg) %||% loadNamespace(pkg), is_apex = is_at_apex())
433448
}
434449

435450
#' @return \code{mod_exports} returns an export environment containing the
@@ -517,6 +532,7 @@ assign_temp_alias = function (spec, caller) {
517532
create_mod_alias = is.null(spec$attach) || spec$explicit
518533
if (! create_mod_alias) return()
519534

535+
self = environment()
520536
callers = list()
521537

522538
binding = function (mod_exports) {
@@ -527,25 +543,47 @@ assign_temp_alias = function (spec, caller) {
527543
sys.calls()
528544
)), 1L)
529545
frame = sys.frame(mod_exports_frame_index)
530-
env = frame$env
531-
assign('callers', append(callers, env), envir = parent.env(environment()))
546+
self$callers = c(callers, frame$env)
532547

533548
# FIXME: Do we need to create transitive placeholder active bindings?
534-
structure(list(), class = 'placeholder')
549+
structure(list(), class = 'box$placeholder')
535550
} else {
536551
# Resolve assignments
537552
for (env in callers) {
538-
box_unlock_binding(spec$alias, env)
539553
assign(spec$alias, mod_exports, envir = env)
540-
lockBinding(spec$alias, env)
554+
defer_locking(env)
541555
}
556+
542557
# Replace myself
543-
unlock_environment(caller)
544558
rm(list = spec$alias, envir = caller)
545559
assign(spec$alias, mod_exports, envir = caller)
546-
lockEnvironment(caller)
547560
}
548561
}
549562

550563
makeActiveBinding(spec$alias, structure(binding, class = 'box$placeholder'), caller)
551564
}
565+
566+
lock_info = new.env(parent = emptyenv())
567+
lock_info$envs = list()
568+
569+
defer_locking = function (ns) {
570+
is_apex = is_at_apex()
571+
lock_info$envs = c(lock_info$envs, list(ns))
572+
is_apex
573+
}
574+
575+
is_at_apex = function () {
576+
length(lock_info$envs) == 0L
577+
}
578+
579+
lock_all_environments = function (is_apex) {
580+
if (! is_apex) {
581+
return()
582+
}
583+
584+
for (ns in lock_info$envs) {
585+
lockEnvironment(ns, bindings = TRUE)
586+
}
587+
588+
lock_info$envs = list()
589+
}

src/exports.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
#include "Rinternals.h"
33

44
SEXP strict_extract(SEXP e1, SEXP e2);
5-
SEXP unlock_env(SEXP env);
65

76
static const R_CallMethodDef methods[] = {
87
{"c_strict_extract", (DL_FUNC) &strict_extract, 2},
9-
{"c_unlock_env", (DL_FUNC) &unlock_env, 1},
108
{NULL, NULL, 0}
119
};
1210

src/unlock_env.c

Lines changed: 0 additions & 13 deletions
This file was deleted.

tests/testthat/support/run-shiny.r

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
app = box::file('shiny-app')
22
tryCatch(
3-
shiny::runApp(app, launch.browser = FALSE),
3+
suppressMessages(shiny::runApp(app, launch.browser = FALSE)),
44
error = invisible
55
)

tests/testthat/test-basic.r

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,14 @@ test_that('hidden objects are not exported', {
6868
test_that('module bindings are locked', {
6969
box::use(mod/a)
7070

71+
ns = attr(a, 'namespace')
72+
7173
expect_true(environmentIsLocked(a))
74+
expect_true(environmentIsLocked(ns))
7275
expect_true(bindingIsLocked('get_counter', a))
7376
expect_true(bindingIsLocked('modname', a))
77+
expect_true(bindingIsLocked('get_counter', ns))
78+
expect_true(bindingIsLocked('modname', ns))
7479

7580
err = try({a$counter = 2L}, silent = TRUE)
7681
expect_s3_class(err, 'try-error')

tests/testthat/test-cyclic.r

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ context('circular dependencies')
22

33
test_that('cyclic dependencies load in finite time', {
44
box::use(mod/cyclic_a)
5-
expect_true(TRUE)
5+
ns = attr(cyclic_a, 'namespace')
6+
expect_true(environmentIsLocked(cyclic_a))
7+
expect_true(bindingIsLocked('name', cyclic_a))
8+
expect_true(environmentIsLocked(ns))
9+
expect_true(bindingIsLocked('name', ns))
610
})
711

812
test_that('cyclic import fully loads dependencies', {
913
box::use(a = mod/cyclic_a)
1014
box::use(b = mod/cyclic_b)
15+
ns_a = attr(a, 'namespace')
16+
ns_b = attr(b, 'namespace')
1117

1218
expect_equal(a$name, 'a')
1319
expect_equal(b$name, 'b')
@@ -19,4 +25,13 @@ test_that('cyclic import fully loads dependencies', {
1925
expect_equal(a$b$a$name, 'a')
2026
expect_equal(b$a$b$a_name(), 'a')
2127
expect_equal(b$a$b$name, 'b')
28+
29+
expect_true(environmentIsLocked(a))
30+
expect_true(bindingIsLocked('name', a))
31+
expect_true(environmentIsLocked(b))
32+
expect_true(bindingIsLocked('name', b))
33+
expect_true(environmentIsLocked(ns_a))
34+
expect_true(bindingIsLocked('name', ns_a))
35+
expect_true(environmentIsLocked(ns_b))
36+
expect_true(bindingIsLocked('name', ns_b))
2237
})

0 commit comments

Comments
 (0)