Skip to content

Commit f21a858

Browse files
authored
Fallback for validate_subclass() (#6119)
* add lookup via constructor * add tests * add news bullet * catch errors when constructor needs arguments
1 parent 5b99d3c commit f21a858

File tree

4 files changed

+72
-13
lines changed

4 files changed

+72
-13
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@
314314
(@teunbrand, #4335).
315315
* `ggsave()` can write a multi-page pdf file when provided with a list of plots
316316
(@teunbrand, #5093).
317+
* (internal) When `validate_subclass()` fails to find a class directly, it tries
318+
to retrieve the class via constructor functions (@teunbrand).
317319

318320
# ggplot2 3.5.1
319321

R/layer.R

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -458,18 +458,50 @@ validate_subclass <- function(x, subclass,
458458

459459
if (inherits(x, subclass)) {
460460
return(x)
461-
} else if (is_scalar_character(x)) {
462-
name <- paste0(subclass, camelize(x, first = TRUE))
463-
obj <- find_global(name, env = env)
461+
}
462+
if (!is_scalar_character(x)) {
463+
stop_input_type(x, as_cli("either a string or a {.cls {subclass}} object"), arg = x_arg)
464+
}
464465

465-
if (is.null(obj) || !inherits(obj, subclass)) {
466-
cli::cli_abort("Can't find {argname} called {.val {x}}.", call = call)
467-
}
466+
# Try getting class object directly
467+
name <- paste0(subclass, camelize(x, first = TRUE))
468+
obj <- find_global(name, env = env)
469+
if (inherits(obj, subclass)) {
470+
return(obj)
471+
}
472+
473+
# Try retrieving class via constructors
474+
name <- snakeize(name)
475+
obj <- find_global(name, env = env, mode = "function")
476+
if (is.function(obj)) {
477+
obj <- try_fetch(
478+
obj(),
479+
error = function(cnd) {
480+
# replace `obj()` call with name of actual constructor
481+
cnd$call <- call(name)
482+
cli::cli_abort(
483+
"Failed to retrieve a {.cls {subclass}} object from {.fn {name}}.",
484+
parent = cnd, call = call
485+
)
486+
})
487+
}
488+
# Position constructors return classes directly
489+
if (inherits(obj, subclass)) {
490+
return(obj)
491+
}
492+
# Try prying the class from a layer
493+
if (inherits(obj, "Layer")) {
494+
obj <- switch(
495+
subclass,
496+
Geom = obj$geom,
497+
Stat = obj$stat,
498+
NULL
499+
)
500+
}
501+
if (inherits(obj, subclass)) {
468502
return(obj)
469-
} else if (is.null(x)) {
470-
cli::cli_abort("The {.arg {x_arg}} argument cannot be empty.", call = call)
471503
}
472-
stop_input_type(x, as_cli("either a string or a {.cls {subclass}} object"))
504+
cli::cli_abort("Can't find {argname} called {.val {x}}.", call = call)
473505
}
474506

475507
# helper function to adjust the draw_key slot of a geom

tests/testthat/_snaps/layer.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# layer() checks its input
22

3-
The `geom` argument cannot be empty.
3+
`geom` must be either a string or a <Geom> object, not `NULL`.
44

55
---
66

7-
The `stat` argument cannot be empty.
7+
`stat` must be either a string or a <Stat> object, not `NULL`.
88

99
---
1010

11-
The `position` argument cannot be empty.
11+
`position` must be either a string or a <Position> object, not `NULL`.
1212

1313
---
1414

@@ -25,7 +25,13 @@
2525

2626
---
2727

28-
`x` must be either a string or a <geom> object, not an environment.
28+
`environment()` must be either a string or a <geom> object, not an environment.
29+
30+
---
31+
32+
Failed to retrieve a <Geom> object from `geom_foo()`.
33+
Caused by error in `geom_foo()`:
34+
! This function is unconstructable.
2935

3036
# unknown params create warning
3137

tests/testthat/test-layer.R

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ test_that("layer() checks its input", {
1010

1111
expect_snapshot_error(validate_subclass("test", "geom"))
1212
expect_snapshot_error(validate_subclass(environment(), "geom"))
13+
14+
geom_foo <- function(...) stop("This function is unconstructable.")
15+
expect_snapshot_error(layer("foo", "identity", position = "identity"))
1316
})
1417

1518
test_that("aesthetics go in aes_params", {
@@ -154,6 +157,22 @@ test_that("layer names can be resolved", {
154157
expect_snapshot(p + l + l, error = TRUE)
155158
})
156159

160+
test_that("check_subclass can resolve classes via constructors", {
161+
162+
env <- new_environment(list(
163+
geom_foobar = geom_point,
164+
stat_foobar = stat_boxplot,
165+
position_foobar = position_nudge,
166+
guide_foobar = guide_axis_theta
167+
))
168+
169+
expect_s3_class(validate_subclass("foobar", "Geom", env = env), "GeomPoint")
170+
expect_s3_class(validate_subclass("foobar", "Stat", env = env), "StatBoxplot")
171+
expect_s3_class(validate_subclass("foobar", "Position", env = env), "PositionNudge")
172+
expect_s3_class(validate_subclass("foobar", "Guide", env = env), "GuideAxisTheta")
173+
174+
})
175+
157176
test_that("attributes on layer data are preserved", {
158177
# This is a good layer for testing because:
159178
# * It needs to compute a statistic at the group level

0 commit comments

Comments
 (0)