Skip to content

Commit d1caf49

Browse files
authored
Run class_validate() on properties (#675)
* Fix bug in `class_POSIXct` thus revealed * Fix bug in S4 validation thus revealed
1 parent b24e2d7 commit d1caf49

8 files changed

Lines changed: 65 additions & 9 deletions

File tree

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# S7 (development version)
22

33
* Errors thrown by S7 now report the function where they occurred, making it easier to track down the source of a problem (#646).
4+
* `class_POSIXct` uses the `tzone` attribute (not `tz`), and allows it to be absent (#401).
45
* Base type wrappers like `class_integer` now define their constructor and validator in the S7 namespace. (#553).
56
* Method dispatch on `class_missing` now correctly handles missing arguments forwarded through a wrapper functions (#595).
67
* `convert()` no longer automatically converts between sibling classes (classes that merely share a common ancestor); the default downcast now applies only when `to` is genuinely a descendant of `from`'s class (#509).
@@ -15,6 +16,7 @@
1516
* `new_object()` now gives an informative error when `.parent` is a class specification rather than an instance of the parent class (#409).
1617
* `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).
1718
* `new_object()` no longer re-runs property validators for properties inherited unchanged from an already-validated parent class, so constructing an instance of a deeply nested class hierarchy validates each property exactly once (#539).
19+
* `new_property()` now runs the property class's own validator when checking a value, not just the structural class check, so a property restricted to an S3 class (e.g. `class_factor`) now enforces constraints that aren't visible in `class()` (#401).
1820
* `new_property()` now accepts a `setter` that takes `self`, `name`, and `value` making it easy to reuse the same definition for multiple properties (#552).
1921
* `new_S3_class()` objects now work with `inherits()` (and other functions that use `nameOfClass()`) in R 4.3 and later (@lawremi, #521).
2022
* `print(<S7_class>)` now shows property defaults inline (`= "value"`) and annotates read-only properties (`[read-only]`) (#439).

R/S3.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,9 @@ validate_POSIXct <- function(self) {
184184
return("Underlying data must be numeric")
185185
}
186186

187-
tz <- attr(self, "tz")
188-
if (!is.character(tz) || length(tz) != 1) {
189-
return("attr(, 'tz') must be a single string")
187+
tz <- attr(self, "tzone", exact = TRUE)
188+
if (!is.null(tz) && (!is.character(tz) || length(tz) != 1)) {
189+
return("attr(, 'tzone') must be NULL or a single string")
190190
}
191191
}
192192

R/class-spec.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,10 @@ class_constructor <- function(.x) {
192192
class_validate <- function(class, object) {
193193
validator <- switch(
194194
class_type(class),
195-
S4 = methods::validObject,
195+
S4 = function(object) {
196+
check <- methods::validObject(object, test = TRUE)
197+
if (isTRUE(check)) NULL else check
198+
},
196199
S7 = class@validator,
197200
S7_base = class$validator,
198201
S7_S3 = class$validator,

R/property.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ prop_validate <- function(prop, value, object = NULL) {
278278
))
279279
}
280280

281+
class_error <- class_validate(prop$class, value)
282+
if (length(class_error) > 0) {
283+
return(paste0(prop_label(object, prop$name), ": ", class_error))
284+
}
285+
281286
if (is.null(validator <- prop$validator)) {
282287
return(NULL)
283288
}

tests/testthat/_snaps/S3.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,13 @@
6363
# catches invalid POSIXct
6464

6565
Code
66-
validate_POSIXct(structure("x", tz = "UTC"))
66+
validate_POSIXct(structure("x", tzone = "UTC"))
6767
Output
6868
[1] "Underlying data must be numeric"
6969
Code
70-
validate_POSIXct(structure(1, tz = 1))
70+
validate_POSIXct(structure(1, tzone = 1))
7171
Output
72-
[1] "attr(, 'tz') must be a single string"
72+
[1] "attr(, 'tzone') must be NULL or a single string"
7373

7474
# catches invalid data.frame
7575

tests/testthat/_snaps/property.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,24 @@
212212
! <foo> object properties are invalid:
213213
- @x must be length 1
214214

215+
# property validation runs the class's own validator
216+
217+
Code
218+
Foo(x = bad)
219+
Condition
220+
Error in `Foo()`:
221+
! <Foo> object properties are invalid:
222+
- @x: Not enough 'levels' for underlying data
223+
224+
# property validation runs an S4 class's validity method
225+
226+
Code
227+
Foo(x = bad)
228+
Condition
229+
Error in `Foo()`:
230+
! <Foo> object properties are invalid:
231+
- @x: n must be positive
232+
215233
# prop<- won't infinitly recurse on a custom setter
216234

217235
Code

tests/testthat/test-S3.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,10 @@ test_that("catches invalid dates", {
6767

6868
test_that("catches invalid POSIXct", {
6969
expect_snapshot({
70-
validate_POSIXct(structure("x", tz = "UTC"))
71-
validate_POSIXct(structure(1, tz = 1))
70+
validate_POSIXct(structure("x", tzone = "UTC"))
71+
validate_POSIXct(structure(1, tzone = 1))
7272
})
73+
expect_null(validate_POSIXct(Sys.time()))
7374
})
7475

7576
test_that("catches invalid data.frame", {

tests/testthat/test-property.R

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,33 @@ test_that("can validate with custom validator", {
484484
})
485485
})
486486

487+
test_that("property validation runs the class's own validator", {
488+
Foo <- new_class("Foo", package = NULL, properties = list(x = class_factor))
489+
490+
# A malformed factor passes the structural check (its class is "factor")
491+
# but fails the factor validator because it has too few levels.
492+
bad <- structure(1:3, levels = "a", class = "factor")
493+
expect_snapshot(Foo(x = bad), error = TRUE)
494+
})
495+
496+
test_that("property validation runs an S4 class's validity method", {
497+
PosNum <- methods::setClass(
498+
"PosNum",
499+
slots = c(n = "numeric"),
500+
validity = function(object) {
501+
if (object@n <= 0) "n must be positive" else TRUE
502+
}
503+
)
504+
on.exit(S4_remove_classes("PosNum"))
505+
Foo <- new_class("Foo", package = NULL, properties = list(x = PosNum))
506+
507+
# An S4 object that passes the structural check but fails its own validity
508+
# method is rejected
509+
bad <- PosNum(n = 1)
510+
bad@n <- -5
511+
expect_snapshot(Foo(x = bad), error = TRUE)
512+
})
513+
487514
test_that("prop<- won't infinitly recurse on a custom setter", {
488515
chattily_sync_ab <- function(self, value) {
489516
cat("Starting syncup with value:", value, "\n")

0 commit comments

Comments
 (0)