From 294e57caccd4c37d993dc2e9509e0614df89db98 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 22 May 2026 14:55:42 -0500 Subject: [PATCH 1/9] Implement `S7_classes()` and `S7_generics()` Fixes #335 --- NAMESPACE | 2 ++ NEWS.md | 1 + R/introspect.R | 36 ++++++++++++++++++++++++++++++++ _pkgdown.yml | 5 ++++- man/S7_classes.Rd | 31 +++++++++++++++++++++++++++ tests/testthat/test-introspect.R | 29 +++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 R/introspect.R create mode 100644 man/S7_classes.Rd create mode 100644 tests/testthat/test-introspect.R diff --git a/NAMESPACE b/NAMESPACE index c0dfa06a..77542cc4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -38,8 +38,10 @@ export("prop<-") export("props<-") export(S4_register) export(S7_class) +export(S7_classes) export(S7_data) export(S7_dispatch) +export(S7_generics) export(S7_inherits) export(S7_object) export(as_class) diff --git a/NEWS.md b/NEWS.md index 795ec1fd..de18ab09 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # S7 (development version) +* `S7_classes()` and `S7_generics()` list the S7 classes / generics defined in a given environment (default: the caller's environment); pass `asNamespace("pkg")` to inspect a specific package (#335). * `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). * Internal changes to support R-devel (4.6) (#592, #593, #598, #600). * `S7_error_method_not_found` now has a correct class vector without a duplicate `"error"` entry (@jjjermiah, #604) diff --git a/R/introspect.R b/R/introspect.R new file mode 100644 index 00000000..ce1ee58c --- /dev/null +++ b/R/introspect.R @@ -0,0 +1,36 @@ +#' List S7 classes and generics in an environment +#' +#' @description +#' * `S7_classes()` returns the names of S7 classes defined in `env`. +#' * `S7_generics()` returns the names of S7 generics defined in `env`. +#' +#' @param env An environment. Defaults to the caller's environment. +#' To inspect a package, pass `asNamespace("pkg")`; to inspect the global +#' environment, pass `globalenv()`. +#' @returns A character vector of names. +#' @export +#' @examples +#' # List S7 classes exported by the S7 package itself +#' S7_classes(asNamespace("S7")) +#' S7_generics(asNamespace("S7")) +S7_classes <- function(env = parent.frame()) { + find_objects(env, is_class) +} + +#' @export +#' @rdname S7_classes +S7_generics <- function(env = parent.frame()) { + find_objects(env, is_S7_generic) +} + +find_objects <- function(env, predicate) { + if (isNamespace(env)) { + # Not attached; use exported values + names <- getNamespaceExports(env) + } else { + # Attached or global; use all values + names <- ls(envir = env) + } + + Filter(\(name) predicate(get(name, envir = env, inherits = FALSE)), names) +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 43bce828..b711c472 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -26,10 +26,13 @@ reference: - convert - class_missing - class_any + - super + +- title: Introspection - method - method_explain - - super - S7_class + - S7_classes - title: Packages desc: > diff --git a/man/S7_classes.Rd b/man/S7_classes.Rd new file mode 100644 index 00000000..f8ce8729 --- /dev/null +++ b/man/S7_classes.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/introspect.R +\name{S7_classes} +\alias{S7_classes} +\alias{S7_generics} +\title{List S7 classes and generics in an environment} +\usage{ +S7_classes(env = parent.frame()) + +S7_generics(env = parent.frame()) +} +\arguments{ +\item{env}{An environment. Defaults to the caller's environment.} +} +\value{ +A character vector of names. +} +\description{ +\itemize{ +\item \code{S7_classes()} returns the names of S7 classes defined in \code{env}. +\item \code{S7_generics()} returns the names of S7 generics defined in \code{env}. +} + +To inspect a package, pass \code{asNamespace("pkg")}; to inspect the global +environment, pass \code{globalenv()}. +} +\examples{ +# List S7 classes exported by the S7 package itself +S7_classes(asNamespace("S7")) +S7_generics(asNamespace("S7")) +} diff --git a/tests/testthat/test-introspect.R b/tests/testthat/test-introspect.R new file mode 100644 index 00000000..9a3a244b --- /dev/null +++ b/tests/testthat/test-introspect.R @@ -0,0 +1,29 @@ +test_that("S7_classes() / S7_generics() inspect a single environment", { + # Namespace: restricted to exports + expect_equal(S7_classes(asNamespace("S7")), "S7_object") + expect_equal(S7_generics(asNamespace("S7")), "convert") +}) + +test_that("default `env` is the caller's environment", { + local({ + Foo <- new_class("Foo", package = NULL) + Bar <- new_class("Bar", package = NULL) + my_gen <- new_generic("my_gen", "x") + + expect_setequal(S7_classes(), c("Foo", "Bar")) + expect_setequal(S7_generics(), "my_gen") + }) + + expect_setequal(S7_classes(), character()) + expect_setequal(S7_generics(), character()) +}) + +test_that("find_objects() returns matching names", { + env <- new.env(parent = emptyenv()) + env$Foo <- new_class("Foo", package = NULL) + env$bar <- 1L + env$Baz <- new_class("Baz", package = NULL) + + expect_setequal(find_objects(env, is_class), c("Foo", "Baz")) + expect_setequal(find_objects(env, is.integer), "bar") +}) From cb67829c6dc3a5f0d0eceeb963020e85a8189ad8 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 22 May 2026 15:34:45 -0500 Subject: [PATCH 2/9] Draft `S7_methods()` --- NAMESPACE | 1 + NEWS.md | 2 +- R/introspect.R | 97 +++++++++++++++++++++++++++++ _pkgdown.yml | 1 + man/S7_classes.Rd | 7 +-- man/S7_methods.Rd | 39 ++++++++++++ tests/testthat/_snaps/introspect.md | 18 ++++++ tests/testthat/test-introspect.R | 73 ++++++++++++++++++++++ 8 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 man/S7_methods.Rd create mode 100644 tests/testthat/_snaps/introspect.md diff --git a/NAMESPACE b/NAMESPACE index 77542cc4..b2e2c17c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -43,6 +43,7 @@ export(S7_data) export(S7_dispatch) export(S7_generics) export(S7_inherits) +export(S7_methods) export(S7_object) export(as_class) export(check_is_S7) diff --git a/NEWS.md b/NEWS.md index de18ab09..9c2b67a7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # S7 (development version) -* `S7_classes()` and `S7_generics()` list the S7 classes / generics defined in a given environment (default: the caller's environment); pass `asNamespace("pkg")` to inspect a specific package (#335). +* `S7_classes()`, `S7_generics()`, and `S7_methods()` are new introspection helpers. `S7_classes()` and `S7_generics()` list the S7 classes / generics defined in a given environment; pass `asNamespace("pkg")` to inspect a specific package. `S7_methods()` returns a data frame of methods registered on a generic, or registered for a class across all attached packages (#335). * `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). * Internal changes to support R-devel (4.6) (#592, #593, #598, #600). * `S7_error_method_not_found` now has a correct class vector without a duplicate `"error"` entry (@jjjermiah, #604) diff --git a/R/introspect.R b/R/introspect.R index ce1ee58c..28b7edf1 100644 --- a/R/introspect.R +++ b/R/introspect.R @@ -23,6 +23,103 @@ S7_generics <- function(env = parent.frame()) { find_objects(env, is_S7_generic) } +#' List S7 methods +#' +#' List the methods registered on an S7 `generic`, or the methods registered +#' for a given `class` across all S7 generics defined in attached packages. +#' +#' @param generic An S7 generic. Mutually exclusive with `class`. +#' @param class A class specification (anything accepted by [as_class()]). +#' When supplied, every S7 generic in every attached package is searched +#' for methods with this class in their signature. Mutually exclusive with +#' `generic`. +#' @returns A data frame with one row per matching method and columns: +#' +#' * `generic`: the generic's name. +#' * `signature`: human-readable description of the dispatch signature. +#' * `method`: a string giving the `method()` call that retrieves the +#' method. +#' @export +#' @examples +#' Foo <- new_class("Foo", package = NULL) +#' Bar <- new_class("Bar", package = NULL) +#' my_gen <- new_generic("my_gen", "x") +#' method(my_gen, Foo) <- function(x) "foo" +#' method(my_gen, Bar) <- function(x) "bar" +#' +#' S7_methods(generic = my_gen) +#' S7_methods(class = Foo) +S7_methods <- function(generic = NULL, class = NULL) { + if (is.null(generic) == is.null(class)) { + stop("Must supply exactly one of `generic` or `class`.") + } + + if (!is.null(generic)) { + if (!is_S7_generic(generic)) { + stop("`generic` must be an S7 generic.") + } + generics <- list(generic) + target <- NULL + } else { + target <- class_register(as_class(class)) + generics <- attached_generics() + } + + rows <- lapply(generics, function(g) generic_method_rows(g, target)) + do.call(rbind, rows) +} + +# Per-generic helper: turn the generic's registered methods into a data +# frame, optionally filtering to those whose signature contains `target`. +generic_method_rows <- function(generic, target = NULL) { + ms <- methods(generic) + if (length(ms) == 0) { + return(empty_methods_df()) + } + if (!is.null(target)) { + keep <- vlapply(ms, \(m) { + any(vcapply(m@signature, class_register) == target) + }) + ms <- ms[keep] + } + if (length(ms) == 0) { + return(empty_methods_df()) + } + + data.frame( + generic = rep(generic@name, length(ms)), + signature = vcapply( + ms, + \(m) paste0(vcapply(m@signature, class_desc), collapse = ", ") + ), + method = vcapply(ms, \(m) method_signature(generic, m@signature)) + ) +} + +empty_methods_df <- function() { + data.frame( + generic = character(), + signature = character(), + method = character() + ) +} + +# All S7 generics reachable from attached packages and the global env. +attached_generics <- function() { + envs <- attached_envs() + out <- list() + for (env in envs) { + for (name in S7_generics(env)) { + out[[length(out) + 1L]] <- get0(name, envir = env, inherits = FALSE) + } + } + out +} + +attached_envs <- function() { + lapply(search(), as.environment) +} + find_objects <- function(env, predicate) { if (isNamespace(env)) { # Not attached; use exported values diff --git a/_pkgdown.yml b/_pkgdown.yml index b711c472..cd7970d4 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -33,6 +33,7 @@ reference: - method_explain - S7_class - S7_classes + - S7_methods - title: Packages desc: > diff --git a/man/S7_classes.Rd b/man/S7_classes.Rd index f8ce8729..5f9d1dd7 100644 --- a/man/S7_classes.Rd +++ b/man/S7_classes.Rd @@ -10,7 +10,9 @@ S7_classes(env = parent.frame()) S7_generics(env = parent.frame()) } \arguments{ -\item{env}{An environment. Defaults to the caller's environment.} +\item{env}{An environment. Defaults to the caller's environment. +To inspect a package, pass \code{asNamespace("pkg")}; to inspect the global +environment, pass \code{globalenv()}.} } \value{ A character vector of names. @@ -20,9 +22,6 @@ A character vector of names. \item \code{S7_classes()} returns the names of S7 classes defined in \code{env}. \item \code{S7_generics()} returns the names of S7 generics defined in \code{env}. } - -To inspect a package, pass \code{asNamespace("pkg")}; to inspect the global -environment, pass \code{globalenv()}. } \examples{ # List S7 classes exported by the S7 package itself diff --git a/man/S7_methods.Rd b/man/S7_methods.Rd new file mode 100644 index 00000000..a034c429 --- /dev/null +++ b/man/S7_methods.Rd @@ -0,0 +1,39 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/introspect.R +\name{S7_methods} +\alias{S7_methods} +\title{List S7 methods} +\usage{ +S7_methods(generic = NULL, class = NULL) +} +\arguments{ +\item{generic}{An S7 generic. Mutually exclusive with \code{class}.} + +\item{class}{A class specification (anything accepted by \code{\link[=as_class]{as_class()}}). +When supplied, every S7 generic in every attached package is searched +for methods with this class in their signature. Mutually exclusive with +\code{generic}.} +} +\value{ +A data frame with one row per matching method and columns: +\itemize{ +\item \code{generic}: the generic's name. +\item \code{signature}: human-readable description of the dispatch signature. +\item \code{method}: a string giving the \code{method()} call that retrieves the +method. +} +} +\description{ +List the methods registered on an S7 \code{generic}, or the methods registered +for a given \code{class} across all S7 generics defined in attached packages. +} +\examples{ +Foo <- new_class("Foo", package = NULL) +Bar <- new_class("Bar", package = NULL) +my_gen <- new_generic("my_gen", "x") +method(my_gen, Foo) <- function(x) "foo" +method(my_gen, Bar) <- function(x) "bar" + +S7_methods(generic = my_gen) +S7_methods(class = Foo) +} diff --git a/tests/testthat/_snaps/introspect.md b/tests/testthat/_snaps/introspect.md new file mode 100644 index 00000000..2db68411 --- /dev/null +++ b/tests/testthat/_snaps/introspect.md @@ -0,0 +1,18 @@ +# S7_methods() validates inputs + + Code + S7_methods() + Condition + Error in `S7_methods()`: + ! Must supply exactly one of `generic` or `class`. + Code + S7_methods(generic = new_generic("g", "x"), class = class_integer) + Condition + Error in `S7_methods()`: + ! Must supply exactly one of `generic` or `class`. + Code + S7_methods(generic = "not a generic") + Condition + Error in `S7_methods()`: + ! `generic` must be an S7 generic. + diff --git a/tests/testthat/test-introspect.R b/tests/testthat/test-introspect.R index 9a3a244b..3444184c 100644 --- a/tests/testthat/test-introspect.R +++ b/tests/testthat/test-introspect.R @@ -18,6 +18,79 @@ test_that("default `env` is the caller's environment", { expect_setequal(S7_generics(), character()) }) +test_that("S7_methods(generic) lists registered methods", { + Foo <- new_class("Foo", package = NULL) + Bar <- new_class("Bar", package = NULL) + gen <- new_generic("gen", "x") + method(gen, Foo) <- function(x) "foo" + method(gen, Bar) <- function(x) "bar" + + res <- S7_methods(generic = gen) + expect_s3_class(res, "data.frame") + expect_named(res, c("generic", "signature", "method")) + expect_equal(res$generic, c("gen", "gen")) + expect_setequal(res$signature, c("", "")) + expect_setequal(res$method, c("method(gen, Foo)", "method(gen, Bar)")) +}) + +test_that("S7_methods(generic) handles multi-dispatch", { + Foo <- new_class("Foo", package = NULL) + Bar <- new_class("Bar", package = NULL) + gen <- new_generic("gen", c("x", "y")) + method(gen, list(Foo, Bar)) <- function(x, y) "fb" + + res <- S7_methods(generic = gen) + expect_equal(res$signature, ", ") + expect_equal(res$method, "method(gen, list(Foo, Bar))") +}) + +test_that("S7_methods(generic) returns empty df when no methods", { + gen <- new_generic("gen", "x") + res <- S7_methods(generic = gen) + expect_s3_class(res, "data.frame") + expect_equal(nrow(res), 0) + expect_named(res, c("generic", "signature", "method")) +}) + +test_that("S7_methods(class) scans attached generics", { + Foo <- new_class("Foo", package = NULL) + Bar <- new_class("Bar", package = NULL) + g1 <- new_generic("S7_introspect_g1_xyzzy", "x") + g2 <- new_generic("S7_introspect_g2_xyzzy", "x") + method(g1, Foo) <- function(x) "foo" + method(g2, Bar) <- function(x) "bar" + + assign("S7_introspect_g1_xyzzy", g1, envir = globalenv()) + assign("S7_introspect_g2_xyzzy", g2, envir = globalenv()) + defer(rm( + list = c("S7_introspect_g1_xyzzy", "S7_introspect_g2_xyzzy"), + envir = globalenv() + )) + + res <- S7_methods(class = Foo) + expect_true("S7_introspect_g1_xyzzy" %in% res$generic) + expect_false("S7_introspect_g2_xyzzy" %in% res$generic) +}) + +test_that("S7_methods() validates inputs", { + expect_snapshot(error = TRUE, { + S7_methods() + S7_methods(generic = new_generic("g", "x"), class = class_integer) + S7_methods(generic = "not a generic") + }) +}) + +test_that("the `method` column round-trips via eval(parse(...))", { + Foo <- new_class("Foo", package = NULL) + gen <- new_generic("gen", "x") + method(gen, Foo) <- function(x) "foo result" + + res <- S7_methods(generic = gen) + m <- eval(parse(text = res$method[1])) + expect_s3_class(m, "S7_method") + expect_equal(m(Foo()), "foo result") +}) + test_that("find_objects() returns matching names", { env <- new.env(parent = emptyenv()) env$Foo <- new_class("Foo", package = NULL) From 46442f92153dd096d7bbc095ed16235ca3b34156 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 22 May 2026 15:35:51 -0500 Subject: [PATCH 3/9] News bullet --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 9c2b67a7..d17d67cc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # S7 (development version) -* `S7_classes()`, `S7_generics()`, and `S7_methods()` are new introspection helpers. `S7_classes()` and `S7_generics()` list the S7 classes / generics defined in a given environment; pass `asNamespace("pkg")` to inspect a specific package. `S7_methods()` returns a data frame of methods registered on a generic, or registered for a class across all attached packages (#335). +* `S7_classes()`, `S7_generics()`, and `S7_methods()` are new introspection helpers. `S7_classes()` and `S7_generics()` list the S7 classes / generics defined in a given environment; pass `asNamespace("pkg")` to inspect a specific package (#335). `S7_methods()` returns a data frame of methods registered on a generic, or — analogously to `utils::methods(class = )` — registered for a class across all attached packages (#435). * `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). * Internal changes to support R-devel (4.6) (#592, #593, #598, #600). * `S7_error_method_not_found` now has a correct class vector without a duplicate `"error"` entry (@jjjermiah, #604) From bcfc021c94a043ee52c36ce2c8081209c98ce15a Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 1 Jun 2026 16:26:37 -0400 Subject: [PATCH 4/9] Restrict attached_envs --- R/introspect.R | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/R/introspect.R b/R/introspect.R index 28b7edf1..eec9a494 100644 --- a/R/introspect.R +++ b/R/introspect.R @@ -117,7 +117,11 @@ attached_generics <- function() { } attached_envs <- function() { - lapply(search(), as.environment) + envs <- search() + pkgs <- envs[grepl("^package:", envs)] + pkgs <- setdiff(pkgs, "package:base") + + c(lapply(pkgs, as.environment), globalenv()) } find_objects <- function(env, predicate) { From a56e2e8b632938ba08f2dd723a74e25396ab7c9a Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 1 Jun 2026 16:37:46 -0400 Subject: [PATCH 5/9] Reduce some duplication --- R/introspect.R | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/R/introspect.R b/R/introspect.R index eec9a494..e21b0e4a 100644 --- a/R/introspect.R +++ b/R/introspect.R @@ -106,12 +106,9 @@ empty_methods_df <- function() { # All S7 generics reachable from attached packages and the global env. attached_generics <- function() { - envs <- attached_envs() out <- list() - for (env in envs) { - for (name in S7_generics(env)) { - out[[length(out) + 1L]] <- get0(name, envir = env, inherits = FALSE) - } + for (env in attached_envs()) { + out <- c(out, unname(find_matches(env, is_S7_generic))) } out } @@ -125,6 +122,11 @@ attached_envs <- function() { } find_objects <- function(env, predicate) { + names(find_matches(env, predicate)) +} + +# Named list of objects in `env` satisfying `predicate`. +find_matches <- function(env, predicate) { if (isNamespace(env)) { # Not attached; use exported values names <- getNamespaceExports(env) @@ -133,5 +135,6 @@ find_objects <- function(env, predicate) { names <- ls(envir = env) } - Filter(\(name) predicate(get(name, envir = env, inherits = FALSE)), names) + objs <- mget(names, envir = env, inherits = FALSE) + Filter(predicate, objs) } From 7d22ebdb606f8a231c239279bf79c30f4fa22651 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 1 Jun 2026 16:39:36 -0400 Subject: [PATCH 6/9] Style --- R/introspect.R | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/R/introspect.R b/R/introspect.R index e21b0e4a..95732809 100644 --- a/R/introspect.R +++ b/R/introspect.R @@ -88,10 +88,9 @@ generic_method_rows <- function(generic, target = NULL) { data.frame( generic = rep(generic@name, length(ms)), - signature = vcapply( - ms, - \(m) paste0(vcapply(m@signature, class_desc), collapse = ", ") - ), + signature = vcapply(ms, function(m) { + paste0(vcapply(m@signature, class_desc), collapse = ", ") + }), method = vcapply(ms, \(m) method_signature(generic, m@signature)) ) } From 39ba25d503d74a9dca79f0fe1ee83bd746904033 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Tue, 2 Jun 2026 11:36:31 -0400 Subject: [PATCH 7/9] More polishing --- R/introspect.R | 68 ++++++++++++++++------------- _pkgdown.yml | 2 +- man/S7_classes.Rd | 2 +- man/S7_methods.Rd | 8 ++-- tests/testthat/_snaps/introspect.md | 10 ----- tests/testthat/test-introspect.R | 19 ++++++-- 6 files changed, 59 insertions(+), 50 deletions(-) diff --git a/R/introspect.R b/R/introspect.R index 95732809..fa0d3ee4 100644 --- a/R/introspect.R +++ b/R/introspect.R @@ -1,4 +1,4 @@ -#' List S7 classes and generics in an environment +#' Find S7 classes and generics in an environment #' #' @description #' * `S7_classes()` returns the names of S7 classes defined in `env`. @@ -28,14 +28,16 @@ S7_generics <- function(env = parent.frame()) { #' List the methods registered on an S7 `generic`, or the methods registered #' for a given `class` across all S7 generics defined in attached packages. #' -#' @param generic An S7 generic. Mutually exclusive with `class`. +#' @param generic An S7 generic. #' @param class A class specification (anything accepted by [as_class()]). #' When supplied, every S7 generic in every attached package is searched -#' for methods with this class in their signature. Mutually exclusive with -#' `generic`. +#' for methods with this class in their signature. #' @returns A data frame with one row per matching method and columns: #' #' * `generic`: the generic's name. +#' * `package`: the package the generic is defined in, or `NA` for generics +#' found in the global environment (or when `generic` is supplied +#' directly). #' * `signature`: human-readable description of the dispatch signature. #' * `method`: a string giving the `method()` call that retrieves the #' method. @@ -50,44 +52,44 @@ S7_generics <- function(env = parent.frame()) { #' S7_methods(generic = my_gen) #' S7_methods(class = Foo) S7_methods <- function(generic = NULL, class = NULL) { - if (is.null(generic) == is.null(class)) { - stop("Must supply exactly one of `generic` or `class`.") - } - if (!is.null(generic)) { if (!is_S7_generic(generic)) { stop("`generic` must be an S7 generic.") } - generics <- list(generic) - target <- NULL + generics <- list(list(generic = generic, package = NA_character_)) } else { - target <- class_register(as_class(class)) generics <- attached_generics() } - rows <- lapply(generics, function(g) generic_method_rows(g, target)) + if (!is.null(class)) { + target <- class_register(as_class(class)) + } else { + target <- NULL + } + + rows <- lapply(generics, function(g) { + generic_method_rows(g$generic, g$package, target) + }) do.call(rbind, rows) } # Per-generic helper: turn the generic's registered methods into a data # frame, optionally filtering to those whose signature contains `target`. -generic_method_rows <- function(generic, target = NULL) { +generic_method_rows <- function( + generic, + package = NA_character_, + target = NULL +) { ms <- methods(generic) - if (length(ms) == 0) { - return(empty_methods_df()) - } if (!is.null(target)) { - keep <- vlapply(ms, \(m) { + ms <- Filter(x = ms, function(m) { any(vcapply(m@signature, class_register) == target) }) - ms <- ms[keep] - } - if (length(ms) == 0) { - return(empty_methods_df()) } data.frame( generic = rep(generic@name, length(ms)), + package = rep(package, length(ms)), signature = vcapply(ms, function(m) { paste0(vcapply(m@signature, class_desc), collapse = ", ") }), @@ -95,23 +97,27 @@ generic_method_rows <- function(generic, target = NULL) { ) } -empty_methods_df <- function() { - data.frame( - generic = character(), - signature = character(), - method = character() - ) -} - -# All S7 generics reachable from attached packages and the global env. +# All S7 generics reachable from attached packages and the global env, +# each tagged with the package it was found in (`NA` for the global env). attached_generics <- function() { out <- list() for (env in attached_envs()) { - out <- c(out, unname(find_matches(env, is_S7_generic))) + package <- env_package(env) + for (generic in unname(find_matches(env, is_S7_generic))) { + out[[length(out) + 1L]] <- list(generic = generic, package = package) + } } out } +env_package <- function(env) { + if (identical(env, globalenv())) { + NA_character_ + } else { + sub("^package:", "", environmentName(env)) + } +} + attached_envs <- function() { envs <- search() pkgs <- envs[grepl("^package:", envs)] diff --git a/_pkgdown.yml b/_pkgdown.yml index ea0af336..cd3c38fd 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -33,9 +33,9 @@ reference: - method - method_explain - S7_class + - S7_class_desc - S7_classes - S7_methods - - S7_class_desc - title: Packages desc: > diff --git a/man/S7_classes.Rd b/man/S7_classes.Rd index 5f9d1dd7..facfb7fc 100644 --- a/man/S7_classes.Rd +++ b/man/S7_classes.Rd @@ -3,7 +3,7 @@ \name{S7_classes} \alias{S7_classes} \alias{S7_generics} -\title{List S7 classes and generics in an environment} +\title{Find S7 classes and generics in an environment} \usage{ S7_classes(env = parent.frame()) diff --git a/man/S7_methods.Rd b/man/S7_methods.Rd index a034c429..2c2ce6d5 100644 --- a/man/S7_methods.Rd +++ b/man/S7_methods.Rd @@ -7,17 +7,19 @@ S7_methods(generic = NULL, class = NULL) } \arguments{ -\item{generic}{An S7 generic. Mutually exclusive with \code{class}.} +\item{generic}{An S7 generic.} \item{class}{A class specification (anything accepted by \code{\link[=as_class]{as_class()}}). When supplied, every S7 generic in every attached package is searched -for methods with this class in their signature. Mutually exclusive with -\code{generic}.} +for methods with this class in their signature.} } \value{ A data frame with one row per matching method and columns: \itemize{ \item \code{generic}: the generic's name. +\item \code{package}: the package the generic is defined in, or \code{NA} for generics +found in the global environment (or when \code{generic} is supplied +directly). \item \code{signature}: human-readable description of the dispatch signature. \item \code{method}: a string giving the \code{method()} call that retrieves the method. diff --git a/tests/testthat/_snaps/introspect.md b/tests/testthat/_snaps/introspect.md index 2db68411..9067ba9d 100644 --- a/tests/testthat/_snaps/introspect.md +++ b/tests/testthat/_snaps/introspect.md @@ -1,15 +1,5 @@ # S7_methods() validates inputs - Code - S7_methods() - Condition - Error in `S7_methods()`: - ! Must supply exactly one of `generic` or `class`. - Code - S7_methods(generic = new_generic("g", "x"), class = class_integer) - Condition - Error in `S7_methods()`: - ! Must supply exactly one of `generic` or `class`. Code S7_methods(generic = "not a generic") Condition diff --git a/tests/testthat/test-introspect.R b/tests/testthat/test-introspect.R index 3444184c..901211bf 100644 --- a/tests/testthat/test-introspect.R +++ b/tests/testthat/test-introspect.R @@ -27,7 +27,7 @@ test_that("S7_methods(generic) lists registered methods", { res <- S7_methods(generic = gen) expect_s3_class(res, "data.frame") - expect_named(res, c("generic", "signature", "method")) + expect_named(res, c("generic", "package", "signature", "method")) expect_equal(res$generic, c("gen", "gen")) expect_setequal(res$signature, c("", "")) expect_setequal(res$method, c("method(gen, Foo)", "method(gen, Bar)")) @@ -49,7 +49,7 @@ test_that("S7_methods(generic) returns empty df when no methods", { res <- S7_methods(generic = gen) expect_s3_class(res, "data.frame") expect_equal(nrow(res), 0) - expect_named(res, c("generic", "signature", "method")) + expect_named(res, c("generic", "package", "signature", "method")) }) test_that("S7_methods(class) scans attached generics", { @@ -70,12 +70,23 @@ test_that("S7_methods(class) scans attached generics", { res <- S7_methods(class = Foo) expect_true("S7_introspect_g1_xyzzy" %in% res$generic) expect_false("S7_introspect_g2_xyzzy" %in% res$generic) + expect_equal( + res$package[res$generic == "S7_introspect_g1_xyzzy"], + NA_character_ + ) +}) + +test_that("S7_methods() reports the generic's package", { + Foo <- new_class("Foo", package = NULL) + gen <- new_generic("gen", "x") + method(gen, Foo) <- function(x) "foo" + + res <- S7_methods(generic = gen) + expect_equal(res$package, NA_character_) }) test_that("S7_methods() validates inputs", { expect_snapshot(error = TRUE, { - S7_methods() - S7_methods(generic = new_generic("g", "x"), class = class_integer) S7_methods(generic = "not a generic") }) }) From 10dc991eb7b167a6ffcf790657ed94e40534a357 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Wed, 3 Jun 2026 15:29:28 -0400 Subject: [PATCH 8/9] Tweak columns --- NAMESPACE | 4 ++ R/introspect.R | 48 ++++++++++++++---------- R/method-register.R | 11 ++++++ man/S7_methods.Rd | 5 +-- tests/testthat/_snaps/introspect.md | 9 +++++ tests/testthat/_snaps/method-register.md | 7 ++++ tests/testthat/test-introspect.R | 44 +++++++++++++--------- tests/testthat/test-method-register.R | 8 ++++ 8 files changed, 97 insertions(+), 39 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 080412ed..483678c9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -11,6 +11,8 @@ S3method("|",S7_class) S3method(Ops,S7_object) S3method(Ops,S7_super) S3method(c,S7_class) +S3method(format,S7_signature) +S3method(format,S7_signature_list) S3method(print,S7_S3_class) S3method(print,S7_any) S3method(print,S7_base_class) @@ -21,6 +23,8 @@ S3method(print,S7_method) S3method(print,S7_missing) S3method(print,S7_object) S3method(print,S7_property) +S3method(print,S7_signature) +S3method(print,S7_signature_list) S3method(print,S7_super) S3method(print,S7_union) S3method(str,S7_S3_class) diff --git a/R/introspect.R b/R/introspect.R index fa0d3ee4..243942a2 100644 --- a/R/introspect.R +++ b/R/introspect.R @@ -38,9 +38,8 @@ S7_generics <- function(env = parent.frame()) { #' * `package`: the package the generic is defined in, or `NA` for generics #' found in the global environment (or when `generic` is supplied #' directly). -#' * `signature`: human-readable description of the dispatch signature. -#' * `method`: a string giving the `method()` call that retrieves the -#' method. +#' * `signature`: a list column of `S7_signature` objects describing the +#' dispatch signature. `format()` them for a human-readable description. #' @export #' @examples #' Foo <- new_class("Foo", package = NULL) @@ -70,33 +69,44 @@ S7_methods <- function(generic = NULL, class = NULL) { rows <- lapply(generics, function(g) { generic_method_rows(g$generic, g$package, target) }) - do.call(rbind, rows) + out <- do.call(rbind, rows) + out$signature <- new_signature_list(out$signature) + out } -# Per-generic helper: turn the generic's registered methods into a data -# frame, optionally filtering to those whose signature contains `target`. -generic_method_rows <- function( - generic, - package = NA_character_, - target = NULL -) { +generic_method_rows <- function(generic, package, class) { ms <- methods(generic) - if (!is.null(target)) { - ms <- Filter(x = ms, function(m) { - any(vcapply(m@signature, class_register) == target) - }) + if (!is.null(class)) { + has_class <- function(m) any(vcapply(m@signature, class_register) == class) + ms <- Filter(has_class, ms) } data.frame( generic = rep(generic@name, length(ms)), package = rep(package, length(ms)), - signature = vcapply(ms, function(m) { - paste0(vcapply(m@signature, class_desc), collapse = ", ") - }), - method = vcapply(ms, \(m) method_signature(generic, m@signature)) + signature = I(lapply(ms, \(m) new_signature(m@signature))) ) } +# A list column of S7_signatures, used by S7_methods(). Needs its own class +# so that print.data.frame() formats it per-element: data frames format whole +# columns, so the scalar format.S7_signature() method is never reached. +new_signature_list <- function(x) { + class(x) <- "S7_signature_list" + x +} + +#' @export +format.S7_signature_list <- function(x, ...) { + vcapply(unclass(x), format) +} + +#' @export +print.S7_signature_list <- function(x, ...) { + print(format(x), quote = FALSE) + invisible(x) +} + # All S7 generics reachable from attached packages and the global env, # each tagged with the package it was found in (`NA` for the global env). attached_generics <- function() { diff --git a/R/method-register.R b/R/method-register.R index 663013e2..b5a43729 100644 --- a/R/method-register.R +++ b/R/method-register.R @@ -290,6 +290,17 @@ new_signature <- function(x) { x } +#' @export +format.S7_signature <- function(x, ...) { + paste0(vcapply(unclass(x), class_desc), collapse = ", ") +} + +#' @export +print.S7_signature <- function(x, ...) { + cat(format(x), "\n", sep = "") + invisible(x) +} + check_method <- function( method, generic, diff --git a/man/S7_methods.Rd b/man/S7_methods.Rd index 2c2ce6d5..810b12b0 100644 --- a/man/S7_methods.Rd +++ b/man/S7_methods.Rd @@ -20,9 +20,8 @@ A data frame with one row per matching method and columns: \item \code{package}: the package the generic is defined in, or \code{NA} for generics found in the global environment (or when \code{generic} is supplied directly). -\item \code{signature}: human-readable description of the dispatch signature. -\item \code{method}: a string giving the \code{method()} call that retrieves the -method. +\item \code{signature}: a list column of \code{S7_signature} objects describing the +dispatch signature. \code{format()} them for a human-readable description. } } \description{ diff --git a/tests/testthat/_snaps/introspect.md b/tests/testthat/_snaps/introspect.md index 9067ba9d..40e3b9ad 100644 --- a/tests/testthat/_snaps/introspect.md +++ b/tests/testthat/_snaps/introspect.md @@ -1,3 +1,12 @@ +# S7_methods() prints the signature column readably + + Code + print(S7_methods(generic = gen)) + Output + generic package signature + 1 gen + 2 gen + # S7_methods() validates inputs Code diff --git a/tests/testthat/_snaps/method-register.md b/tests/testthat/_snaps/method-register.md index 2648d432..39fee0c6 100644 --- a/tests/testthat/_snaps/method-register.md +++ b/tests/testthat/_snaps/method-register.md @@ -105,6 +105,13 @@ Error: ! `signature` must be length 2. +# S7_signature has format and print methods + + Code + print(sig) + Output + , + # check_method complains if the functions are not compatible Code diff --git a/tests/testthat/test-introspect.R b/tests/testthat/test-introspect.R index 901211bf..d3f756d3 100644 --- a/tests/testthat/test-introspect.R +++ b/tests/testthat/test-introspect.R @@ -27,10 +27,32 @@ test_that("S7_methods(generic) lists registered methods", { res <- S7_methods(generic = gen) expect_s3_class(res, "data.frame") - expect_named(res, c("generic", "package", "signature", "method")) + expect_named(res, c("generic", "package", "signature")) expect_equal(res$generic, c("gen", "gen")) - expect_setequal(res$signature, c("", "")) - expect_setequal(res$method, c("method(gen, Foo)", "method(gen, Bar)")) + expect_setequal(vcapply(res$signature, format), c("", "")) +}) + +test_that("S7_methods() prints the signature column readably", { + Foo <- new_class("Foo", package = NULL) + Bar <- new_class("Bar", package = NULL) + gen <- new_generic("gen", "x") + method(gen, Foo) <- function(x) "foo" + method(gen, Bar) <- function(x) "bar" + + expect_snapshot(print(S7_methods(generic = gen))) +}) + +test_that("S7_signature_list formats per element", { + foo <- new_generic("foo", c("x", "y")) + sigs <- new_signature_list(list( + as_signature(list(class_integer, class_character), foo), + as_signature(list(class_double, class_logical), foo) + )) + + expect_equal( + format(sigs), + c(", ", ", ") + ) }) test_that("S7_methods(generic) handles multi-dispatch", { @@ -40,8 +62,7 @@ test_that("S7_methods(generic) handles multi-dispatch", { method(gen, list(Foo, Bar)) <- function(x, y) "fb" res <- S7_methods(generic = gen) - expect_equal(res$signature, ", ") - expect_equal(res$method, "method(gen, list(Foo, Bar))") + expect_equal(vcapply(res$signature, format), ", ") }) test_that("S7_methods(generic) returns empty df when no methods", { @@ -49,7 +70,7 @@ test_that("S7_methods(generic) returns empty df when no methods", { res <- S7_methods(generic = gen) expect_s3_class(res, "data.frame") expect_equal(nrow(res), 0) - expect_named(res, c("generic", "package", "signature", "method")) + expect_named(res, c("generic", "package", "signature")) }) test_that("S7_methods(class) scans attached generics", { @@ -91,17 +112,6 @@ test_that("S7_methods() validates inputs", { }) }) -test_that("the `method` column round-trips via eval(parse(...))", { - Foo <- new_class("Foo", package = NULL) - gen <- new_generic("gen", "x") - method(gen, Foo) <- function(x) "foo result" - - res <- S7_methods(generic = gen) - m <- eval(parse(text = res$method[1])) - expect_s3_class(m, "S7_method") - expect_equal(m(Foo()), "foo result") -}) - test_that("find_objects() returns matching names", { env <- new.env(parent = emptyenv()) env$Foo <- new_class("Foo", package = NULL) diff --git a/tests/testthat/test-method-register.R b/tests/testthat/test-method-register.R index bd3fcc37..0500774a 100644 --- a/tests/testthat/test-method-register.R +++ b/tests/testthat/test-method-register.R @@ -296,6 +296,14 @@ describe("as_signature()", { }) }) +test_that("S7_signature has format and print methods", { + foo <- new_generic("foo", c("x", "y")) + sig <- as_signature(list(class_integer, class_character), foo) + + expect_equal(format(sig), ", ") + expect_snapshot(print(sig)) +}) + test_that("check_method returns TRUE if the functions are compatible", { foo <- new_generic("foo", "x", function(x, ...) S7_dispatch()) expect_true(check_method(function(x, ...) x, foo)) From 3c990ba1d221e00f354763b0bb5f37d8dc988798 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 15 Jun 2026 07:59:19 -0500 Subject: [PATCH 9/9] Polish news --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 0e9d64f5..3eff5e79 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,7 +29,7 @@ * `prop()` no longer leaves an object in a broken state when a custom getter signals an error (#520, #640, #638). * `prop<-()` no longer fails when assigning a call or symbol to a property (#511, #633, #638). * New `prop_info()` returns a data frame summarising the properties of an S7 object or class, with one row per property and columns for name, default, class, getter, setter, and validator (#551). -* New `S7_classes()`, `S7_generics()`, and `S7_methods()` are new introspection helpers. `S7_classes()` and `S7_generics()` list the S7 classes / generics defined in a given environment; pass `asNamespace("pkg")` to inspect a specific package (#335). `S7_methods()` returns a data frame of methods registered on a generic, or — analogously to `utils::methods(class = )` — registered for a class across all attached packages (#435). +* New `S7_classes()`, `S7_generics()`, and `S7_methods()` introspection helpers. `S7_classes()` and `S7_generics()` list the S7 classes / generics defined in a given environment/package (#335). `S7_methods()` list methods methods registered on a generic or all methods associated with a class (across generics in attached packages) (#435). * `S7_dispatch()` now gives a clear error when called from a function that is not an S7 generic, e.g. `unclass(generic)()`, instead of failing with a confusing message (#684). * `S7_class()` now returns a class specification for any R object, not just S7 objects. It returns the matching `class_*` for base types, a `new_S3_class()` wrapper for S3 objects, and the S4 class for S4 objects, so the result can be passed directly to `method()` or other S7 dispatch helpers (#559). * `S7_class_desc()` is a new exported helper that formats a class specification as a short human-readable string (#594).