From 0154df1af31f440ff7f956eb1ac390989d048bb9 Mon Sep 17 00:00:00 2001
From: Teun van den Brand <tahvdbrand@gmail.com>
Date: Tue, 26 Nov 2024 13:56:48 +0100
Subject: [PATCH 1/3] initial infrastructure

---
 DESCRIPTION |  3 +++
 R/edition.R | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 75 insertions(+)
 create mode 100644 R/edition.R

diff --git a/DESCRIPTION b/DESCRIPTION
index 585a51285a..78c1675077 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -33,11 +33,13 @@ Depends:
     R (>= 4.0)
 Imports: 
     cli,
+    desc,
     grDevices,
     grid,
     gtable (>= 0.1.1),
     isoband,
     lifecycle (> 1.0.1),
+    pkgload,
     rlang (>= 1.1.0),
     scales (>= 1.3.0),
     stats,
@@ -122,6 +124,7 @@ Collate:
     'coord-transform.R'
     'data.R'
     'docs_layer.R'
+    'edition.R'
     'facet-.R'
     'facet-grid-.R'
     'facet-null.R'
diff --git a/R/edition.R b/R/edition.R
new file mode 100644
index 0000000000..9c2baa590c
--- /dev/null
+++ b/R/edition.R
@@ -0,0 +1,72 @@
+
+ggplot_edition <- new.env(parent = emptyenv())
+
+local_edition <- function(edition, .env = parent.frame()) {
+  stopifnot(is_zap(edition) || (is.numeric(edition) && length(edition) == 1))
+  pkg <- get_pkg_name(.env)
+  local_bindings(!!pkg := edition, .env = ggplot_edition, .frame = .env)
+}
+
+edition_get <- function(.env = parent.frame(), default = 2024L) {
+  pkg <- get_pkg_name(.env)
+
+  # Try to query edition from cache
+  edition <- env_get(ggplot_edition, nm = pkg, default = NULL)
+  if (!is.null(edition)) {
+    return(edition)
+  }
+
+  # Try to query package description
+  desc <- find_description(path = ".", package = pkg)
+  if (is.null(desc)) {
+    return(default)
+  }
+
+  # Look up edition from the description
+  field_name <- "Config/ggplot2/edition"
+  edition <- as.integer(desc$get_field(field_name, default = default))
+
+  # Cache result
+  env_bind(ggplot_edition, !!pkg := edition)
+  return(edition)
+}
+
+edition_deprecate <- function(edition, ..., .env = parent.frame()) {
+  check_number_whole(edition)
+  if (edition_get(.env) < edition) {
+    return(invisible(NULL))
+  }
+
+  edition <- I(paste0("edition ", edition))
+  lifecycle::deprecate_stop(edition, ...)
+}
+
+edition_require <- function(edition, what, .env = parent.frame()) {
+  check_number_whole(edition)
+  current <- edition_get(.env)
+  if (current >= edition) {
+    return(invisible(NULL))
+  }
+  msg <- paste0(what, " requires the ", edition, " edition of {.pkg ggplot2}.")
+  cli::cli_abort(msg)
+}
+
+find_description <- function(path, package = NULL) {
+  if (!is.null(package)) {
+    return(desc::desc(package = package))
+  } else {
+    try_fetch(
+      pkgload::pkg_desc(path),
+      error = function(e) NULL
+    )
+  }
+}
+
+get_pkg_name <- function(env = parent.frame()) {
+  env  <- topenv(env)
+  name <- environmentName(env)
+  if (!isNamespace(env) && name != "R_GlobalEnv") {
+    return(NULL)
+  }
+  name
+}

From 28ca4f99fa2f059eeec1aee9ef8f5375a4215041 Mon Sep 17 00:00:00 2001
From: Teun van den Brand <tahvdbrand@gmail.com>
Date: Mon, 6 Jan 2025 11:54:32 +0100
Subject: [PATCH 2/3] avoid desc/pkgload dependencies

---
 DESCRIPTION |  2 --
 R/edition.R | 17 +++++++++--------
 2 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/DESCRIPTION b/DESCRIPTION
index 78c1675077..c7fffa1b49 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -33,13 +33,11 @@ Depends:
     R (>= 4.0)
 Imports: 
     cli,
-    desc,
     grDevices,
     grid,
     gtable (>= 0.1.1),
     isoband,
     lifecycle (> 1.0.1),
-    pkgload,
     rlang (>= 1.1.0),
     scales (>= 1.3.0),
     stats,
diff --git a/R/edition.R b/R/edition.R
index 9c2baa590c..0d4ac73187 100644
--- a/R/edition.R
+++ b/R/edition.R
@@ -17,14 +17,14 @@ edition_get <- function(.env = parent.frame(), default = 2024L) {
   }
 
   # Try to query package description
-  desc <- find_description(path = ".", package = pkg)
-  if (is.null(desc)) {
+  desc_file <- find_description(path = ".", package = pkg)
+  if (is.null(desc_file)) {
     return(default)
   }
 
   # Look up edition from the description
   field_name <- "Config/ggplot2/edition"
-  edition <- as.integer(desc$get_field(field_name, default = default))
+  edition <- as.integer(read.dcf(desc_file, fields = field_name))
 
   # Cache result
   env_bind(ggplot_edition, !!pkg := edition)
@@ -53,13 +53,14 @@ edition_require <- function(edition, what, .env = parent.frame()) {
 
 find_description <- function(path, package = NULL) {
   if (!is.null(package)) {
-    return(desc::desc(package = package))
+    path <- system.file(package = package, "DESCRIPTION")
   } else {
-    try_fetch(
-      pkgload::pkg_desc(path),
-      error = function(e) NULL
-    )
+    path <- file.path(path, "DESCRIPTION")
   }
+  if (!file.exists(path)) {
+    return(NULL)
+  }
+  path
 }
 
 get_pkg_name <- function(env = parent.frame()) {

From 43c10eeae6dcc56f065180cd695e25e804ede844 Mon Sep 17 00:00:00 2001
From: Teun van den Brand <tahvdbrand@gmail.com>
Date: Mon, 6 Jan 2025 12:10:58 +0100
Subject: [PATCH 3/3] add basic test

---
 tests/testthat/_snaps/edition.md | 16 ++++++++++++++++
 tests/testthat/test-edition.R    | 10 ++++++++++
 2 files changed, 26 insertions(+)
 create mode 100644 tests/testthat/_snaps/edition.md
 create mode 100644 tests/testthat/test-edition.R

diff --git a/tests/testthat/_snaps/edition.md b/tests/testthat/_snaps/edition.md
new file mode 100644
index 0000000000..53b698d408
--- /dev/null
+++ b/tests/testthat/_snaps/edition.md
@@ -0,0 +1,16 @@
+# basic edition infrastructure works as intended
+
+    Code
+      edition_deprecate(2025, what = "foo()")
+    Condition
+      Error:
+      ! `foo()` was deprecated in ggplot2 edition 2025 and is now defunct.
+
+---
+
+    Code
+      edition_require(2025, what = "foo()")
+    Condition
+      Error in `edition_require()`:
+      ! foo() requires the 2025 edition of ggplot2.
+
diff --git a/tests/testthat/test-edition.R b/tests/testthat/test-edition.R
new file mode 100644
index 0000000000..20378a164d
--- /dev/null
+++ b/tests/testthat/test-edition.R
@@ -0,0 +1,10 @@
+test_that("basic edition requirement and deprecation works as intended", {
+
+  local_edition(2025)
+  expect_snapshot(edition_deprecate(2025, what = "foo()"), error = TRUE)
+  expect_silent(edition_require(2025, what = "foo()"))
+
+  local_edition(2024)
+  expect_silent(edition_deprecate(2025, what = "foo()"))
+  expect_snapshot(edition_require(2025, what = "foo()"), error = TRUE)
+})