Skip to content

Commit f08d6f9

Browse files
gustavdeliusclaude
andauthored
Let extensions upgrade their own data via versioned @extensions (#403)
mizer could upgrade core slots across mizer versions, but extensions had no hook to migrate their own data and no record of which extension version last conformed an object. This adds per-extension version tracking and an upgrade dispatch path so extensions can upgrade their objects independently of the mizer version. - The `@extensions` slot (now typed "ANY") may hold either the legacy named character vector of requirement strings or a named list of `c(requirement, version)` entries. `objectExtensions()` returns the requirement-only view, so all dispatch/suffix logic is unchanged. New helpers: `recordExtension()` (exported), `extensionRequirements()`, `extensionVersions()`, `extensionVersion()`, `makeExtensions()`. - `upgradeParams()`/`upgradeSim()` are now the `upgrade.MizerParams()` / `upgrade.MizerSim()` methods of `utils::upgrade()`; the old names remain as thin internal wrappers. - `validParams()` orchestrates upgrades: it runs the core upgrade when `@mizer_version` is stale, then `runExtensionUpgrades()` calls each out-of-date extension's `upgrade` method directly (no `NextMethod()`) and re-stamps it. `needs_upgrading()` now also fires on a missing or stale extension stamp. - `registerExtensions()` / `readParams()` / `requiredExtensionPackages()` accept the versioned list form via the requirement view. - Documented the pattern in the creating-extension-packages vignette and added tests in test-extension-versions.R. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0c9ec6c commit f08d6f9

30 files changed

Lines changed: 754 additions & 41 deletions

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ S3method(summary,ArrayTimeBySpecies)
238238
S3method(summary,ArrayTimeBySpeciesBySize)
239239
S3method(summary,MizerParams)
240240
S3method(summary,MizerSim)
241+
S3method(utils::upgrade,MizerParams)
242+
S3method(utils::upgrade,MizerSim)
241243
S3method(validParams,MizerParams)
242244
S3method(validSim,MizerSim)
243245
export("catchability<-")
@@ -500,6 +502,7 @@ export(project_simple)
500502
export(psi)
501503
export(readParams)
502504
export(readSim)
505+
export(recordExtension)
503506
export(registerExtension)
504507
export(registerExtensions)
505508
export(removeBackgroundSpecies)

NEWS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# mizer (development version)
22

3+
- Extension packages can now upgrade their own data in saved model objects
4+
independently of the mizer version. The `@extensions` slot can record, for
5+
each extension, the version of the extension package that the object conforms
6+
to (write entries with the new `recordExtension()`). `needs_upgrading()` flags
7+
an object when an extension's recorded version is missing or older than the
8+
installed package version, and `validParams()` then calls the extension's own
9+
`upgrade()` method (an S3 method on `utils::upgrade()`, registered with
10+
`@exportS3Method utils::upgrade`). The core mizer upgrade is now itself the
11+
`upgrade.MizerParams()` / `upgrade.MizerSim()` method. See the "Upgrading
12+
objects across versions of your extension" section of
13+
`vignette("creating-extension-packages")`.
14+
315
- `getTrophicLevel()` and `getTrophicLevelBySpecies()` now assign the resource a
416
size-dependent trophic level
517
`T_R(w) = max(1, 1 + log(w / w_R) / log(beta_R))` instead of treating it as

R/MizerParams-class.R

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,16 @@ validMizerParams <- function(object) {
288288
#' @slot metadata A list with metadata information. See [setMetadata()].
289289
#' @slot mizer_version The package version of mizer (as returned by
290290
#' `packageVersion("mizer")`) that created or upgraded the model.
291-
#' @slot extensions A named vector of strings describing the extension chain
292-
#' needed to run the model. The names are extension identifiers and S4 marker
293-
#' class names. The order is the S3 dispatch order, from outermost to
294-
#' innermost extension. The values are version strings, installation
295-
#' specifications, or `NA_character_`. Extension subclasses are marker
291+
#' @slot extensions Describes the extension chain needed to run the model. The
292+
#' entries are named by extension identifier (also the S4 marker class name)
293+
#' and ordered in S3 dispatch order, from outermost to innermost extension.
294+
#' It is either a named character vector whose values are requirement strings
295+
#' (version strings, installation specifications, or `NA_character_`), or a
296+
#' named list whose entries are length-2 character vectors
297+
#' `c(requirement = ..., version = ...)`. The `version` records the version of
298+
#' the extension package that last upgraded the object (`NA` if unknown) and
299+
#' is used by [needs_upgrading()]. Use [recordExtension()] to write entries
300+
#' rather than modifying the slot directly. Extension subclasses are marker
296301
#' classes only and must not add slots.
297302
#' @slot time_created A POSIXct date-time object with the creation time.
298303
#' @slot time_modified A POSIXct date-time object with the last modified time.
@@ -426,7 +431,7 @@ setClass(
426431
slots = c(
427432
metadata = "list",
428433
mizer_version = "ANY",
429-
extensions = "character",
434+
extensions = "ANY",
430435
time_created = "POSIXct",
431436
time_modified = "POSIXct",
432437
w = "numeric",
@@ -918,12 +923,15 @@ validParams <- function(params, info_level = 3) {
918923
#' @export
919924
validParams.MizerParams <- function(params, info_level = 3) {
920925

921-
if (needs_upgrading(params)) {
922-
params <- suppressWarnings(upgradeParams(params))
926+
if (mizer_needs_upgrading(params)) {
927+
params <- suppressWarnings(upgrade.MizerParams(params))
923928
if (info_level > 0) {
924929
warning("Your MizerParams object was created with an earlier version of mizer. You can upgrade it with `params <- validParams(params)` where you should replace `params` by the name of the variable that holds your MizerParams object.")
925930
}
926931
}
932+
if (extension_needs_upgrading(params)) {
933+
params <- suppressWarnings(runExtensionUpgrades(params))
934+
}
927935

928936
params@given_species_params <-
929937
validGivenSpeciesParams(params@given_species_params)

R/registerExtensions.R

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,170 @@ coerceToExtensionClass <- function(object, extensions = objectExtensions(object)
175175
#' @keywords internal
176176
objectExtensions <- function(object) {
177177
if (is(object, "MizerParams")) {
178-
return(object@extensions)
178+
return(extensionRequirements(object@extensions))
179179
}
180180
if (is(object, "MizerSim")) {
181-
return(object@params@extensions)
181+
return(extensionRequirements(object@params@extensions))
182182
}
183183
stop("Can only get extensions for MizerParams or MizerSim objects.")
184184
}
185185

186+
#' Extract the requirement view of an extension chain
187+
#'
188+
#' The `@extensions` slot may be stored either as a named character vector of
189+
#' requirement strings (the legacy/unversioned form) or as a named list whose
190+
#' entries are length-2 character vectors `c(requirement = ..., version = ...)`.
191+
#' This helper returns the requirement strings as a plain named character
192+
#' vector, which is the form used for dispatch and suffix comparison.
193+
#'
194+
#' @param ext The contents of an `@extensions` slot (character vector or list).
195+
#' @return A named character vector of requirement strings.
196+
#' @keywords internal
197+
extensionRequirements <- function(ext) {
198+
if (is.null(ext) || length(ext) == 0) {
199+
return(character())
200+
}
201+
if (is.list(ext)) {
202+
req <- vapply(ext, function(e) {
203+
if (!is.null(names(e)) && "requirement" %in% names(e)) {
204+
as.character(e[["requirement"]])
205+
} else {
206+
as.character(e[[1]])
207+
}
208+
}, character(1))
209+
names(req) <- names(ext)
210+
return(req)
211+
}
212+
ext
213+
}
214+
215+
#' Extract the version stamps of an extension chain
216+
#'
217+
#' Returns the version of the extension package that last upgraded the object
218+
#' for each extension, or `NA_character_` where no stamp is recorded (including
219+
#' the legacy character-vector form, which carries no versions).
220+
#'
221+
#' @param ext The contents of an `@extensions` slot (character vector or list).
222+
#' @return A named character vector of version strings (`NA` where unknown).
223+
#' @keywords internal
224+
extensionVersions <- function(ext) {
225+
if (is.null(ext) || length(ext) == 0) {
226+
return(character())
227+
}
228+
if (is.list(ext)) {
229+
ver <- vapply(ext, function(e) {
230+
if (!is.null(names(e)) && "version" %in% names(e)) {
231+
as.character(e[["version"]])
232+
} else {
233+
NA_character_
234+
}
235+
}, character(1))
236+
names(ver) <- names(ext)
237+
return(ver)
238+
}
239+
setNames(rep(NA_character_, length(ext)), names(ext))
240+
}
241+
242+
#' Build a versioned extension list from requirements and versions
243+
#'
244+
#' @param requirements A named character vector of requirement strings.
245+
#' @param versions A named character vector of version stamps. Names not present
246+
#' default to `NA_character_`.
247+
#' @return A named list whose entries are
248+
#' `c(requirement = ..., version = ...)`, or an empty character vector when
249+
#' `requirements` is empty.
250+
#' @keywords internal
251+
makeExtensions <- function(requirements, versions = character()) {
252+
if (length(requirements) == 0) {
253+
return(character())
254+
}
255+
out <- lapply(names(requirements), function(name) {
256+
version <- if (name %in% names(versions)) {
257+
as.character(versions[[name]])
258+
} else {
259+
NA_character_
260+
}
261+
c(requirement = unname(as.character(requirements[[name]])),
262+
version = version)
263+
})
264+
names(out) <- names(requirements)
265+
out
266+
}
267+
268+
#' Record an extension and its version stamp on a mizer object
269+
#'
270+
#' Writes an entry for `name` into the object's `@extensions` slot, converting
271+
#' the slot to the versioned list form. Existing entries (and their version
272+
#' stamps) are preserved. The requirement is taken from the existing entry if
273+
#' present, otherwise from the registered extension chain.
274+
#'
275+
#' Extension packages should call this instead of assigning to `@extensions`
276+
#' directly. Pass `version` (typically `packageVersion(name)`) only when the
277+
#' object has just been created or upgraded to conform to that version; leave it
278+
#' `NULL` for ordinary modifications so the existing stamp is preserved.
279+
#'
280+
#' @param params A `MizerParams` object.
281+
#' @param name The extension identifier (its S4 marker class name).
282+
#' @param version Optional version string to stamp. If `NULL` (default) the
283+
#' existing stamp is preserved.
284+
#' @return The `params` object with the updated `@extensions` slot.
285+
#' @seealso "Creating a mizer extension package":
286+
#' \code{vignette("creating-extension-packages", package = "mizer")}
287+
#' @export
288+
#' @family extension tools
289+
recordExtension <- function(params, name, version = NULL) {
290+
assert_that(is(params, "MizerParams"), is.string(name))
291+
ext <- params@extensions
292+
reqs <- extensionRequirements(ext)
293+
present <- name %in% names(reqs)
294+
295+
# Requirement: keep the existing one, else take it from the registry.
296+
if (present) {
297+
req <- unname(reqs[[name]])
298+
} else {
299+
registered <- getRegisteredExtensions()
300+
req <- if (name %in% names(registered)) {
301+
unname(registered[[name]])
302+
} else {
303+
NA_character_
304+
}
305+
}
306+
307+
if (is.null(version)) {
308+
# Preserve the existing stamp. If the entry already exists, leave the
309+
# slot exactly as it is so ordinary modifications do not perturb it.
310+
if (present) return(params)
311+
# Otherwise add an unversioned entry, keeping the slot's current form.
312+
if (is.list(ext)) {
313+
ext[[name]] <- c(requirement = req, version = NA_character_)
314+
} else {
315+
ext[[name]] <- req
316+
}
317+
params@extensions <- ext
318+
return(params)
319+
}
320+
321+
# Stamping a version requires the versioned list form for this entry.
322+
if (!is.list(ext)) {
323+
ext <- makeExtensions(reqs, extensionVersions(ext))
324+
if (!is.list(ext)) ext <- setNames(list(), character())
325+
}
326+
ext[[name]] <- c(requirement = req, version = as.character(version))
327+
params@extensions <- ext
328+
params
329+
}
330+
331+
#' Get the recorded version stamp for one extension on an object
332+
#'
333+
#' @param params A `MizerParams` object.
334+
#' @param name The extension identifier.
335+
#' @return The recorded version string, or `NA_character_` if none.
336+
#' @keywords internal
337+
extensionVersion <- function(params, name) {
338+
vers <- extensionVersions(params@extensions)
339+
if (name %in% names(vers)) vers[[name]] else NA_character_
340+
}
341+
186342
#' Assert that an object's extension chain is compatible with the session
187343
#'
188344
#' Stops with an informative error if the object's extension chain is not a
@@ -259,6 +415,11 @@ validateExtensionsVector <- function(extensions) {
259415
if (is.null(extensions)) {
260416
extensions <- character()
261417
}
418+
# Accept the versioned list form of an `@extensions` slot by reducing it to
419+
# the requirement view that the registry and dispatch logic operate on.
420+
if (is.list(extensions)) {
421+
extensions <- extensionRequirements(extensions)
422+
}
262423
if (!is.character(extensions)) {
263424
stop("`extensions` must be a named character vector.")
264425
}

R/saveParams.R

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ readParams <- function(file, install_extensions = FALSE) {
5353
}
5454

5555
if (length(params@extensions) > 0) {
56-
registerExtensions(params@extensions, install = install_extensions)
56+
registerExtensions(extensionRequirements(params@extensions),
57+
install = install_extensions)
5758
}
5859

5960
params <- coerceToExtensionClass(params)
@@ -117,7 +118,8 @@ readSim <- function(file, install_extensions = FALSE) {
117118
}
118119

119120
if (length(sim@params@extensions) > 0) {
120-
registerExtensions(sim@params@extensions, install = install_extensions)
121+
registerExtensions(extensionRequirements(sim@params@extensions),
122+
install = install_extensions)
121123
}
122124

123125
sim@params <- coerceToExtensionClass(sim@params)
@@ -178,7 +180,8 @@ checkCustomFunctions <- function(params) {
178180
}
179181

180182
requiredExtensionPackages <- function(params) {
181-
c("mizer", names(params@extensions)[!is.na(params@extensions)])
183+
reqs <- extensionRequirements(params@extensions)
184+
c("mizer", names(reqs)[!is.na(reqs)])
182185
}
183186

184187
is_custom <- function(name, packages) {

0 commit comments

Comments
 (0)