Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't inline classes in constructor if possible #481

Merged
merged 14 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions R/class-spec.R
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@
} else {
# namespace the pkgname::classname() call
cl <- as.call(list(quote(`::`), as.name(f@package), as.name(f@name)))
t-kalinowski marked this conversation as resolved.
Show resolved Hide resolved

# check the call evaluates to f.
# This will error if package is not installed or object is not exported.
f2 <- eval(cl, baseenv())
if (!identical(f, f2)) {
msg <- sprintf(
"`%s::%s` is not identical to the class with the same @package and @name properties",
f@package, f@name
)
stop(msg, call. = FALSE)
}
return(as.call(list(cl)))
}
}
Expand All @@ -123,7 +134,7 @@
# `new_object()` must be called from the class constructor, can't
# be safely unwrapped
if ("new_object" %in% all.names(fb)) {
return(as.call(list(f)))

Check warning on line 137 in R/class-spec.R

View check run for this annotation

Codecov / codecov/patch

R/class-spec.R#L137

Added line #L137 was not covered by tests
}

# maybe unwrap body if it is a single expression wrapped in `{`
Expand All @@ -144,7 +155,7 @@
}

#else, return a call to the constructor
as.call(list(f))

Check warning on line 158 in R/class-spec.R

View check run for this annotation

Codecov / codecov/patch

R/class-spec.R#L158

Added line #L158 was not covered by tests
}

class_constructor <- function(.x) {
Expand Down
9 changes: 0 additions & 9 deletions tests/testthat/_snaps/constructor.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,3 @@
new_object(foo(...), y = y)
<environment: 0x0>

# package exported classes are not inlined in constructor formals

Code
formals(Bar)
Output
$foo
pkgname::Foo()


38 changes: 38 additions & 0 deletions tests/testthat/_snaps/external-generic.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,41 @@
Output
<S7_external_generic> foo::bar(x)

# new_method works with both hard and soft dependencies

Code
args(Foo)
Output
function (bar = t0::AnS7Class())
NULL
Code
args(t2::AnS7Class2)
Output
function (bar = t0::AnS7Class())
t-kalinowski marked this conversation as resolved.
Show resolved Hide resolved
NULL
Code
args(t2:::AnInternalClass)
Output
function (foo = t0::AnS7Class(), bar = AnS7Class2())
NULL

---

Code
new_class("Foo", properties = list(bar = new_class("MadeUpClass", package = "t0")))
Condition
Error:
! 'MadeUpClass' is not an exported object from 'namespace:t0'
Code
new_class("Foo", properties = list(bar = new_class("MadeUpClass", package = "MadeUpPackage")))
Condition
Error in `loadNamespace()`:
! there is no package called 'MadeUpPackage'
Code
modified_class <- t0::AnS7Class
attr(modified_class, "xyz") <- "abc"
new_class("Foo", properties = list(bar = modified_class))
Condition
Error:
! `t0::AnS7Class` is not identical to the class with the same @package and @name properties

1 change: 1 addition & 0 deletions tests/testthat/t0/NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Generated by roxygen2: do not edit by hand

export(AnS7Class)
export(an_s3_generic)
export(an_s7_generic)
3 changes: 3 additions & 0 deletions tests/testthat/t0/R/t0.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ an_s7_generic <- S7::new_generic("an_s7_generic", "x")

#' @export
an_s3_generic <- function(x) UseMethod("an_s3_generic")

#' @export
AnS7Class <- S7::new_class("AnS7Class")
2 changes: 2 additions & 0 deletions tests/testthat/t2/NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Generated by roxygen2: do not edit by hand

export(AnS7Class2)
export(an_s7_class)
importFrom(t0,AnS7Class)
importFrom(t0,an_s3_generic)
importFrom(t0,an_s7_generic)
11 changes: 11 additions & 0 deletions tests/testthat/t2/R/t2.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,24 @@ S7::method(an_s7_generic, an_s7_class) <- function(x) "foo"
S7::method(an_s3_generic, an_s7_class) <- function(x) "foo"


#' @importFrom t0 AnS7Class
#' @export
AnS7Class2 <- S7::new_class("AnS7Class2", properties = list(bar = AnS7Class))

AnInternalClass <- S7::new_class("AnInternalClass", properties = list(
foo = AnS7Class,
bar = AnS7Class2
))


another_s7_generic <- S7::new_external_generic("t1", "another_s7_generic", "x")
S7::method(another_s7_generic, S7::class_character) <- function(x) "foo"
S7::method(another_s7_generic, an_s7_class) <- function(x) "foo"

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


.onLoad <- function(libname, pkgname) {
S7::methods_register()
}
13 changes: 0 additions & 13 deletions tests/testthat/test-constructor.R
Original file line number Diff line number Diff line change
Expand Up @@ -191,16 +191,3 @@ test_that("Dynamic settable properties are included in constructor", {
expect_equal(foo@dynamic_settable, 1)

})

test_that("package exported classes are not inlined in constructor formals", {
# https://github.com/RConsortium/S7/issues/477
Foo := new_class(package = "pkgname")
Bar := new_class(properties = list(foo = Foo))

expect_identical(
formals(Bar)$foo,
quote(pkgname::Foo())
)

expect_snapshot(formals(Bar))
})
33 changes: 33 additions & 0 deletions tests/testthat/test-external-generic.R
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,39 @@ test_that("new_method works with both hard and soft dependencies", {
expect_equal(an_s3_generic(t2::an_s7_class()), "foo")
expect_equal(an_s7_generic("x"), "foo")

# test that new_class() will construct a property default as a namespaced call
# to t0::AnS7Class() (and not inline the full class object).
# As these tests grow, consider splitting this into a separate context like:
# test_that("package exported classes are not inlined in constructor formals", {...})
Foo <- new_class("Foo", properties = list(bar = t0::AnS7Class))
expect_identical(formals(Foo) , as.pairlist(alist(bar = t0::AnS7Class())))
expect_identical(formals(t2::AnS7Class2), as.pairlist(alist(bar = t0::AnS7Class())))
expect_identical(formals(t2:::AnInternalClass), as.pairlist(alist(
foo = t0::AnS7Class(), bar = AnS7Class2()
)))

expect_snapshot({
args(Foo)
args(t2::AnS7Class2)
args(t2:::AnInternalClass)
})

# test we emit informative error messages if a new_class() call with an
# external class dependency is malformed.
# https://github.com/RConsortium/S7/issues/477
expect_snapshot(error = TRUE, {
new_class("Foo", properties = list(
bar = new_class("MadeUpClass", package = "t0")
))
new_class("Foo", properties = list(
bar = new_class("MadeUpClass", package = "MadeUpPackage")
))

modified_class <- t0::AnS7Class
attr(modified_class, "xyz") <- "abc"
new_class("Foo", properties = list(bar = modified_class))
})

# Now install the soft dependency
quick_install(test_path("t1"), tmp_lib)

Expand Down
Loading