diff --git a/.Rbuildignore b/.Rbuildignore index 671a5f1e..f9d9a651 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -15,3 +15,6 @@ ^pkgdown$ ^cran-comments\.md$ ^CRAN-SUBMISSION$ +^compile_commands\.json$ +^\.cache$ +^\.vscode$ diff --git a/.gitignore b/.gitignore index 683065ab..931b8c42 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ script.R inst/doc docs .Rhistory +compile_commands.json +.cache +.vscode diff --git a/DESCRIPTION b/DESCRIPTION index 33a80436..98d65945 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -45,4 +45,5 @@ Config/testthat/parallel: TRUE Config/testthat/start-first: external-generic Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2 +Config/build/compilation-database: true diff --git a/NEWS.md b/NEWS.md index e55f3ce9..91a42e85 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # S7 (development version) +* The default object constructor returned by `new_class()` has been updated. + It now accepts missing and lazy (promise) property defaults. Additionally, + all custom property setters are now consistently invoked by the default + constructor. If you're using S7 in an R package, you'll need to re-document + to ensure that your docs match the updated usage (#438). + * Fixed an issue where a custom property `getter()` would infinitely recurse when accessing itself (reported in #403, fixed in #406). diff --git a/R/aaa.R b/R/aaa.R new file mode 100644 index 00000000..6b33979d --- /dev/null +++ b/R/aaa.R @@ -0,0 +1,9 @@ + + +`%||%` <- function(x, y) if (is.null(x)) y else x + +new_function <- function(args = NULL, + body = NULL, + env = asNamespace("S7")) { + as.function.default(c(args, body) %||% list(NULL), env) +} diff --git a/R/base.R b/R/base.R index 753da654..0e7dc40a 100644 --- a/R/base.R +++ b/R/base.R @@ -1,16 +1,16 @@ new_base_class <- function(name, constructor_name = name) { force(name) - constructor <- function(.data = class_missing) { - if (is_class_missing(.data)) { - .data <- base_default(name) - } - .data - } + constructor <- new_function( + args = list(.data = base_default(name)), + body = quote(.data), + env = baseenv() + ) validator <- function(object) { if (base_class(object) != name) { - sprintf("Underlying data must be <%s> not <%s>", name, base_class(object)) + sprintf("Underlying data must be <%s> not <%s>", + name, base_class(object)) } } @@ -35,8 +35,8 @@ base_default <- function(type) { list = list(), expression = expression(), - `function` = function() {}, - environment = new.env(parent = emptyenv()) + `function` = quote(function() {}), + environment = quote(new.env(parent = emptyenv())) )} diff --git a/R/class-spec.R b/R/class-spec.R index 525766f6..74ab7db2 100644 --- a/R/class-spec.R +++ b/R/class-spec.R @@ -80,21 +80,75 @@ class_friendly <- function(x) { ) } -class_constructor <- function(.x, ...) { +class_construct <- function(.x, ...) { + eval(class_construct_expr(.x, ...)) +} + + +class_construct_expr <- function(.x, ...) { + f <- class_constructor(.x) + # If the constructor is a closure wrapping a simple expression, try + # to extract the expression + # (mostly for nicer printing and introspection.) + + ## early return if not safe to unwrap + # can't unwrap if we're passing on ... + if(...length()) { + return(as.call(list(f, ...))) + } + + # can't unwrap if the closure is potentially important + # (this can probably be relaxed to allow additional environments) + fe <- environment(f) + if(!identical(fe, baseenv())) { + return(as.call(list(f, ...))) + } + + # special case for `class_missing` + if (identical(body(f) -> fb, quote(expr =))) { + return(quote(expr =)) + } + + # `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, ...))) + } + + # maybe unwrap body if it is a single expression wrapped in `{` + if (length(fb) == 2L && identical(fb[[1L]], quote(`{`))) + fb <- fb[[2L]] + + # If all the all the work happens in the promise to the `.data` arg, + # return the `.data` expression. + ff <- formals(f) + if ((identical(fb, quote(.data))) && + identical(names(ff), ".data")) { + return(ff$.data) + } + + # if all the work happens in the function body, return the body. + if (is.null(ff)) { + return(fb) + } + + #else, return a call to the constructor + as.call(list(f, ...)) +} + +class_constructor <- function(.x) { switch(class_type(.x), - NULL = function() NULL, - any = function() NULL, - S4 = function(...) methods::new(.x, ...), - S7 = .x, - S7_base = .x$constructor, - S7_union = class_constructor(.x$classes[[1]]), - S7_S3 = .x$constructor, - stop(sprintf("Can't construct %s", class_friendly(.x)), call. = FALSE) + NULL = function() NULL, + missing = new_function(, quote(expr =), baseenv()), + any = function() NULL, + S4 = function(...) methods::new(.x, ...), + S7 = .x, + S7_base = .x$constructor, + S7_union = class_constructor(.x$classes[[1]]), + S7_S3 = .x$constructor, + stop(sprintf("Can't construct %s", class_friendly(.x)), call. = FALSE) ) } -class_construct <- function(.x, ...) { - class_constructor(.x)(...) -} class_validate <- function(class, object) { validator <- switch(class_type(class), diff --git a/R/class.R b/R/class.R index 426387bb..fe6521fc 100644 --- a/R/class.R +++ b/R/class.R @@ -260,17 +260,9 @@ new_object <- function(.parent, ...) { attr(object, "S7_class") <- class class(object) <- class_dispatch(class) - supplied_props <- nms[!vlapply(args, is_class_missing)] - for (prop in supplied_props) { - prop(object, prop, check = FALSE) <- args[[prop]] - } - - # We have to fill in missing values after setting the initial properties, - # because custom setters might set property values - missing_props <- setdiff(nms, union(supplied_props, names(attributes(object)))) - for (prop in missing_props) { - prop(object, prop, check = FALSE) <- prop_default(class@properties[[prop]]) - } + # Set properties. This will potentially invoke custom property setters + for (name in names(args)) + prop(object, name, check = FALSE) <- args[[name]] # Don't need to validate if parent class already validated, # i.e. it's a non-abstract S7 class diff --git a/R/constructor.R b/R/constructor.R index 1319e4e6..fb24231d 100644 --- a/R/constructor.R +++ b/R/constructor.R @@ -1,10 +1,11 @@ new_constructor <- function(parent, properties) { + properties <- as_properties(properties) arg_info <- constructor_args(parent, properties) - self_args <- as_names(arg_info$self, named = TRUE) + self_args <- as_names(names(arg_info$self), named = TRUE) if (identical(parent, S7_object) || (is_class(parent) && parent@abstract)) { return(new_function( - args = missing_args(arg_info$self), + args = arg_info$self, body = new_call("new_object", c(list(quote(S7_object())), self_args)), env = asNamespace("S7") )) @@ -13,16 +14,16 @@ new_constructor <- function(parent, properties) { if (is_class(parent)) { parent_name <- parent@name parent_fun <- parent - args <- missing_args(union(arg_info$parent, arg_info$self)) + args <- modify_list(arg_info$parent, arg_info$self) } else if (is_base_class(parent)) { parent_name <- parent$constructor_name parent_fun <- parent$constructor - args <- missing_args(union(arg_info$parent, arg_info$self)) + args <- modify_list(arg_info$parent, arg_info$self) } else if (is_S3_class(parent)) { parent_name <- paste0("new_", parent$class[[1]]) parent_fun <- parent$constructor args <- formals(parent$constructor) - args[arg_info$self] <- missing_args(arg_info$self) + args[names(arg_info$self)] <- arg_info$self } else { # user facing error in S7_class() stop("Unsupported `parent` type", call. = FALSE) @@ -33,7 +34,8 @@ new_constructor <- function(parent, properties) { args[names(args) == "..."] <- list(quote(expr = )) } - parent_args <- as_names(arg_info$parent, named = TRUE) + parent_args <- as_names(names(arg_info$parent), named = TRUE) + names(parent_args)[names(parent_args) == "..."] <- "" parent_call <- new_call(parent_name, parent_args) body <- new_call("new_object", c(parent_call, self_args)) @@ -44,34 +46,32 @@ new_constructor <- function(parent, properties) { } constructor_args <- function(parent, properties = list()) { - parent_args <- names2(formals(class_constructor(parent))) + parent_args <- formals(class_constructor(parent)) - self_args <- names2(properties) + self_arg_nms <- names2(properties) # Remove dynamic arguments - self_args <- self_args[vlapply(properties, function(x) is.null(x$getter))] + self_arg_nms <- self_arg_nms[vlapply(properties, function(x) is.null(x$getter))] + if (is_class(parent) && !parent@abstract) { # Remove any parent properties; can't use parent_args() since the constructor # might automatically set some properties. - self_args <- setdiff(self_args, names2(parent@properties)) + self_arg_nms <- setdiff(self_arg_nms, names2(parent@properties)) } - list( - parent = parent_args, - self = self_args + self_args <- as.pairlist(lapply( + setNames(, self_arg_nms), + function(name) prop_default(properties[[name]])) ) + + list(parent = parent_args, + self = self_args) } + # helpers ----------------------------------------------------------------- -new_function <- function(args, body, env) { - f <- function() {} - formals(f) <- args - body(f) <- body - environment(f) <- env - attr(f, "srcref") <- NULL +is_property_dynamic <- function(x) is.function(x$getter) - f -} missing_args <- function(names) { lapply(setNames(, names), function(i) quote(class_missing)) } diff --git a/R/property.R b/R/property.R index 66c53e12..2bf677ec 100644 --- a/R/property.R +++ b/R/property.R @@ -7,7 +7,8 @@ #' #' By specifying a `getter` and/or `setter`, you can make the property #' "dynamic" so that it's computed when accessed or has some non-standard -#' behaviour when modified. +#' behaviour when modified. Dynamic properties are not included as an argument +#' to the default class constructor. #' #' @param class Class that the property must be an instance of. #' See [as_class()] for details. @@ -31,8 +32,10 @@ #' The validator will be called after the `class` has been verified, so #' your code can assume that `value` has known type. #' @param default When an object is created and the property is not supplied, -#' what should it default to? If `NULL`, defaults to the "empty" instance -#' of `class`. +#' what should it default to? If `NULL`, it defaults to the "empty" instance +#' of `class`. This can also be a quoted call, which then becomes a standard +#' function promise in the default constructor, evaluated at the time the +#' object is constructed. #' @param name Property name, primarily used for error messages. Generally #' don't need to set this here, as it's more convenient to supply as a #' the element name when defining a list of properties. If both `name` @@ -59,9 +62,14 @@ #' my_clock <- clock() #' my_clock@now; Sys.sleep(1) #' my_clock@now -#' # This property is read only +#' # This property is read only, because there is a 'getter' but not a 'setter' #' try(my_clock@now <- 10) #' +#' # Because the property is dynamic, it is not included as an +#' # argument to the default constructor +#' try(clock(now = 10)) +#' args(clock) +#' #' # These can be useful if you want to deprecate a property #' person <- new_class("person", properties = list( #' first_name = class_character, @@ -81,6 +89,27 @@ #' hadley@firstName #' hadley@firstName <- "John" #' hadley@first_name +#' +#' # Properties can have default values that are quoted calls. +#' # These become standard function promises in the default constructor, +#' # evaluated at the time the object is constructed. +#' stopwatch <- new_class("stopwatch", properties = list( +#' starttime = new_property(class = class_POSIXct, default = quote(Sys.time())), +#' totaltime = new_property(getter = function(self) +#' difftime(Sys.time(), self@starttime, units = "secs")) +#' )) +#' args(stopwatch) +#' round(stopwatch()@totaltime) +#' round(stopwatch(Sys.time() - 1)@totaltime) +#' +#' # Properties can also have a 'missing' default value, making them +#' # required arguments to the default constructor. +#' # You can generate a missing arg with `quote(expr =)` or `rlang::missing_arg()` +#' Person <- new_class("Person", properties = list( +#' name = new_property(class_character, default = quote(expr = )) +#' )) +#' try(Person()) +#' Person("Alice") new_property <- function(class = class_any, getter = NULL, setter = NULL, @@ -88,7 +117,9 @@ new_property <- function(class = class_any, default = NULL, name = NULL) { class <- as_class(class) - if (!is.null(default) && !class_inherits(default, class)) { + if (!is.null(default) && + !(is.call(default) || is.symbol(default)) && # allow promises + !class_inherits(default, class)) { msg <- sprintf("`default` must be an instance of %s, not a %s", class_desc(class), obj_desc(default)) stop(msg) } @@ -131,7 +162,7 @@ str.S7_property <- function(object, ..., nest.lev = 0) { } prop_default <- function(prop) { - prop$default %||% class_construct(prop$class) + prop$default %||% class_construct_expr(prop$class) } #' Get/set a property diff --git a/R/special.R b/R/special.R index 1afbae03..2ecebe2c 100644 --- a/R/special.R +++ b/R/special.R @@ -10,10 +10,12 @@ #' @examples #' foo <- new_generic("foo", "x") #' method(foo, class_numeric) <- function(x) "number" +#' method(foo, class_missing) <- function(x) "missing" #' method(foo, class_any) <- function(x) "fallback" #' #' foo(1) #' foo() +#' foo("") class_missing <- structure(list(), class = "S7_missing") is_class_missing <- function(x) inherits(x, "S7_missing") diff --git a/R/utils.R b/R/utils.R index 9e6ea4d6..e04ec31b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -11,7 +11,6 @@ global_variables <- function(names) { vlapply <- function(X, FUN, ...) vapply(X = X, FUN = FUN, FUN.VALUE = logical(1), ...) vcapply <- function(X, FUN, ...) vapply(X = X, FUN = FUN, FUN.VALUE = character(1), ...) -`%||%` <- function(x, y) if (is.null(x)) y else x method_signature <- function(generic, signature) { single <- length(generic@dispatch_args) == 1 @@ -113,13 +112,9 @@ show_function <- function(x, constructor = FALSE) { args <- formals(x) if (constructor) { - args <- lapply(args, function(x) { - if (identical(x, quote(class_missing))) { - quote(expr = ) - } else { - x - } - }) + # don't show the defaults arg values in the constructor, keep it compact + # TODO: do show the default values next to properties in class printouts. + args <- lapply(args, function(q) quote(expr =)) } show_args(args, suffix = " {...}") @@ -136,6 +131,20 @@ show_args <- function(x, name = "function", suffix = "") { paste0(name, "(", args, ")", suffix) } +modify_list <- function (x, new_vals) { + stopifnot(is.list(x) || is.pairlist(x), all(nzchar(names2(x)))) + + if (length(new_vals)) { + nms <- names2(new_vals) + if (!all(nzchar(nms))) + stop("all elements in `new_vals` must be named") + x[nms] <- new_vals + } + + x +} + + # For older versions of R ---------------------------------------------------- deparse1 <- function(expr, collapse = " ", width.cutoff = 500L, ...) { paste(deparse(expr, width.cutoff, ...), collapse = collapse) diff --git a/R/valid.R b/R/valid.R index ed2aaf27..65173557 100644 --- a/R/valid.R +++ b/R/valid.R @@ -103,14 +103,14 @@ validate <- function(object, recursive = TRUE, properties = TRUE) { validate_properties <- function(object, class) { errors <- character() - for (prop in class@properties) { + for (prop_obj in class@properties) { # Don't validate dynamic properties - if (!is.null(prop$getter)) { + if (!is.null(prop_obj$getter)) { next } - value <- prop(object, prop$name) - errors <- c(errors, prop_validate(prop, value)) + value <- prop(object, prop_obj$name) + errors <- c(errors, prop_validate(prop_obj, value)) } errors diff --git a/R/zzz.R b/R/zzz.R index b4f463e5..b7ba1c36 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -88,25 +88,36 @@ check_subsettable <- function(x, allow_env = FALSE) { invisible(TRUE) } -S7_generic <- new_class( - name = "S7_generic", - properties = list( - name = class_character, - methods = class_environment, - dispatch_args = class_character - ), - parent = class_function -) +S7_generic <- NULL + +on_load_define_S7_generic <- function() { + # we do this in .onLoad() because dynlib `prop_` symbol + # is not available at pkg build time, and new_class() + # errors if `@` is not usable. + S7_generic <<- new_class( + name = "S7_generic", + properties = list( + name = class_character, + methods = class_environment, + dispatch_args = class_character + ), + parent = class_function + ) +} + methods::setOldClass(c("S7_generic", "function", "S7_object")) is_S7_generic <- function(x) inherits(x, "S7_generic") -S7_method <- new_class("S7_method", - parent = class_function, - properties = list( - generic = S7_generic, - signature = class_list + +S7_method <- NULL + +on_load_define_S7_method <- function() { + S7_method <<- new_class( + "S7_method", + parent = class_function, + properties = list(generic = S7_generic, signature = class_list) ) -) +} methods::setOldClass(c("S7_method", "function", "S7_object")) # hooks ------------------------------------------------------------------- @@ -121,6 +132,8 @@ methods::setOldClass(c("S7_method", "function", "S7_object")) .onLoad <- function(...) { activate_backward_compatiblility() + on_load_define_S7_generic() + on_load_define_S7_method() on_load_make_convert_generic() on_load_define_ops() on_load_define_or_methods() diff --git a/man/class_missing.Rd b/man/class_missing.Rd index 058aa187..b745e66c 100644 --- a/man/class_missing.Rd +++ b/man/class_missing.Rd @@ -18,9 +18,11 @@ i.e. it's missing in the sense of \code{\link[=missing]{missing()}}, not in the \examples{ foo <- new_generic("foo", "x") method(foo, class_numeric) <- function(x) "number" +method(foo, class_missing) <- function(x) "missing" method(foo, class_any) <- function(x) "fallback" foo(1) foo() +foo("") } \keyword{datasets} diff --git a/man/new_property.Rd b/man/new_property.Rd index 70e7cd58..461e64b0 100644 --- a/man/new_property.Rd +++ b/man/new_property.Rd @@ -40,8 +40,10 @@ The validator will be called after the \code{class} has been verified, so your code can assume that \code{value} has known type.} \item{default}{When an object is created and the property is not supplied, -what should it default to? If \code{NULL}, defaults to the "empty" instance -of \code{class}.} +what should it default to? If \code{NULL}, it defaults to the "empty" instance +of \code{class}. This can also be a quoted call, which then becomes a standard +function promise in the default constructor, evaluated at the time the +object is constructed.} \item{name}{Property name, primarily used for error messages. Generally don't need to set this here, as it's more convenient to supply as a @@ -58,7 +60,8 @@ limited to a data of a specific \code{class}. By specifying a \code{getter} and/or \code{setter}, you can make the property "dynamic" so that it's computed when accessed or has some non-standard -behaviour when modified. +behaviour when modified. Dynamic properties are not included as an argument +to the default class constructor. } \examples{ # Simple properties store data inside an object @@ -80,9 +83,14 @@ clock <- new_class("clock", properties = list( my_clock <- clock() my_clock@now; Sys.sleep(1) my_clock@now -# This property is read only +# This property is read only, because there is a 'getter' but not a 'setter' try(my_clock@now <- 10) +# Because the property is dynamic, it is not included as an +# argument to the default constructor +try(clock(now = 10)) +args(clock) + # These can be useful if you want to deprecate a property person <- new_class("person", properties = list( first_name = class_character, @@ -102,4 +110,25 @@ hadley <- person(first_name = "Hadley") hadley@firstName hadley@firstName <- "John" hadley@first_name + +# Properties can have default values that are quoted calls. +# These become standard function promises in the default constructor, +# evaluated at the time the object is constructed. +stopwatch <- new_class("stopwatch", properties = list( + starttime = new_property(class = class_POSIXct, default = quote(Sys.time())), + totaltime = new_property(getter = function(self) + difftime(Sys.time(), self@starttime, units = "secs")) +)) +args(stopwatch) +round(stopwatch()@totaltime) +round(stopwatch(Sys.time() - 1)@totaltime) + +# Properties can also have a 'missing' default value, making them +# required arguments to the default constructor. +# You can generate a missing arg with `quote(expr =)` or `rlang::missing_arg()` +Person <- new_class("Person", properties = list( + name = new_property(class_character, default = quote(expr = )) +)) +try(Person()) +Person("Alice") } diff --git a/tests/testthat/_snaps/class.md b/tests/testthat/_snaps/class.md index 3cb70b94..64515b4c 100644 --- a/tests/testthat/_snaps/class.md +++ b/tests/testthat/_snaps/class.md @@ -33,7 +33,7 @@ .. ..$ validator: NULL .. ..$ default : NULL @ abstract : logi FALSE - @ constructor: function (x = class_missing, y = class_missing) + @ constructor: function (x = integer(0), y = integer(0)) @ validator : NULL Code str(list(foo2)) diff --git a/tests/testthat/_snaps/constructor.md b/tests/testthat/_snaps/constructor.md index 5f48e511..8c824bb9 100644 --- a/tests/testthat/_snaps/constructor.md +++ b/tests/testthat/_snaps/constructor.md @@ -9,21 +9,21 @@ Code new_constructor(S7_object, as_properties(list(x = class_numeric, y = class_numeric))) Output - function (x = class_missing, y = class_missing) + function (x = integer(0), y = integer(0)) new_object(S7_object(), x = x, y = y) Code foo <- new_class("foo", parent = class_character) new_constructor(foo, list()) Output - function (.data = class_missing) + function (.data = character(0)) new_object(foo(.data = .data)) Code foo2 <- new_class("foo2", parent = foo) new_constructor(foo2, list()) Output - function (.data = class_missing) + function (.data = character(0)) new_object(foo2(.data = .data)) @@ -38,8 +38,8 @@ Code new_constructor(class_factor, as_properties(list(x = class_numeric, y = class_numeric))) Output - function (.data = integer(), levels = character(), x = class_missing, - y = class_missing) + function (.data = integer(), levels = character(), x = integer(0), + y = integer(0)) new_object(new_factor(.data = .data, levels = levels), x = x, y = y) @@ -56,7 +56,7 @@ Code new_constructor(foo1, as_properties(list(y = class_double))) Output - function (y = class_missing) + function (y = numeric(0)) new_object(S7_object(), y = y) @@ -65,7 +65,7 @@ Code new_constructor(foo, list(y = class_double)) Output - function (..., y = class_missing) - new_object(foo(... = ...), y = y) + function (..., y = numeric(0)) + new_object(foo(...), y = y) diff --git a/tests/testthat/test-constructor.R b/tests/testthat/test-constructor.R index 45a73b20..0796ece5 100644 --- a/tests/testthat/test-constructor.R +++ b/tests/testthat/test-constructor.R @@ -1,26 +1,26 @@ test_that("generates correct arguments from parent + properties", { # No arguments args <- constructor_args(S7_object) - expect_equal(args$self, character()) - expect_equal(args$parent, character()) + expect_equal(args$self, pairlist()) + expect_equal(args$parent, pairlist()) # Includes properties args <- constructor_args(S7_object, as_properties(list(x = class_numeric))) - expect_equal(args$self, "x") - expect_equal(args$parent, character()) + expect_equal(args$self, pairlist(x = integer())) + expect_equal(args$parent, pairlist()) # unless they're dynamic args <- constructor_args(S7_object, as_properties(list(x = new_property(getter = function(self) 10))) ) - expect_equal(args$self, character()) - expect_equal(args$parent, character()) + expect_equal(args$self, pairlist()) + expect_equal(args$parent, pairlist()) # Includes parent properties foo <- new_class("foo", properties = list(x = class_numeric)) args <- constructor_args(foo, as_properties(list(y = class_numeric))) - expect_equal(args$self, "y") - expect_equal(args$parent, "x") + expect_equal(args$self, pairlist(y = integer())) + expect_equal(args$parent, pairlist(x = integer())) # But only those in the constructor foo <- new_class("foo", @@ -28,8 +28,8 @@ test_that("generates correct arguments from parent + properties", { constructor = function() new_object(x = 1) ) args <- constructor_args(foo, as_properties(list(y = class_numeric))) - expect_equal(args$self, "y") - expect_equal(args$parent, character()) + expect_equal(args$self, pairlist(y = integer())) + expect_equal(args$parent, pairlist()) }) test_that("generates meaningful constructors", { @@ -85,3 +85,66 @@ test_that("can use `...` in parent constructor", { expect_equal(bar(2)@x, list(2)) expect_equal(bar(y = 2)@x, list()) }) + +test_that("can create constructors with missing or lazy defaults", { + + Person <- new_class( + name = "Person", + properties = list( + # non-dynamic, default missing (required constructor arg) + first_name = new_property(class_character, default = quote(expr = )), + + # non-dynamic, static default (optional constructor arg) + middle_name = new_property(class_character, default = ""), + + # non-dynamic, default missing (required constructor arg) (same as first_name) + last_name = new_property(class_missing | class_character), + + # non-dynamic, but defaults to the value of another property + nick_name = new_property(class_character, default = quote(first_name)), + + # non-dynamic, optional constructor argument, read-only after construction. + birthdate = new_property( + class = class_Date, + default = quote(Sys.Date()), + setter = function(self, value) { + if (!is.null(self@birthdate)) + stop("Can't set read-only property Person@birthdate") + self@birthdate <- value + self + } + ), + + # dynamic property, not a constructor argument + age = new_property(class = class_any, getter = function(self) { + Sys.Date() - self@birthdate + }) + ) + ) + + expect_equal(formals(Person), as.pairlist(alist( + first_name = , + middle_name = "", + last_name = , + nick_name = first_name, + birthdate = Sys.Date() + ))) # no age + + expect_error(Person(), 'argument "first_name" is missing, with no default') + expect_error(Person("Alice"), 'argument "last_name" is missing, with no default') + + p <- Person("Alice", ,"Smith") + + expect_equal(p@nick_name, "Alice") + expect_equal(p@middle_name, "") + expect_equal(p@birthdate, Sys.Date()) + expect_equal(p@age, Sys.Date() - Sys.Date()) + + p <- Person("Bob", nick_name = "Bobby", "Allen" , "Smith", as.Date('1970-01-01')) + expect_equal(p@nick_name, "Bobby") + expect_equal(p@birthdate, as.Date('1970-01-01')) + expect_equal(p@age, Sys.Date() - as.Date('1970-01-01')) + expect_equal(p@middle_name, "Allen") + expect_error(p@birthdate <- as.Date('1970-01-01'), + "Can\'t set read-only property Person@birthdate") +}) diff --git a/tests/testthat/test-property.R b/tests/testthat/test-property.R index f5d9c813..e244ac83 100644 --- a/tests/testthat/test-property.R +++ b/tests/testthat/test-property.R @@ -108,22 +108,25 @@ describe("prop setting", { "foo", properties = list( x = new_property(setter = function(self, value) { - self@x <- 1 - self@y <- value + 1 + self@x <- value + self@y <- paste0(value, "_set_by_x_setter") self }), y = new_property(setter = function(self, value) { - self@y <- 2 - self@z <- as.integer(value + 1) + self@y <- value + self@z <- paste0(value, "_set_by_y_setter") self }), - z = new_property(class_integer) + z = new_property(class_character) ), validator = function(self) { add(times_validated) <<- 1L; NULL } ) - out <- foo(x = 1) + out <- foo() expect_equal(times_validated, 1L) - expect_identical(out@z, 3L) + + out@x <- "VAL" + expect_equal(times_validated, 2L) + expect_equal(out@z, "VAL_set_by_x_setter_set_by_y_setter") }) it("does not run the check or validation functions if check = FALSE", { @@ -244,7 +247,7 @@ test_that("properties can be base, S3, S4, S7, or S7 union", { anything = class_any, null = NULL, base = class_integer, - S3 = new_S3_class("factor"), + S3 = class_factor, S4 = class_S4, S7 = class_S7, S7_union = new_union(class_integer, class_logical) @@ -396,8 +399,7 @@ test_that("custom getters don't infinitely recurse", { someclass <- new_class("someclass", properties = list( someprop = new_property( class_character, - getter = function(self) - self@someprop, + getter = function(self) self@someprop, setter = function(self, value) { self@someprop <- toupper(value) self