Skip to content

Commit e7cc230

Browse files
authored
Allow a property setter to (optionally) take name (#624)
Fixes #552
1 parent 3566af0 commit e7cc230

11 files changed

Lines changed: 120 additions & 22 deletions

File tree

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Method dispatch on `class_missing` now correctly handles missing arguments forwarded through a wrapper functions (#595).
1414
* `super()` now works with S3 and S4 objects, not just S7 objects (#500).
1515
* `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).
16+
* `new_property()` now accepts a `setter` that takes `self`, `name`, and `value` making it easy to reuse the same definition for multiple properties (#552).
1617
* `new_S3_class()` objects now work with `inherits()` (and other functions that use `nameOfClass()`) in R 4.3 and later (@lawremi, #521).
1718
* New `prop_info()` returns a data frame summarising the properties of an S7 object or class, with one row per property and columns for name, default, class, getter, setter, and validator (#551).
1819
* `prop()` and `prop<-()` errors from custom getters and setters now report a synthetic `<Class>@<prop>` call, making it easier to see which property triggered the error (#536, #627, #638).

R/property.R

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@
2121
#' an object of the correct `class`; it will not be validated automatically.
2222
#'
2323
#' If a property has a getter but doesn't have a setter, it is read only.
24-
#' @param setter An optional function used to set the value. The function
25-
#' should take `self` and `value` and return a modified object.
24+
#' @param setter An optional function used to set the value.
25+
#' There are two supported forms:
26+
#'
27+
#' * `function(self, value)` is supplied the object and the value.
28+
#' * `function(self, name, value)` also gets the property name being set,
29+
#' which makes it easy to reuse the same property for multiple properties.
2630
#' @param validator A function taking a single argument, `value`, the value
2731
#' to validate.
2832
#'
@@ -93,7 +97,13 @@ new_property <- function(
9397
check_function(getter, alist(self = ))
9498
}
9599
if (!is.null(setter)) {
96-
check_function(setter, alist(self = , value = ))
100+
check_function(
101+
setter,
102+
list(
103+
alist(self = , value = ),
104+
alist(self = , name = , value = )
105+
)
106+
)
97107
}
98108
if (!is.null(validator)) {
99109
check_function(validator, alist(value = ))

R/utils.R

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,30 @@ check_function <- function(f, args, arg = deparse(substitute(f))) {
106106
stop(msg, call. = FALSE)
107107
}
108108

109-
args <- as.pairlist(args)
110-
if (!identical(formals(f), args)) {
111-
msg <- sprintf(
112-
"`%s` must be %s, not %s.",
113-
arg,
114-
show_args(args),
115-
show_args(formals(f))
116-
)
117-
stop(msg, call. = FALSE)
109+
# `args` is either a single formals list (e.g. alist(self = , value = ))
110+
# or an unnamed list of such formals lists. Distinguish via names: a
111+
# single signature has named entries (one per arg); a list of signatures
112+
# is unnamed.
113+
if (length(args) == 0 || any(nzchar(names2(args)))) {
114+
candidates <- list(as.pairlist(args))
115+
} else {
116+
candidates <- lapply(args, as.pairlist)
117+
}
118+
119+
for (cand in candidates) {
120+
if (identical(formals(f), cand)) {
121+
return(invisible())
122+
}
118123
}
124+
125+
expected <- oxford_or(vapply(candidates, show_args, character(1)))
126+
msg <- sprintf(
127+
"`%s` must be %s, not %s.",
128+
arg,
129+
expected,
130+
show_args(formals(f))
131+
)
132+
stop(msg, call. = FALSE)
119133
}
120134

121135
show_function <- function(x, constructor = FALSE) {

man/new_property.Rd

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/compat.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ static inline
1515
SEXP s7_get_var_in_frame(SEXP env, SEXP sym, SEXP ifnotfound) {
1616
return R_getVarEx(sym, env, FALSE, ifnotfound);
1717
}
18+
#define getClosureFormals R_ClosureFormals
1819
#else
1920
static inline
2021
SEXP s7_get_var_in_frame(SEXP env, SEXP sym, SEXP ifnotfound) {
2122
SEXP val = Rf_findVarInFrame(env, sym);
2223
return val == R_UnboundValue ? ifnotfound : val;
2324
}
25+
#define getClosureFormals FORMALS
2426
#endif
2527

2628
static inline

src/method-dispatch.c

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
#include "compat.h"
22

3-
#if (R_VERSION >= R_Version(4, 5, 0))
4-
#define getClosureFormals R_ClosureFormals
5-
#else
6-
#define getClosureFormals FORMALS
7-
#endif
8-
93
extern SEXP parent_sym;
104
extern SEXP sym_ANY;
115
extern SEXP ns_S7;

src/prop.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,14 @@ SEXP do_setter_call(SEXP setter, SEXP S7_class, SEXP name, SEXP object,
328328
object = protect_quote_if_needed(object, &n_protected);
329329
value = protect_quote_if_needed(value, &n_protected);
330330

331-
SEXP call = PROTECT(Rf_lang3(fn_sym, object, value));
331+
// Support both function(self, value) and function(self, name, value)
332+
// signatures. `name` is a string, so it self-evaluates and needs no quoting.
333+
SEXP call;
334+
if (Rf_length(getClosureFormals(setter)) >= 3) {
335+
call = PROTECT(Rf_lang4(fn_sym, object, name, value));
336+
} else {
337+
call = PROTECT(Rf_lang3(fn_sym, object, value));
338+
}
332339
++n_protected;
333340
call_data.call = call;
334341

tests/testthat/_snaps/property.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
new_property(setter = function(x, y, z) { })
6767
Condition
6868
Error:
69-
! `setter` must be function(self, value), not function(x, y, z).
69+
! `setter` must be function(self, value) or function(self, name, value), not function(x, y, z).
7070

7171
# new_property() / validates default
7272

tests/testthat/_snaps/utils.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# check_function() accepts a matching single signature
2+
3+
Code
4+
check_function(1, alist(x = ), arg = "f")
5+
Condition
6+
Error:
7+
! `f` must be a function.
8+
9+
---
10+
11+
Code
12+
check_function(function(y) { }, alist(x = ), arg = "f")
13+
Condition
14+
Error:
15+
! `f` must be function(x), not function(y).
16+
17+
# check_function() accepts any of several candidate signatures
18+
19+
Code
20+
check_function(function(x, y, z) { }, sigs, arg = "setter")
21+
Condition
22+
Error:
23+
! `setter` must be function(self, value) or function(self, name, value), not function(x, y, z).
24+

tests/testthat/test-property.R

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,28 @@ describe("prop setting", {
163163
x <- "foo"
164164
expect_error(x@blah <- "bar", "is not a slot in class")
165165
})
166+
167+
it("setter can receive the property name (#552)", {
168+
property_colour <- new_property(
169+
class = class_character,
170+
setter = function(self, name, value) {
171+
prop(self, name) <- as.character(value)
172+
self
173+
}
174+
)
175+
Rectangle <- new_class(
176+
"Rectangle",
177+
properties = list(colour = property_colour, fill = property_colour),
178+
package = NULL
179+
)
180+
181+
r <- Rectangle(colour = "red", fill = 99L)
182+
expect_equal(r@colour, "red")
183+
expect_equal(r@fill, "99")
184+
185+
r@colour <- 42L
186+
expect_equal(r@colour, "42")
187+
})
166188
})
167189

168190
describe("props<-", {

0 commit comments

Comments
 (0)