diff --git a/DESCRIPTION b/DESCRIPTION index 01f91fcb9a..d0d2072f3d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -281,6 +281,7 @@ Collate: 'utilities-break.R' 'utilities-grid.R' 'utilities-help.R' + 'utilities-lifecycle.R' 'utilities-patterns.R' 'utilities-performance.R' 'utilities-resolution.R' diff --git a/NAMESPACE b/NAMESPACE index ce32705b6d..526917cdd9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -671,6 +671,7 @@ export(scale_y_reverse) export(scale_y_sqrt) export(scale_y_time) export(sec_axis) +export(set_edition) export(set_last_plot) export(set_theme) export(sf_transform_xy) diff --git a/R/annotation-logticks.R b/R/annotation-logticks.R index 548a89470b..7757bafa45 100644 --- a/R/annotation-logticks.R +++ b/R/annotation-logticks.R @@ -93,7 +93,7 @@ annotation_logticks <- function(base = 10, sides = "bl", outside = FALSE, scaled if (!is.null(color)) colour <- color - lifecycle::signal_stage("superseded", "annotation_logticks()", "guide_axis_logticks()") + supersede("2026", "annotation_logticks()", "guide_axis_logticks()") if (lifecycle::is_present(size)) { deprecate("3.5.0", I("Using the `size` aesthetic in this geom"), I("`linewidth`")) diff --git a/R/coord-flip.R b/R/coord-flip.R index 3ea68a8273..f19b75c64d 100644 --- a/R/coord-flip.R +++ b/R/coord-flip.R @@ -57,7 +57,7 @@ #' geom_area() + #' coord_flip() coord_flip <- function(xlim = NULL, ylim = NULL, expand = TRUE, clip = "on") { - lifecycle::signal_stage("superseded", "coord_flip()") + supersede("2026", "coord_flip()", with = I("swapping x and y aesthetics")) check_coord_limits(xlim) check_coord_limits(ylim) ggproto(NULL, CoordFlip, diff --git a/R/coord-map.R b/R/coord-map.R index b23ca8abad..50938617f2 100644 --- a/R/coord-map.R +++ b/R/coord-map.R @@ -136,7 +136,7 @@ coord_map <- function(projection="mercator", ..., parameters = NULL, orientation } else { params <- parameters } - lifecycle::signal_stage("superseded", "coord_map()", "coord_sf()") + supersede("2026", "coord_map()", "coord_sf()") check_coord_limits(xlim) check_coord_limits(ylim) diff --git a/R/coord-polar.R b/R/coord-polar.R index 107cbf0f74..b4763da20f 100644 --- a/R/coord-polar.R +++ b/R/coord-polar.R @@ -3,7 +3,7 @@ coord_polar <- function(theta = "x", start = 0, direction = 1, clip = "on") { theta <- arg_match0(theta, c("x", "y")) r <- if (theta == "x") "y" else "x" - lifecycle::signal_stage("superseded", "coord_polar()", "coord_radial()") + supersede("2026", "coord_polar()", "coord_radial()") ggproto(NULL, CoordPolar, theta = theta, diff --git a/R/ggplot-global.R b/R/ggplot-global.R index 495dc65ae0..dfcbbbccc9 100644 --- a/R/ggplot-global.R +++ b/R/ggplot-global.R @@ -52,3 +52,5 @@ ggplot_global$x_aes <- c("x", "xmin", "xmax", "xend", "xintercept", ggplot_global$y_aes <- c("y", "ymin", "ymax", "yend", "yintercept", "ymin_final", "ymax_final", "lower", "middle", "upper", "y0") + +ggplot_global$edition <- NULL diff --git a/R/labels.R b/R/labels.R index 87e5bd2d63..ab47b2a72d 100644 --- a/R/labels.R +++ b/R/labels.R @@ -239,21 +239,21 @@ labs <- function(..., title = waiver(), subtitle = waiver(), #' superseded. It is recommended to use the `labs(x, y, title, subtitle)` #' arguments instead. xlab <- function(label) { - lifecycle::signal_stage("superseded", "xlab()", "labs(x)") + supersede("2026", "xlab()", "labs(x)") labs(x = label) } #' @rdname labs #' @export ylab <- function(label) { - lifecycle::signal_stage("superseded", "ylab()", "labs(y)") + supersede("2026", "ylab()", "labs(y)") labs(y = label) } #' @rdname labs #' @export ggtitle <- function(label, subtitle = waiver()) { - lifecycle::signal_stage("superseded", "ggtitle()", I("labs(title, subtitle)")) + supersede("2026", "ggtitle()", I("labs(title, subtitle)")) labs(title = label, subtitle = subtitle) } diff --git a/R/limits.R b/R/limits.R index 976307a467..25d4a1d4f9 100644 --- a/R/limits.R +++ b/R/limits.R @@ -184,7 +184,8 @@ limits.POSIXlt <- function(lims, var, call = caller_env()) { expand_limits <- function(...) { data <- list2(...) - lifecycle::signal_stage("superseded", "expand_limits()") + + supersede("2026", "expand_limits()") # unpack data frame columns data_dfs <- vapply(data, is.data.frame, logical(1)) diff --git a/R/utilities-lifecycle.R b/R/utilities-lifecycle.R new file mode 100644 index 0000000000..0173829766 --- /dev/null +++ b/R/utilities-lifecycle.R @@ -0,0 +1,108 @@ +#' Set ggplot2 edition +#' +#' ggplot2 uses the 'edition' concept to manage the lifecycles of functions and +#' arguments. Setting a recent edition opens up the latest features but also +#' closes down deprecated and superseded functionality. +#' +#' @param edition An edition. Possible values currently include `"2026"` only. +#' Can be `NULL` (default) to unset an edition. +#' +#' @returns The previous `edition` value. This function is called for the side +#' effect of setting the edition though. +#' @export +#' @keywords internal +#' +#' @examples +#' set_edition(2026) +set_edition <- function(edition = NULL) { + old <- ggplot_global$edition + ggplot_global$edition <- validate_edition(edition) + invisible(old) +} + +get_edition <- function() { + ggplot_global$edition[[1]] +} + +# Any new editions should be appended here and anchored to a version +edition_versions <- c( + "2025" = "4.0.0", + "2026" = "4.1.0" +) + +validate_edition <- function(edition, allow_null = TRUE, call = caller_env()) { + if (is.null(edition) && allow_null) { + return(NULL) + } + edition <- as.character(edition) + check_string(edition, allow_empty = FALSE, call = call) + arg_match0(edition, names(edition_versions), error_call = call) +} + +edition_require <- function(edition = NULL, what, call = caller_env()) { + edition <- validate_edition(edition) + current_edition <- get_edition() + if ( + !is.null(current_edition) && + as.numeric(current_edition) >= as.numeric(edition) + ) { + return(invisible()) + } + cli::cli_abort( + "{what} requires the {edition} edition of {.pkg ggplot2}.", + call = call + ) +} + +deprecate <- function(when, ..., id = NULL, always = FALSE, user_env = NULL, + escalate = NULL) { + + defunct <- "3.0.0" + full <- "3.4.0" + soft <- utils::packageVersion("ggplot2") + + if (identical(escalate, "delay")) { + soft <- full + full <- defunct + defunct <- "0.0.0" + } + + edition <- get_edition() + if (!is.null(edition) && edition %in% names(edition_versions)) { + soft <- full <- defunct <- edition_versions[[edition]] + } + + version <- as.package_version(when) + if (version <= defunct || identical(escalate, "abort")) { + lifecycle::deprecate_stop(when, ...) + } + user_env <- user_env %||% getOption("ggplot2_plot_env") %||% caller_env(2) + if (version <= full || identical(escalate, "warn")) { + lifecycle::deprecate_warn(when, ..., id = id, always = always, user_env = user_env) + } else if (version <= soft) { + lifecycle::deprecate_soft(when, ..., id = id, user_env = user_env) + } + invisible() +} + +supersede <- function(edition, what, with = NULL, ..., env = caller_env()) { + current_edition <- get_edition() + if ( + !is.null(current_edition) && + current_edition %in% names(edition_versions) && + as.numeric(current_edition) >= as.numeric(edition) + ) { + lifecycle::deprecate_stop( + when = paste0("edition ", edition), + what = what, + with = with, + env = env + ) + } + lifecycle::signal_stage( + stage = "superseded", + what = what, + with = with, + env = env + ) +} diff --git a/R/utilities.R b/R/utilities.R index 598b9a8fc3..4b72934d36 100644 --- a/R/utilities.R +++ b/R/utilities.R @@ -789,32 +789,6 @@ as_cli <- function(..., env = caller_env()) { cli::cli_fmt(cli::cli_text(..., .envir = env)) } -deprecate <- function(when, ..., id = NULL, always = FALSE, user_env = NULL, - escalate = NULL) { - - defunct <- "3.0.0" - full <- "3.4.0" - soft <- utils::packageVersion("ggplot2") - - if (identical(escalate, "delay")) { - soft <- full - full <- defunct - defunct <- "0.0.0" - } - - version <- as.package_version(when) - if (version < defunct || identical(escalate, "abort")) { - lifecycle::deprecate_stop(when, ...) - } - user_env <- user_env %||% getOption("ggplot2_plot_env") %||% caller_env(2) - if (version <= full || identical(escalate, "warn")) { - lifecycle::deprecate_warn(when, ..., id = id, always = always, user_env = user_env) - } else if (version <= soft) { - lifecycle::deprecate_soft(when, ..., id = id, user_env = user_env) - } - invisible() -} - as_unordered_factor <- function(x) { x <- as.factor(x) class(x) <- setdiff(class(x), "ordered") diff --git a/man/set_edition.Rd b/man/set_edition.Rd new file mode 100644 index 0000000000..dbbcc566a3 --- /dev/null +++ b/man/set_edition.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utilities-lifecycle.R +\name{set_edition} +\alias{set_edition} +\title{Set ggplot2 edition} +\usage{ +set_edition(edition = NULL) +} +\arguments{ +\item{edition}{An edition. Possible values currently include \code{"2026"} only. +Can be \code{NULL} (default) to unset an edition.} +} +\value{ +The previous \code{edition} value. This function is called for the side +effect of setting the edition though. +} +\description{ +ggplot2 uses the 'edition' concept to manage the lifecycles of functions and +arguments. Setting a recent edition opens up the latest features but also +closes down deprecated and superseded functionality. +} +\examples{ +set_edition(2026) +} +\keyword{internal} diff --git a/tests/testthat/_snaps/utilities-lifecycle.md b/tests/testthat/_snaps/utilities-lifecycle.md new file mode 100644 index 0000000000..87255851d8 --- /dev/null +++ b/tests/testthat/_snaps/utilities-lifecycle.md @@ -0,0 +1,39 @@ +# editions can be set and unset + + Code + set_edition("nonsense") + Condition + Error in `set_edition()`: + ! `edition` must be one of "2025" or "2026", not "nonsense". + +# edition deprecation works + + `foo()` was deprecated in ggplot2 4.0.0. + i Please use `bar()` instead. + +--- + + Code + foo() + Condition + Error: + ! `foo()` was deprecated in ggplot2 4.0.0 and is now defunct. + i Please use `bar()` instead. + +# edition supersession works + + Code + foo() + Condition + Error: + ! `foo()` was deprecated in edition 2025 and is now defunct. + i Please use `bar()` instead. + +# edition requirements work + + Code + foo() + Condition + Error in `foo()`: + ! foo() requires the 2025 edition of ggplot2. + diff --git a/tests/testthat/test-utilities-lifecycle.R b/tests/testthat/test-utilities-lifecycle.R new file mode 100644 index 0000000000..ad93d50158 --- /dev/null +++ b/tests/testthat/test-utilities-lifecycle.R @@ -0,0 +1,56 @@ +test_that("editions can be set and unset", { + + x <- set_edition(2026) + expect_null(x) # Set edition returns old value + expect_equal(get_edition(), "2026") + + x <- set_edition(NULL) + expect_equal(x, "2026") + expect_equal(get_edition(), NULL) + + # Test invalid values + expect_snapshot( + set_edition("nonsense"), + error = TRUE + ) +}) + +test_that("edition deprecation works", { + foo <- function() { + deprecate("4.0.0", what = "foo()", with = "bar()") + } + expect_snapshot_warning(foo()) + + set_edition(2025) + withr::defer(set_edition()) + + expect_snapshot(foo(), error = TRUE) +}) + +test_that("edition supersession works", { + foo <- function() { + supersede("2025", what = "foo()", with = "bar()") + NULL + } + expect_silent(foo()) + + set_edition(2025) + withr::defer(set_edition()) + + expect_snapshot(foo(), error = TRUE) +}) + +test_that("edition requirements work", { + + foo <- function() { + edition_require("2025", what = "foo()") + NULL + } + + expect_snapshot(foo(), error = TRUE) + + set_edition(2025) + withr::defer(set_edition()) + + expect_silent(foo()) +})