Skip to content

Commit bf78cd3

Browse files
authored
Class/generic/methods introspection (#620)
* Implement `S7_classes()` and `S7_generics()`. Fixes #335 * Implement `S7_methods()`. Fixes #435.
1 parent d9e8faa commit bf78cd3

11 files changed

Lines changed: 401 additions & 0 deletions

File tree

NAMESPACE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ S3method("|",S7_class)
1111
S3method(Ops,S7_object)
1212
S3method(Ops,S7_super)
1313
S3method(c,S7_class)
14+
S3method(format,S7_signature)
15+
S3method(format,S7_signature_list)
1416
S3method(print,S7_S3_class)
1517
S3method(print,S7_any)
1618
S3method(print,S7_base_class)
@@ -21,6 +23,8 @@ S3method(print,S7_method)
2123
S3method(print,S7_missing)
2224
S3method(print,S7_object)
2325
S3method(print,S7_property)
26+
S3method(print,S7_signature)
27+
S3method(print,S7_signature_list)
2428
S3method(print,S7_super)
2529
S3method(print,S7_union)
2630
S3method(str,S7_S3_class)
@@ -40,9 +44,12 @@ export("props<-")
4044
export(S4_register)
4145
export(S7_class)
4246
export(S7_class_desc)
47+
export(S7_classes)
4348
export(S7_data)
4449
export(S7_dispatch)
50+
export(S7_generics)
4551
export(S7_inherits)
52+
export(S7_methods)
4653
export(S7_object)
4754
export(S7_on_build)
4855
export(S7_on_load)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* `prop()` no longer leaves an object in a broken state when a custom getter signals an error (#520, #640, #638).
3232
* `prop<-()` no longer fails when assigning a call or symbol to a property (#511, #633, #638).
3333
* 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).
34+
* 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).
3435
* `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).
3536
* `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).
3637
* `S7_class_desc()` is a new exported helper that formats a class specification as a short human-readable string (#594).

R/introspect.R

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#' Find S7 classes and generics in an environment
2+
#'
3+
#' @description
4+
#' * `S7_classes()` returns the names of S7 classes defined in `env`.
5+
#' * `S7_generics()` returns the names of S7 generics defined in `env`.
6+
#'
7+
#' @param env An environment. Defaults to the caller's environment.
8+
#' To inspect a package, pass `asNamespace("pkg")`; to inspect the global
9+
#' environment, pass `globalenv()`.
10+
#' @returns A character vector of names.
11+
#' @export
12+
#' @examples
13+
#' # List S7 classes exported by the S7 package itself
14+
#' S7_classes(asNamespace("S7"))
15+
#' S7_generics(asNamespace("S7"))
16+
S7_classes <- function(env = parent.frame()) {
17+
find_objects(env, is_class)
18+
}
19+
20+
#' @export
21+
#' @rdname S7_classes
22+
S7_generics <- function(env = parent.frame()) {
23+
find_objects(env, is_S7_generic)
24+
}
25+
26+
#' List S7 methods
27+
#'
28+
#' List the methods registered on an S7 `generic`, or the methods registered
29+
#' for a given `class` across all S7 generics defined in attached packages.
30+
#'
31+
#' @param generic An S7 generic.
32+
#' @param class A class specification (anything accepted by [as_class()]).
33+
#' When supplied, every S7 generic in every attached package is searched
34+
#' for methods with this class in their signature.
35+
#' @returns A data frame with one row per matching method and columns:
36+
#'
37+
#' * `generic`: the generic's name.
38+
#' * `package`: the package the generic is defined in, or `NA` for generics
39+
#' found in the global environment (or when `generic` is supplied
40+
#' directly).
41+
#' * `signature`: a list column of `S7_signature` objects describing the
42+
#' dispatch signature. `format()` them for a human-readable description.
43+
#' @export
44+
#' @examples
45+
#' Foo <- new_class("Foo", package = NULL)
46+
#' Bar <- new_class("Bar", package = NULL)
47+
#' my_gen <- new_generic("my_gen", "x")
48+
#' method(my_gen, Foo) <- function(x) "foo"
49+
#' method(my_gen, Bar) <- function(x) "bar"
50+
#'
51+
#' S7_methods(generic = my_gen)
52+
#' S7_methods(class = Foo)
53+
S7_methods <- function(generic = NULL, class = NULL) {
54+
if (!is.null(generic)) {
55+
if (!is_S7_generic(generic)) {
56+
stop("`generic` must be an S7 generic.")
57+
}
58+
generics <- list(list(generic = generic, package = NA_character_))
59+
} else {
60+
generics <- attached_generics()
61+
}
62+
63+
if (!is.null(class)) {
64+
target <- class_register(as_class(class))
65+
} else {
66+
target <- NULL
67+
}
68+
69+
rows <- lapply(generics, function(g) {
70+
generic_method_rows(g$generic, g$package, target)
71+
})
72+
out <- do.call(rbind, rows)
73+
out$signature <- new_signature_list(out$signature)
74+
out
75+
}
76+
77+
generic_method_rows <- function(generic, package, class) {
78+
ms <- methods(generic)
79+
if (!is.null(class)) {
80+
has_class <- function(m) any(vcapply(m@signature, class_register) == class)
81+
ms <- Filter(has_class, ms)
82+
}
83+
84+
data.frame(
85+
generic = rep(generic@name, length(ms)),
86+
package = rep(package, length(ms)),
87+
signature = I(lapply(ms, \(m) new_signature(m@signature)))
88+
)
89+
}
90+
91+
# A list column of S7_signatures, used by S7_methods(). Needs its own class
92+
# so that print.data.frame() formats it per-element: data frames format whole
93+
# columns, so the scalar format.S7_signature() method is never reached.
94+
new_signature_list <- function(x) {
95+
class(x) <- "S7_signature_list"
96+
x
97+
}
98+
99+
#' @export
100+
format.S7_signature_list <- function(x, ...) {
101+
vcapply(unclass(x), format)
102+
}
103+
104+
#' @export
105+
print.S7_signature_list <- function(x, ...) {
106+
print(format(x), quote = FALSE)
107+
invisible(x)
108+
}
109+
110+
# All S7 generics reachable from attached packages and the global env,
111+
# each tagged with the package it was found in (`NA` for the global env).
112+
attached_generics <- function() {
113+
out <- list()
114+
for (env in attached_envs()) {
115+
package <- env_package(env)
116+
for (generic in unname(find_matches(env, is_S7_generic))) {
117+
out[[length(out) + 1L]] <- list(generic = generic, package = package)
118+
}
119+
}
120+
out
121+
}
122+
123+
env_package <- function(env) {
124+
if (identical(env, globalenv())) {
125+
NA_character_
126+
} else {
127+
sub("^package:", "", environmentName(env))
128+
}
129+
}
130+
131+
attached_envs <- function() {
132+
envs <- search()
133+
pkgs <- envs[grepl("^package:", envs)]
134+
pkgs <- setdiff(pkgs, "package:base")
135+
136+
c(lapply(pkgs, as.environment), globalenv())
137+
}
138+
139+
find_objects <- function(env, predicate) {
140+
names(find_matches(env, predicate))
141+
}
142+
143+
# Named list of objects in `env` satisfying `predicate`.
144+
find_matches <- function(env, predicate) {
145+
if (isNamespace(env)) {
146+
# Not attached; use exported values
147+
names <- getNamespaceExports(env)
148+
} else {
149+
# Attached or global; use all values
150+
names <- ls(envir = env)
151+
}
152+
153+
objs <- mget(names, envir = env, inherits = FALSE)
154+
Filter(predicate, objs)
155+
}

R/method-register.R

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,17 @@ new_signature <- function(x) {
239239
x
240240
}
241241

242+
#' @export
243+
format.S7_signature <- function(x, ...) {
244+
paste0(vcapply(unclass(x), class_desc), collapse = ", ")
245+
}
246+
247+
#' @export
248+
print.S7_signature <- function(x, ...) {
249+
cat(format(x), "\n", sep = "")
250+
invisible(x)
251+
}
252+
242253
check_method <- function(
243254
method,
244255
generic,

_pkgdown.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ reference:
3535
- method_explain
3636
- S7_class
3737
- S7_class_desc
38+
- S7_classes
39+
- S7_methods
3840

3941
- title: Packages
4042
desc: >

man/S7_classes.Rd

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/S7_methods.Rd

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# S7_methods() prints the signature column readably
2+
3+
Code
4+
print(S7_methods(generic = gen))
5+
Output
6+
generic package signature
7+
1 gen <NA> <Foo>
8+
2 gen <NA> <Bar>
9+
10+
# S7_methods() validates inputs
11+
12+
Code
13+
S7_methods(generic = "not a generic")
14+
Condition
15+
Error in `S7_methods()`:
16+
! `generic` must be an S7 generic.
17+

tests/testthat/_snaps/method-register.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@
6969
Error:
7070
! `signature` must be length 2.
7171

72+
# S7_signature has format and print methods
73+
74+
Code
75+
print(sig)
76+
Output
77+
<integer>, <character>
78+
7279
# check_method complains if the functions are not compatible
7380

7481
Code

0 commit comments

Comments
 (0)