From dd5e389505056a049c4958efe20c6dd7140902a6 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Wed, 27 May 2026 08:17:26 -0500 Subject: [PATCH 1/5] Allow object construction with a list Fixes #497 --- NEWS.md | 1 + R/class.R | 12 +++++++++++- R/utils.R | 6 ++++++ man/new_class.Rd | 7 ++++++- tests/testthat/_snaps/class.md | 8 ++++++++ tests/testthat/test-class.R | 22 ++++++++++++++++++++++ 6 files changed, 54 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 489a211b..c947feae 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,7 @@ * `S7_error_method_not_found` now has a correct class vector without a duplicate `"error"` entry (@jjjermiah, #604). * `method<-` now gives a clear error when assigning a primitive function (e.g. `log`) as a method (#608). * `method<-` and `method()` now accept a length-1 list as `signature` for single-dispatch generics, matching the list-of-classes form required for multi-dispatch (#555). +* `new_object()` accepts a single unnamed named list as a shortcut for splicing property values, making it easier to programmatically construct an object from a list of properties (#497). * `new_object()` now gives an informative error when `.parent` is a class specification rather than an instance of the parent class (#409). * `new_object()` no longer materialises ALTREP parent values (e.g. `seq_len()`), so constructing an S7 object that wraps a large compact integer sequence is now O(1) in memory instead of O(n) (@kschaubroeck, #607). * `new_S3_class()` objects now work with `inherits()` (and other functions that use `nameOfClass()`) in R 4.3 and later (@lawremi, #521). diff --git a/R/class.R b/R/class.R index 8928e387..28baca40 100644 --- a/R/class.R +++ b/R/class.R @@ -287,6 +287,11 @@ check_parent <- function(parent, class) { #' @param .parent,... Parent object and named properties used to construct the #' object. +#' +#' As a convenience, if `...` is a single unnamed argument that is a +#' named list, the elements of the list are used as the properties. This +#' makes it easy to programmatically construct an object from a list of +#' property values. #' @rdname new_class #' @export new_object <- function(.parent, ...) { @@ -307,7 +312,12 @@ new_object <- function(.parent, ...) { } args <- list(...) - if ("" %in% names2(args)) { + if (is_single_list(args)) { + if ("" %in% names2(args[[1]])) { + stop("All elements of `..1` must be named.") + } + args <- args[[1L]] + } else if ("" %in% names2(args)) { stop("All arguments to `...` must be named.") } diff --git a/R/utils.R b/R/utils.R index 5795bb5f..62a47179 100644 --- a/R/utils.R +++ b/R/utils.R @@ -44,6 +44,12 @@ names2 <- function(x) { } } +# Is `...` a single unnamed list argument? Used by functions that accept +# either named arguments via `...` or a single list spliced in. +is_single_list <- function(args) { + length(args) == 1L && !nzchar(names2(args)) && is.list(args[[1L]]) +} + is_prefix <- function(x, y) { length(x) <= length(y) && identical(unclass(x), unclass(y)[seq_along(x)]) } diff --git a/man/new_class.Rd b/man/new_class.Rd index 020c943d..a92778e6 100644 --- a/man/new_class.Rd +++ b/man/new_class.Rd @@ -74,7 +74,12 @@ See \code{validate()} for more details, examples, and how to temporarily suppress validation when needed.} \item{.parent, ...}{Parent object and named properties used to construct the -object.} +object. + +As a convenience, if \code{...} is a single unnamed argument that is a +named list, the elements of the list are used as the properties. This +makes it easy to programmatically construct an object from a list of +property values.} } \value{ A object constructor, a function that can be used to create objects diff --git a/tests/testthat/_snaps/class.md b/tests/testthat/_snaps/class.md index ff05f78a..99825927 100644 --- a/tests/testthat/_snaps/class.md +++ b/tests/testthat/_snaps/class.md @@ -187,6 +187,14 @@ ! object is invalid: - x must be positive +# new_object() / errors if single unnamed list has unnamed elements + + Code + foo(list(1)) + Condition + Error in `new_object()`: + ! All elements of `..1` must be named. + # new_object() / runs each parent validator exactly once Code diff --git a/tests/testthat/test-class.R b/tests/testthat/test-class.R index c56fa239..f142fdc8 100644 --- a/tests/testthat/test-class.R +++ b/tests/testthat/test-class.R @@ -171,6 +171,28 @@ describe("new_object()", { }) }) + it("accepts a single unnamed named list of properties (#497)", { + foo <- new_class( + "foo", + properties = list(x = class_double, y = class_double), + package = NULL, + constructor = function(props) new_object(S7_object(), props) + ) + obj <- foo(list(x = 1, y = 2)) + expect_equal(obj@x, 1) + expect_equal(obj@y, 2) + }) + + it("errors if single unnamed list has unnamed elements", { + foo <- new_class( + "foo", + properties = list(x = class_double), + package = NULL, + constructor = function(props) new_object(S7_object(), props) + ) + expect_snapshot(foo(list(1)), error = TRUE) + }) + it("runs each parent validator exactly once", { A <- new_class("A", validator = function(self) cat("A ")) B <- new_class("B", parent = A, validator = function(self) cat("B ")) From cbe095c36dc07b8603841de9712b210a46d7756d Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 28 May 2026 10:22:41 -0500 Subject: [PATCH 2/5] Polish docs --- R/class.R | 7 +++---- man/new_class.Rd | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/R/class.R b/R/class.R index 28baca40..75a4418f 100644 --- a/R/class.R +++ b/R/class.R @@ -288,10 +288,9 @@ check_parent <- function(parent, class) { #' @param .parent,... Parent object and named properties used to construct the #' object. #' -#' As a convenience, if `...` is a single unnamed argument that is a -#' named list, the elements of the list are used as the properties. This -#' makes it easy to programmatically construct an object from a list of -#' property values. +#' As a convenience, if `...` is a single unnamed list, then the elements of +#' that list are used as the properties. This makes it easy to +#' programmatically construct an object from a list of property values. #' @rdname new_class #' @export new_object <- function(.parent, ...) { diff --git a/man/new_class.Rd b/man/new_class.Rd index a92778e6..610904db 100644 --- a/man/new_class.Rd +++ b/man/new_class.Rd @@ -76,10 +76,9 @@ suppress validation when needed.} \item{.parent, ...}{Parent object and named properties used to construct the object. -As a convenience, if \code{...} is a single unnamed argument that is a -named list, the elements of the list are used as the properties. This -makes it easy to programmatically construct an object from a list of -property values.} +As a convenience, if \code{...} is a single unnamed list, then the elements of +that list are used as the properties. This makes it easy to +programmatically construct an object from a list of property values.} } \value{ A object constructor, a function that can be used to create objects From 8c1f9f44faedcc51c5a70f91384c1b995361e136 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 29 May 2026 08:28:52 -0500 Subject: [PATCH 3/5] Handle `...` consistently --- NEWS.md | 2 ++ R/class.R | 10 +--------- R/convert.R | 9 +++++---- R/property.R | 6 ++++-- R/utils.R | 16 ++++++++++++++++ man/convert.Rd | 4 +++- man/props.Rd | 4 +++- tests/testthat/_snaps/convert.md | 8 ++++++++ tests/testthat/_snaps/property.md | 25 +++++++++---------------- tests/testthat/test-convert.R | 16 ++++++++++++++++ tests/testthat/test-property.R | 15 +++++++++++++++ 11 files changed, 82 insertions(+), 33 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0e249cd6..3776a644 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,7 @@ * Internal changes to support R-devel (4.6) (#592, #593, #598, #600). * Method dispatch on `class_missing` now correctly handles missing arguments forwarded through a wrapper functions (#595). * `S7_error_method_not_found` now has a correct class vector without a duplicate `"error"` entry (@jjjermiah, #604). +* `convert()` accepts a single unnamed list of property overrides when downcasting, as a shortcut for individual name-value pairs (#497). * `method<-` now gives a clear error when assigning a primitive function (e.g. `log`) as a method (#608). * `method<-` and `method()` now accept a length-1 list as `signature` for single-dispatch generics, matching the list-of-classes form required for multi-dispatch (#555). * `new_object()` accepts a single unnamed named list as a shortcut for splicing property values, making it easier to programmatically construct an object from a list of properties (#497). @@ -21,6 +22,7 @@ * `S7_data()` now preserves the S3 class when the S7 class inherits from an S3 class, so e.g. `S7_data()` on a data.frame subclass now returns a data.frame (#380). * `S7_data<-()` now preserves attributes (like `names` or `dim`) from the replacement data instead of carrying over the originals, so resizing the underlying data works correctly (#478). * `S7_inherits()` and `check_is_S7()` now accept any class specification (S7 class, S7 union, S3 class, S4 class, or base type wrapper like `class_integer`), not just S7 classes (#556). +* `set_props()` accepts a single unnamed named list as a shortcut for splicing property values, making it easier to set properties programmatically (#497). * `str()` on S7 objects that inherit from data.frame (or other S3 classes whose underlying data has a `dim` attribute incompatible with the bare base type) no longer errors (#494). # S7 0.2.2 diff --git a/R/class.R b/R/class.R index 75a4418f..9b55440e 100644 --- a/R/class.R +++ b/R/class.R @@ -310,15 +310,7 @@ new_object <- function(.parent, ...) { check_parent(.parent, class) } - args <- list(...) - if (is_single_list(args)) { - if ("" %in% names2(args[[1]])) { - stop("All elements of `..1` must be named.") - } - args <- args[[1L]] - } else if ("" %in% names2(args)) { - stop("All arguments to `...` must be named.") - } + args <- splice_dots(...) has_setter <- vlapply(class@properties[names(args)], prop_has_setter) diff --git a/R/convert.R b/R/convert.R index d4ff676e..1c5fd487 100644 --- a/R/convert.R +++ b/R/convert.R @@ -37,7 +37,9 @@ #' @param to An S7 class specification, passed to [as_class()]. #' @param ... Other arguments passed to custom `convert()` methods. For #' downcasting, these can be used to override existing properties or set new -#' ones. +#' ones. As a convenience, you can supply a single unnamed list instead of +#' individual name-value pairs, which makes it easy to override properties +#' programmatically. #' @return Either `from` coerced to class `to`, or an error if the coercion #' is not possible. #' @export @@ -86,7 +88,7 @@ convert <- function(from, to, ...) { } else if (class_inherits(from, to)) { convert_up(from, to) } else if (is_down_cast(from, to)) { - convert_down(from, to, ...) + convert_down(from, to, splice_dots(..., error_call = quote(convert()))) } else { msg <- paste_c( "Can't find method with dispatch classes:\n", @@ -124,7 +126,7 @@ is_down_cast <- function(x, class) { inherits(x, setdiff(class_dispatch(class), "S7_object")) } -convert_down <- function(from, to, ...) { +convert_down <- function(from, to, user_args = list()) { # Use `from` as a prototype/seed when constructing `to`: copy over property # values from `from` and supply them as arguments to the `to` constructor. @@ -139,7 +141,6 @@ convert_down <- function(from, to, ...) { } # Drop properties overridden by user-supplied arguments - user_args <- list(...) from_prop_names <- setdiff(from_prop_names, names(user_args)) from_prop_values <- props(from, from_prop_names) diff --git a/R/property.R b/R/property.R index e2cb58f1..2b29f000 100644 --- a/R/property.R +++ b/R/property.R @@ -449,10 +449,12 @@ props <- function(object, names = prop_names(object)) { } #' @export -#' @param ... Name-value pairs given property to modify and new value. +#' @param ... Name-value pairs given property to modify and new value. As a +#' convenience, you can supply a single unnamed list instead of individual +#' name-value pairs, which makes it easy to set properties programmatically. #' @rdname props set_props <- function(object, ..., .check = TRUE) { - props(object, check = .check) <- list(...) + props(object, check = .check) <- splice_dots(...) object } diff --git a/R/utils.R b/R/utils.R index 62a47179..945b3cdf 100644 --- a/R/utils.R +++ b/R/utils.R @@ -50,6 +50,22 @@ is_single_list <- function(args) { length(args) == 1L && !nzchar(names2(args)) && is.list(args[[1L]]) } +# Collect `...` into a named list. As a convenience, a single unnamed list is +# spliced in so its elements become the values, making it easy to supply +# values programmatically. All values must be named. +splice_dots <- function(..., error_call = sys.call(-1)) { + args <- list(...) + if (is_single_list(args)) { + args <- args[[1L]] + if ("" %in% names2(args)) { + stop(simpleError("All elements of `..1` must be named.", error_call)) + } + } else if ("" %in% names2(args)) { + stop(simpleError("All arguments to `...` must be named.", error_call)) + } + args +} + is_prefix <- function(x, y) { length(x) <= length(y) && identical(unclass(x), unclass(y)[seq_along(x)]) } diff --git a/man/convert.Rd b/man/convert.Rd index d1a99e20..b6adaf0a 100644 --- a/man/convert.Rd +++ b/man/convert.Rd @@ -13,7 +13,9 @@ convert(from, to, ...) \item{...}{Other arguments passed to custom \code{convert()} methods. For downcasting, these can be used to override existing properties or set new -ones.} +ones. As a convenience, you can supply a single unnamed list instead of +individual name-value pairs, which makes it easy to override properties +programmatically.} } \value{ Either \code{from} coerced to class \code{to}, or an error if the coercion diff --git a/man/props.Rd b/man/props.Rd index 62a9f0d5..ada6a501 100644 --- a/man/props.Rd +++ b/man/props.Rd @@ -24,7 +24,9 @@ before returning.} \item{value}{A named list of values. The object is checked for validity only after all replacements are performed.} -\item{...}{Name-value pairs given property to modify and new value.} +\item{...}{Name-value pairs given property to modify and new value. As a +convenience, you can supply a single unnamed list instead of individual +name-value pairs, which makes it easy to set properties programmatically.} } \value{ A named list of property values. diff --git a/tests/testthat/_snaps/convert.md b/tests/testthat/_snaps/convert.md index f460808d..ff90251f 100644 --- a/tests/testthat/_snaps/convert.md +++ b/tests/testthat/_snaps/convert.md @@ -8,3 +8,11 @@ - from: - to : +# fallback convert / errors if single unnamed list has unnamed elements (#497) + + Code + convert(Foo(x = 1), Bar, list(2)) + Condition + Error in `convert()`: + ! All elements of `..1` must be named. + diff --git a/tests/testthat/_snaps/property.md b/tests/testthat/_snaps/property.md index 23e1e033..d4ce57bc 100644 --- a/tests/testthat/_snaps/property.md +++ b/tests/testthat/_snaps/property.md @@ -6,22 +6,6 @@ Can't find property @x. -# property retrieval / reports dynamic getter errors as property calls - - Code - foo()@x - Condition - Error in `@x`: - ! nope - -# prop setting / reports dynamic setter errors as property calls - - Code - obj@x <- 1 - Condition - Error in `@x`: - ! nope - # prop setting / can't set read-only properties Code @@ -62,6 +46,14 @@ ! object is invalid: - bad +# props<- / set_props() errors if single unnamed list has unnamed elements (#497) + + Code + set_props(foo(1), list(2)) + Condition + Error in `set_props()`: + ! All elements of `..1` must be named. + # props<- / set_props() skip validation with `.check = FALSE` Code @@ -276,3 +268,4 @@ [tx] finished transmitting. Code expect_equal(receiver@message, "goodbye") + diff --git a/tests/testthat/test-convert.R b/tests/testthat/test-convert.R index 98416d4b..05f02b7b 100644 --- a/tests/testthat/test-convert.R +++ b/tests/testthat/test-convert.R @@ -99,6 +99,22 @@ describe("fallback convert", { expect_error(convert(foo, Unrelated), "Can't find method") }) + it("accepts a single unnamed list of overrides when downcasting (#497)", { + Foo <- new_class("Foo", properties = list(x = class_numeric)) + Bar <- new_class("Bar", Foo, properties = list(y = class_numeric)) + + bar <- convert(Foo(x = 1), Bar, list(x = 2, y = 3)) + expect_equal(bar@x, 2) + expect_equal(bar@y, 3) + }) + + it("errors if single unnamed list has unnamed elements (#497)", { + Foo <- new_class("Foo", properties = list(x = class_numeric)) + Bar <- new_class("Bar", Foo, properties = list(y = class_numeric)) + + expect_snapshot(convert(Foo(x = 1), Bar, list(2)), error = TRUE) + }) + it("can convert to S3 class", { factor2 <- new_class( "factor2", diff --git a/tests/testthat/test-property.R b/tests/testthat/test-property.R index 69c47fa4..60d96297 100644 --- a/tests/testthat/test-property.R +++ b/tests/testthat/test-property.R @@ -202,6 +202,21 @@ describe("props<-", { expect_equal(obj2@x, 2) }) + it("set_props() accepts a single unnamed list (#497)", { + foo <- new_class( + "foo", + properties = list(x = class_double, y = class_double) + ) + obj <- set_props(foo(1, 2), list(x = 3, y = 4)) + expect_equal(obj@x, 3) + expect_equal(obj@y, 4) + }) + + it("set_props() errors if single unnamed list has unnamed elements (#497)", { + foo <- new_class("foo", properties = list(x = class_double)) + expect_snapshot(set_props(foo(1), list(2)), error = TRUE) + }) + it("set_props() skip validation with `.check = FALSE`", { foo <- new_class( "foo", From 194691ef39c102312ecef5e2ed3b4d9c432bd9af Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 29 May 2026 08:40:29 -0500 Subject: [PATCH 4/5] Use `_` for functions with `...` Fixes #423 --- NEWS.md | 1 + R/class.R | 27 +++++++++++++++------------ R/property.R | 10 +++++++--- man/new_class.Rd | 7 +++++-- man/new_property.Rd | 4 +++- man/props.Rd | 5 ++++- tests/testthat/_snaps/class.md | 10 +++++----- tests/testthat/test-class.R | 17 ++++++++++++++--- tests/testthat/test-property.R | 6 ++++++ 9 files changed, 60 insertions(+), 27 deletions(-) diff --git a/NEWS.md b/NEWS.md index 3776a644..2e8a7645 100644 --- a/NEWS.md +++ b/NEWS.md @@ -22,6 +22,7 @@ * `S7_data()` now preserves the S3 class when the S7 class inherits from an S3 class, so e.g. `S7_data()` on a data.frame subclass now returns a data.frame (#380). * `S7_data<-()` now preserves attributes (like `names` or `dim`) from the replacement data instead of carrying over the originals, so resizing the underlying data works correctly (#478). * `S7_inherits()` and `check_is_S7()` now accept any class specification (S7 class, S7 union, S3 class, S4 class, or base type wrapper like `class_integer`), not just S7 classes (#556). +* `set_props()` and `new_object()` now name their first arguments `_object` and `_parent`, so a property with any name (e.g. `object`) can be passed through `...` without clashing with the argument. Property names starting with `_` are now reserved for this sort of internal use (#423). * `set_props()` accepts a single unnamed named list as a shortcut for splicing property values, making it easier to set properties programmatically (#497). * `str()` on S7 objects that inherit from data.frame (or other S3 classes whose underlying data has a `dim` attribute incompatible with the bare base type) no longer errors (#494). diff --git a/R/class.R b/R/class.R index 9b55440e..e3299a96 100644 --- a/R/class.R +++ b/R/class.R @@ -262,7 +262,7 @@ check_parent <- function(parent, class) { parent_class <- class@parent if (is.null(parent_class)) { stop( - "`.parent` must not be supplied when class has no parent.", + "`_parent` must not be supplied when class has no parent.", call. = FALSE ) } @@ -276,7 +276,7 @@ check_parent <- function(parent, class) { return() } msg <- sprintf( - "`.parent` must be an instance of %s, not %s.", + "`_parent` must be an instance of %s, not %s.", class_desc(parent_class), obj_desc(parent) ) @@ -285,15 +285,18 @@ check_parent <- function(parent, class) { # Object ------------------------------------------------------------------ -#' @param .parent,... Parent object and named properties used to construct the +#' @param _parent,... Parent object and named properties used to construct the #' object. #' +#' `_parent` has an unusual name to avoid clashing with property names +#' supplied in `...`; see [new_property()] for details. +#' #' As a convenience, if `...` is a single unnamed list, then the elements of #' that list are used as the properties. This makes it easy to #' programmatically construct an object from a list of property values. #' @rdname new_class #' @export -new_object <- function(.parent, ...) { +new_object <- function(`_parent`, ...) { class <- sys.function(-1) if (!inherits(class, "S7_class")) { stop("`new_object()` must be called from within a constructor.") @@ -306,38 +309,38 @@ new_object <- function(.parent, ...) { stop(msg) } - if (!missing(.parent)) { - check_parent(.parent, class) + if (!missing(`_parent`)) { + check_parent(`_parent`, class) } args <- splice_dots(...) has_setter <- vlapply(class@properties[names(args)], prop_has_setter) - # We must awkwardly operate on `.parent` rather than binding to a local + # We must awkwardly operate on `_parent` rather than binding to a local # variable; since otherwise the extra binding causes ALTREP-wrapped values to # be materialised when byte-compiled (#607). attrs <- c( list(class = class_dispatch(class), S7_class = class), args[!has_setter], - attributes(.parent) + attributes(`_parent`) ) attrs <- attrs[!duplicated(names(attrs))] - attributes(.parent) <- attrs + attributes(`_parent`) <- attrs # invoke custom property setters prop_setter_vals <- args[has_setter] for (name in names(prop_setter_vals)) { - prop(.parent, name, check = FALSE) <- prop_setter_vals[[name]] + prop(`_parent`, name, check = FALSE) <- prop_setter_vals[[name]] } # Don't need to validate if parent class already validated, # i.e. it's a non-abstract S7 class parent_validated <- inherits(class@parent, "S7_object") && !class@parent@abstract - validate(.parent, recursive = !parent_validated) + validate(`_parent`, recursive = !parent_validated) - .parent + `_parent` } #' @export diff --git a/R/property.R b/R/property.R index 2b29f000..989b1aec 100644 --- a/R/property.R +++ b/R/property.R @@ -49,6 +49,8 @@ #' don't need to set this here, as it's more convenient to supply as #' the element name when defining a list of properties. If both `name` #' and a list-name are supplied, the list-name will be used. +#' +#' Avoid names starting with `_`; they are reserved for internal use. #' @returns An S7 property, i.e. a list with class `S7_property`. #' @export #' @examples @@ -449,13 +451,15 @@ props <- function(object, names = prop_names(object)) { } #' @export +#' @param _object The object to modify. It has an unusual name to avoid clashing +#' with property names supplied in `...`; see [new_property()] for details. #' @param ... Name-value pairs given property to modify and new value. As a #' convenience, you can supply a single unnamed list instead of individual #' name-value pairs, which makes it easy to set properties programmatically. #' @rdname props -set_props <- function(object, ..., .check = TRUE) { - props(object, check = .check) <- splice_dots(...) - object +set_props <- function(`_object`, ..., .check = TRUE) { + props(`_object`, check = .check) <- splice_dots(...) + `_object` } as_properties <- function(x) { diff --git a/man/new_class.Rd b/man/new_class.Rd index 610904db..9a4ec4cd 100644 --- a/man/new_class.Rd +++ b/man/new_class.Rd @@ -15,7 +15,7 @@ new_class( validator = NULL ) -new_object(.parent, ...) +new_object(`_parent`, ...) } \arguments{ \item{name}{The name of the class, as a string. (We recommend using @@ -73,9 +73,12 @@ problem, using \verb{@prop_name} to describe where the problem lies. See \code{validate()} for more details, examples, and how to temporarily suppress validation when needed.} -\item{.parent, ...}{Parent object and named properties used to construct the +\item{_parent, ...}{Parent object and named properties used to construct the object. +\verb{_parent} has an unusual name to avoid clashing with property names +supplied in \code{...}; see \code{\link[=new_property]{new_property()}} for details. + As a convenience, if \code{...} is a single unnamed list, then the elements of that list are used as the properties. This makes it easy to programmatically construct an object from a list of property values.} diff --git a/man/new_property.Rd b/man/new_property.Rd index d4f0b98b..3785d272 100644 --- a/man/new_property.Rd +++ b/man/new_property.Rd @@ -54,7 +54,9 @@ 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 the element name when defining a list of properties. If both \code{name} -and a list-name are supplied, the list-name will be used.} +and a list-name are supplied, the list-name will be used. + +Avoid names starting with \verb{_}; they are reserved for internal use.} } \value{ An S7 property, i.e. a list with class \code{S7_property}. diff --git a/man/props.Rd b/man/props.Rd index ada6a501..f9b723d2 100644 --- a/man/props.Rd +++ b/man/props.Rd @@ -10,7 +10,7 @@ props(object, names = prop_names(object)) props(object, check = TRUE) <- value -set_props(object, ..., .check = TRUE) +set_props(`_object`, ..., .check = TRUE) } \arguments{ \item{object}{An object from a S7 class} @@ -24,6 +24,9 @@ before returning.} \item{value}{A named list of values. The object is checked for validity only after all replacements are performed.} +\item{_object}{The object to modify. It has an unusual name to avoid clashing +with property names supplied in \code{...}; see \code{\link[=new_property]{new_property()}} for details.} + \item{...}{Name-value pairs given property to modify and new value. As a convenience, you can supply a single unnamed list instead of individual name-value pairs, which makes it easy to set properties programmatically.} diff --git a/tests/testthat/_snaps/class.md b/tests/testthat/_snaps/class.md index 99825927..08a0cebb 100644 --- a/tests/testthat/_snaps/class.md +++ b/tests/testthat/_snaps/class.md @@ -151,26 +151,26 @@ Error in `new_object()`: ! `new_object()` must be called from within a constructor. -# new_object() / errors if `.parent` doesn't inherit from the parent class (#409) +# new_object() / errors if `_parent` doesn't inherit from the parent class (#409) Code Foo() Condition Error: - ! `.parent` must be an instance of , not S3. + ! `_parent` must be an instance of , not S3. Code Baz() Condition Error: - ! `.parent` must be an instance of , not . + ! `_parent` must be an instance of , not . -# new_object() / errors if `.parent` is supplied but class has no parent +# new_object() / errors if `_parent` is supplied but class has no parent Code NoParent() Condition Error: - ! `.parent` must not be supplied when class has no parent. + ! `_parent` must not be supplied when class has no parent. # new_object() / validates object diff --git a/tests/testthat/test-class.R b/tests/testthat/test-class.R index f142fdc8..7c09f58b 100644 --- a/tests/testthat/test-class.R +++ b/tests/testthat/test-class.R @@ -114,9 +114,9 @@ describe("new_object()", { expect_snapshot(new_object(), error = TRUE) }) - it("errors if `.parent` doesn't inherit from the parent class (#409)", { + it("errors if `_parent` doesn't inherit from the parent class (#409)", { Bar <- new_class("Bar", package = NULL) - # `.parent` should be `Bar()`, not the class spec `Bar` + # `_parent` should be `Bar()`, not the class spec `Bar` Foo <- new_class( "Foo", parent = Bar, @@ -147,7 +147,7 @@ describe("new_object()", { expect_no_error(Concrete(x = 1L)) }) - it("errors if `.parent` is supplied but class has no parent", { + it("errors if `_parent` is supplied but class has no parent", { NoParent <- new_class( "NoParent", package = NULL, @@ -157,6 +157,17 @@ describe("new_object()", { expect_snapshot(NoParent(), error = TRUE) }) + it("can set a property named `.parent` (#423)", { + foo <- new_class( + "foo", + properties = list(.parent = class_double), + package = NULL, + constructor = function(.parent) new_object(S7_object(), .parent = .parent) + ) + obj <- foo(.parent = 1) + expect_equal(obj@.parent, 1) + }) + it("validates object", { foo <- new_class( "foo", diff --git a/tests/testthat/test-property.R b/tests/testthat/test-property.R index 60d96297..6db57c15 100644 --- a/tests/testthat/test-property.R +++ b/tests/testthat/test-property.R @@ -202,6 +202,12 @@ describe("props<-", { expect_equal(obj2@x, 2) }) + it("set_props() can set a property named `object` (#423)", { + foo <- new_class("foo", properties = list(object = class_double)) + obj <- set_props(foo(1), object = 2) + expect_equal(obj@object, 2) + }) + it("set_props() accepts a single unnamed list (#497)", { foo <- new_class( "foo", From 9a8614a7bcb9f2afb6d473c969de71e7087f5fa9 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 29 May 2026 11:02:32 -0500 Subject: [PATCH 5/5] Document what we did --- .claude/skills/property-dots/SKILL.md | 155 ++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 .claude/skills/property-dots/SKILL.md diff --git a/.claude/skills/property-dots/SKILL.md b/.claude/skills/property-dots/SKILL.md new file mode 100644 index 00000000..27046bb0 --- /dev/null +++ b/.claude/skills/property-dots/SKILL.md @@ -0,0 +1,155 @@ +--- +name: property-dots +description: Guide for writing S7 functions that accept property name-value pairs via `...`. Use when adding or editing a function whose `...` collects property values (like `new_object()`, `set_props()`, or `convert()`), so that the fixed arguments don't clash with property names. +--- + +# Accept properties via `...` + +Use this skill when writing a function that collects property name-value pairs +through `...`, e.g. ``new_object(`_parent`, ...)``, ``set_props(`_object`, ...)``, +or a downcasting `convert()` method. + +Two problems show up whenever `...` carries property values: + +1. A fixed argument can shadow a property of the same name. If `set_props()` + had a formal called `object`, then `set_props(x, object = 1)` would bind + `object` to the formal instead of treating it as the property `object`. +2. Callers sometimes want to supply property values programmatically, as a + single list, rather than as individual `name = value` pairs. + +`splice_dots()` and the underscore naming convention solve these. + +## The underscore convention + +Property names starting with `_` are reserved for internal use (documented in +`new_property()`). This lets you name a fixed argument with a leading `_` so it +can never collide with a real property passed through `...`. + +- ``new_object(`_parent`, ...)`` — `_parent` is the parent object. +- ``set_props(`_object`, ...)`` — `_object` is the object to modify. + +`_parent` and `_object` are not syntactically valid names, so they **must be +backtick-quoted** everywhere they appear: + +```r +set_props <- function(`_object`, ..., .check = TRUE) { + props(`_object`, check = .check) <- splice_dots(...) + `_object` +} +``` + +Because these arguments are always passed positionally (and generated +constructors pass the parent positionally), the unusual name never burdens +callers — it only stops the name from competing with `...`. + +Apply the convention when: + +- The function takes properties via `...`, AND +- It also needs a fixed argument that a user might plausibly use as a property + name (`object`, `parent`, `value`, ...). + +Do *not* rename existing public arguments where it would be a breaking change +without good reason. For example `convert(from, to, ...)` keeps `from`/`to` as +is; only the dots-handling was updated. + +## Collecting the dots with `splice_dots()` + +`splice_dots(...)` returns a named list of the `...` values. As a convenience, +if `...` is a single unnamed list, its elements are used instead, which makes +it easy to build the values programmatically. It errors if any value is +unnamed. + +```r +splice_dots(x = 1, y = 2) # list(x = 1, y = 2) +splice_dots(list(x = 1, y = 2)) # list(x = 1, y = 2) -- spliced +splice_dots(1) # error: All arguments to `...` must be named. +splice_dots(list(1)) # error: All elements of `..1` must be named. +``` + +Replace any `args <- list(...)` that collects properties with +`args <- splice_dots(...)`. Don't hand-roll the single-list check — that logic +lives in `splice_dots()` (and `is_single_list()`) so all callers behave +identically. + +## Getting the error call right + +`splice_dots()` reports its error against its caller via +`error_call = sys.call(-1)`. This is correct for ordinary functions: + +```r +new_object <- function(`_parent`, ...) { + ... + args <- splice_dots(...) # errors blame `new_object()` + ... +} +``` + +It is **not** reliable inside an S7 generic, because dispatch rewrites the call +stack (you'll see a bogus call like `.set_ops_need_as_vector()`). For a +generic, pass the call explicitly: + +```r +convert <- function(from, to, ...) { + ... + } else if (is_down_cast(from, to)) { + convert_down(from, to, splice_dots(..., error_call = quote(convert()))) + } + ... +} +``` + +## Documenting it + +In the `@param` for the underscore argument, explain the unusual name and point +to `new_property()`: + +```r +#' @param _object The object to modify. It has an unusual name to avoid clashing +#' with property names supplied in `...`; see [new_property()] for details. +``` + +In the `@param` for `...`, mention the single-list shortcut: + +```r +#' @param ... Name-value pairs of properties to set. As a convenience, you can +#' supply a single unnamed list instead of individual name-value pairs, which +#' makes it easy to set properties programmatically. +``` + +roxygen handles `@param _object` and a backtick-quoted formal fine; the usage +line renders as ``set_props(`_object`, ...)``. + +## Worked example + +A function that returns a modified copy of an object, setting properties from +`...`: + +```r +#' @param _object The object to modify. It has an unusual name to avoid clashing +#' with property names supplied in `...`; see [new_property()] for details. +#' @param ... Name-value pairs of properties to set. As a convenience, you can +#' supply a single unnamed list instead of individual name-value pairs. +update_props <- function(`_object`, ...) { + props(`_object`) <- splice_dots(...) + `_object` +} +``` + +Now both of these work, even for a property named `object`: + +```r +update_props(x, object = 1, width = 10) +update_props(x, list(object = 1, width = 10)) +``` + +## Checklist + +- [ ] Name any clash-prone fixed argument with a leading `_`, backtick-quoted + everywhere it appears. +- [ ] Collect property values with `splice_dots(...)`, not `list(...)`. +- [ ] For an S7 generic, pass `error_call = quote(generic())` to `splice_dots()`. +- [ ] Document the `_` argument (cross-reference `new_property()`) and the + single-list shortcut on `...`. +- [ ] Add a test that a property whose name matches the old fixed argument can + now be set, and a test for the single-list shortcut. +- [ ] Run `air format .` and re-document.