Skip to content

Commit 4ced878

Browse files
authored
Don't inline classes in constructor if possible (RConsortium#481)
* unname `topNamespaceName()` name * try to use `pkg::cls()` call as constructor default if possible * add test * change class constructor parent environment to `new_class()` calling env * fix non-syntatic class names * use pkgname instead of env when deciding to not inline constructor calls * update surrounding code * Add snapshot test * Add comment * update `new_class`: `@param package` doc. * update snapshot test * test external classes with actual packages * use non-syntatic class names in tests
1 parent 498cfad commit 4ced878

16 files changed

+188
-51
lines changed

R/aaa.R

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ new_function <- function(args = NULL,
2626
topNamespaceName <- function(env = parent.frame()) {
2727
env <- topenv(env)
2828
if (!isNamespace(env)) {
29-
return()
29+
return() # print visible
3030
}
3131

32-
getNamespaceName(env)
32+
as.character(getNamespaceName(env)) # unname
3333
}
3434

3535
is_string <- function(x) {

R/base.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ base_default <- function(type) {
4444
name = quote(quote(x)),
4545
call = quote(quote({})),
4646

47-
`function` = quote(function() {}),
47+
`function` = quote(function() NULL),
4848
environment = quote(new.env(parent = emptyenv()))
4949
)}
5050

R/class-spec.R

+35-13
Original file line numberDiff line numberDiff line change
@@ -81,27 +81,49 @@ class_friendly <- function(x) {
8181
}
8282

8383
class_construct <- function(.x, ...) {
84-
eval(class_construct_expr(.x, ...))
84+
class_constructor(.x)(...)
8585
}
8686

8787

88-
class_construct_expr <- function(.x, ...) {
88+
class_construct_expr <- function(.x, envir = NULL, package = NULL) {
8989
f <- class_constructor(.x)
90+
91+
# For S7 class constructors with a non-NULL @package property
92+
# Instead of inlining the full class definition, use either
93+
# `pkgname::classname()` or `classname()`
94+
if (is_class(f) && !is.null(f@package)) {
95+
# Check if the class can be resolved as a bare symbol without pkgname::
96+
# Note: During package build, using pkg::class for a package's own symbols
97+
# will raise an error from `::`.
98+
if (identical(package, f@package)) {
99+
return(call(f@name))
100+
} else {
101+
# namespace the pkgname::classname() call
102+
cl <- as.call(list(quote(`::`), as.name(f@package), as.name(f@name)))
103+
104+
# check the call evaluates to f.
105+
# This will error if package is not installed or object is not exported.
106+
f2 <- eval(cl, baseenv())
107+
if (!identical(f, f2)) {
108+
msg <- sprintf(
109+
"`%s::%s` is not identical to the class with the same @package and @name properties",
110+
f@package, f@name
111+
)
112+
stop(msg, call. = FALSE)
113+
}
114+
return(as.call(list(cl)))
115+
}
116+
}
117+
90118
# If the constructor is a closure wrapping a simple expression, try
91119
# to extract the expression
92120
# (mostly for nicer printing and introspection.)
93121

94-
## early return if not safe to unwrap
95-
# can't unwrap if we're passing on ...
96-
if(...length()) {
97-
return(as.call(list(f, ...)))
98-
}
99-
100122
# can't unwrap if the closure is potentially important
101123
# (this can probably be relaxed to allow additional environments)
102124
fe <- environment(f)
103-
if(!identical(fe, baseenv())) {
104-
return(as.call(list(f, ...)))
125+
if (!identical(fe, baseenv())) {
126+
return(as.call(list(f)))
105127
}
106128

107129
# special case for `class_missing`
@@ -111,8 +133,8 @@ class_construct_expr <- function(.x, ...) {
111133

112134
# `new_object()` must be called from the class constructor, can't
113135
# be safely unwrapped
114-
if("new_object" %in% all.names(fb)) {
115-
return(as.call(list(f, ...)))
136+
if ("new_object" %in% all.names(fb)) {
137+
return(as.call(list(f)))
116138
}
117139

118140
# maybe unwrap body if it is a single expression wrapped in `{`
@@ -133,7 +155,7 @@ class_construct_expr <- function(.x, ...) {
133155
}
134156

135157
#else, return a call to the constructor
136-
as.call(list(f, ...))
158+
as.call(list(f))
137159
}
138160

139161
class_constructor <- function(.x) {

R/class.R

+7-8
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@
1616
#' * An S7 class, like [S7_object].
1717
#' * An S3 class wrapped by [new_S3_class()].
1818
#' * A base type, like [class_logical], [class_integer], etc.
19-
#' @param package Package name. It is good practice to set the package
20-
#' name when exporting an S7 class from a package because it prevents
21-
#' clashes if two packages happen to export a class with the same
22-
#' name.
19+
#' @param package Package name. This is automatically resolved if the class is
20+
#' defined in a package, and `NULL` otherwise.
2321
#'
24-
#' Setting `package` implies that the class is available for external use,
25-
#' so should be accompanied by exporting the constructor. Learn more
26-
#' in `vignette("packages")`.
22+
#' Note, if the class is intended for external use, the constructor should be
23+
#' exported. Learn more in `vignette("packages")`.
2724
#' @param abstract Is this an abstract class? An abstract class can not be
2825
#' instantiated.
2926
#' @param constructor The constructor function. In most cases, you can rely
@@ -134,7 +131,9 @@ new_class <- function(
134131
all_props[names(new_props)] <- new_props
135132

136133
if (is.null(constructor)) {
137-
constructor <- new_constructor(parent, all_props)
134+
constructor <- new_constructor(parent, all_props,
135+
envir = parent.frame(),
136+
package = package)
138137
}
139138

140139
object <- constructor

R/constructor.R

+38-9
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1-
new_constructor <- function(parent, properties) {
1+
new_constructor <- function(parent, properties,
2+
envir = asNamespace("S7"), package = NULL) {
23
properties <- as_properties(properties)
3-
arg_info <- constructor_args(parent, properties)
4+
arg_info <- constructor_args(parent, properties, envir, package)
45
self_args <- as_names(names(arg_info$self), named = TRUE)
56

67
if (identical(parent, S7_object) || (is_class(parent) && parent@abstract)) {
8+
new_object_call <-
9+
if (has_S7_symbols(envir, "new_object", "S7_object")) {
10+
bquote(new_object(S7_object(), ..(self_args)), splice = TRUE)
11+
} else {
12+
bquote(S7::new_object(S7::S7_object(), ..(self_args)), splice = TRUE)
13+
}
14+
715
return(new_function(
816
args = arg_info$self,
917
body = as.call(c(quote(`{`),
1018
# Force all promises here so that any errors are signaled from
1119
# the constructor() call instead of the new_object() call.
1220
unname(self_args),
13-
new_call("new_object", c(list(quote(S7_object())), self_args))
21+
new_object_call
1422
)),
15-
env = asNamespace("S7")
23+
env = envir
1624
))
1725
}
1826

@@ -42,15 +50,19 @@ new_constructor <- function(parent, properties) {
4250
parent_args <- as_names(names(arg_info$parent), named = TRUE)
4351
names(parent_args)[names(parent_args) == "..."] <- ""
4452
parent_call <- new_call(parent_name, parent_args)
45-
body <- new_call("new_object", c(parent_call, self_args))
53+
body <- new_call(
54+
if (has_S7_symbols(envir, "new_object")) "new_object" else c("S7", "new_object"),
55+
c(parent_call, self_args)
56+
)
4657

47-
env <- new.env(parent = asNamespace("S7"))
58+
env <- new.env(parent = envir)
4859
env[[parent_name]] <- parent_fun
4960

5061
new_function(args, body, env)
5162
}
5263

53-
constructor_args <- function(parent, properties = list()) {
64+
constructor_args <- function(parent, properties = list(),
65+
envir = asNamespace("S7"), package = NULL) {
5466
parent_args <- formals(class_constructor(parent))
5567

5668
# Remove read-only properties
@@ -66,7 +78,7 @@ constructor_args <- function(parent, properties = list()) {
6678

6779
self_args <- as.pairlist(lapply(
6880
setNames(, self_arg_nms),
69-
function(name) prop_default(properties[[name]]))
81+
function(name) prop_default(properties[[name]], envir, package))
7082
)
7183

7284
list(parent = parent_args,
@@ -81,8 +93,14 @@ is_property_dynamic <- function(x) is.function(x$getter)
8193
missing_args <- function(names) {
8294
lapply(setNames(, names), function(i) quote(class_missing))
8395
}
96+
8497
new_call <- function(call, args) {
85-
as.call(c(list(as.name(call)), args))
98+
if (is.character(call)) {
99+
call <- switch(length(call),
100+
as.name(call),
101+
as.call(c(quote(`::`), lapply(call, as.name))))
102+
}
103+
as.call(c(list(call), args))
86104
}
87105

88106
as_names <- function(x, named = FALSE) {
@@ -91,3 +109,14 @@ as_names <- function(x, named = FALSE) {
91109
}
92110
lapply(x, as.name)
93111
}
112+
113+
has_S7_symbols <- function(env, ...) {
114+
env <- topenv(env)
115+
if (identical(env, asNamespace("S7")))
116+
return (TRUE)
117+
if (!isNamespace(env))
118+
return (FALSE)
119+
imports <- getNamespaceImports(env)[["S7"]]
120+
symbols <- c(...) %||% getNamespaceExports("S7")
121+
all(symbols %in% imports)
122+
}

R/property.R

+2-2
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ str.S7_property <- function(object, ..., nest.lev = 0) {
155155
print(object, ..., nest.lev = nest.lev)
156156
}
157157

158-
prop_default <- function(prop) {
159-
prop$default %||% class_construct_expr(prop$class)
158+
prop_default <- function(prop, envir, package) {
159+
prop$default %||% class_construct_expr(prop$class, envir, package)
160160
}
161161

162162
#' Get/set a property

man/new_class.Rd

+5-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/class.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
foo <- new_class("foo", abstract = TRUE)
113113
foo()
114114
Condition
115-
Error in `new_object()`:
115+
Error in `S7::new_object()`:
116116
! Can't construct an object from abstract class <foo>
117117

118118
# abstract classes: can't inherit from concrete class

tests/testthat/_snaps/external-generic.md

+38
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,41 @@
55
Output
66
<S7_external_generic> foo::bar(x)
77

8+
# new_method works with both hard and soft dependencies
9+
10+
Code
11+
args(Foo)
12+
Output
13+
function (bar = t0::`An S7 Class`())
14+
NULL
15+
Code
16+
args(t2::`An S7 Class 2`)
17+
Output
18+
function (bar = t0::`An S7 Class`())
19+
NULL
20+
Code
21+
args(t2:::`An Internal Class`)
22+
Output
23+
function (foo = t0::`An S7 Class`(), bar = `An S7 Class 2`())
24+
NULL
25+
26+
---
27+
28+
Code
29+
new_class("Foo", properties = list(bar = new_class("Made Up Class", package = "t0")))
30+
Condition
31+
Error:
32+
! 'Made Up Class' is not an exported object from 'namespace:t0'
33+
Code
34+
new_class("Foo", properties = list(bar = new_class("Made Up Class", package = "Made Up Package")))
35+
Condition
36+
Error in `loadNamespace()`:
37+
! there is no package called 'Made Up Package'
38+
Code
39+
modified_class <- t0::`An S7 Class`
40+
attr(modified_class, "xyz") <- "abc"
41+
new_class("Foo", properties = list(bar = modified_class))
42+
Condition
43+
Error:
44+
! `t0::An S7 Class` is not identical to the class with the same @package and @name properties
45+

tests/testthat/t0/NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Generated by roxygen2: do not edit by hand
22

3+
export("An S7 Class")
34
export(an_s3_generic)
45
export(an_s7_generic)

tests/testthat/t0/R/t0.R

+3
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ an_s7_generic <- S7::new_generic("an_s7_generic", "x")
33

44
#' @export
55
an_s3_generic <- function(x) UseMethod("an_s3_generic")
6+
7+
#' @export
8+
`An S7 Class` <- S7::new_class("An S7 Class")

tests/testthat/t2/NAMESPACE

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Generated by roxygen2: do not edit by hand
22

3+
export("An S7 Class 2")
34
export(an_s7_class)
5+
importFrom(t0, `An S7 Class`)
46
importFrom(t0,an_s3_generic)
57
importFrom(t0,an_s7_generic)

tests/testthat/t2/R/t2.R

+12
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,25 @@ S7::method(an_s7_generic, an_s7_class) <- function(x) "foo"
1010
S7::method(an_s3_generic, an_s7_class) <- function(x) "foo"
1111

1212

13+
#' @rawNamespace importFrom(t0, `An S7 Class`)
14+
#' @export
15+
`An S7 Class 2` <- S7::new_class("An S7 Class 2", properties = list(bar = `An S7 Class`))
16+
NULL
17+
18+
`An Internal Class` <- S7::new_class("An Internal Class", properties = list(
19+
foo = `An S7 Class`,
20+
bar = `An S7 Class 2`
21+
))
22+
23+
1324
another_s7_generic <- S7::new_external_generic("t1", "another_s7_generic", "x")
1425
S7::method(another_s7_generic, S7::class_character) <- function(x) "foo"
1526
S7::method(another_s7_generic, an_s7_class) <- function(x) "foo"
1627

1728
another_s3_generic <- S7::new_external_generic("t1", "another_s3_generic", "x")
1829
S7::method(another_s3_generic, an_s7_class) <- function(x) "foo"
1930

31+
2032
.onLoad <- function(libname, pkgname) {
2133
S7::methods_register()
2234
}

tests/testthat/test-class.R

+8-5
Original file line numberDiff line numberDiff line change
@@ -232,16 +232,19 @@ test_that("c(<S7_class>, ...) gives error", {
232232
})
233233

234234
test_that("can round trip to disk and back", {
235-
foo1 <- new_class("foo1", properties = list(y = class_integer))
236-
foo2 <- new_class("foo2", properties = list(x = foo1))
237-
238-
f <- foo2(x = foo1(y = 1L))
235+
eval(quote({
236+
foo1 <- new_class("foo1", properties = list(y = class_integer))
237+
foo2 <- new_class("foo2", properties = list(x = foo1))
238+
f <- foo2(x = foo1(y = 1L))
239+
}), globalenv())
239240

241+
f <- globalenv()[["f"]]
240242
path <- tempfile()
241243
saveRDS(f, path)
242244
f2 <- readRDS(path)
243245

244-
expect_equal(f2, f)
246+
expect_equal(f, f2)
247+
rm(foo1, foo2, f, envir = globalenv())
245248
})
246249

247250

tests/testthat/test-constructor.R

-2
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,6 @@ test_that("can create constructors with missing or lazy defaults", {
160160
"Can\'t set read-only property Person@birthdate")
161161
})
162162

163-
164-
165163
test_that("Dynamic settable properties are included in constructor", {
166164
Foo <- new_class(
167165
name = "Foo", package = NULL,

0 commit comments

Comments
 (0)