Skip to content

Commit 52512eb

Browse files
authored
Make it possible to unregister a method (#639)
I'm now more sceptical about this approach because S3 doesn't provide any way to unregister a method, and S4's `removeMethod()` explicitly advises against using it. Fixes #613
1 parent bd60b23 commit 52512eb

8 files changed

Lines changed: 210 additions & 3 deletions

File tree

NEWS.md

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

3+
* `method<-` now accepts `NULL` to unregister an existing method, e.g. `method(foo, class_character) <- NULL` (#613).
4+
* `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).
35
* Internal changes to support R-devel (4.6) (#592, #593, #598, #600).
46
* Method dispatch on `class_missing` now correctly handles missing arguments forwarded through a wrapper functions (#595).
57
* `S7_error_method_not_found` now has a correct class vector without a duplicate `"error"` entry (@jjjermiah, #604).

R/external-generic.R

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,19 @@ external_methods_add <- function(package, generic, signature, method) {
155155
invisible()
156156
}
157157

158+
external_methods_remove <- function(package, generic, signature) {
159+
tbl <- S7_methods_table(package)
160+
if (length(tbl) == 0) {
161+
return(invisible())
162+
}
163+
164+
keep <- !vlapply(tbl, function(x) {
165+
identical(x$generic, generic) && identical(x$signature, signature)
166+
})
167+
S7_methods_table(package) <- tbl[keep]
168+
invisible()
169+
}
170+
158171
# Store external methods in an attribute of the S3 methods table since
159172
# this mutable object is present in all packages.
160173

R/generic.R

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,22 @@ generic_add_method <- function(generic, signature, method) {
228228
}
229229
}
230230
}
231+
232+
generic_remove_method <- function(generic, signature) {
233+
p_tbl <- generic@methods
234+
chr_signature <- vcapply(signature, class_register)
235+
236+
for (i in seq_along(chr_signature)) {
237+
class_name <- chr_signature[[i]]
238+
if (i != length(chr_signature)) {
239+
tbl <- p_tbl[[class_name]]
240+
if (is.null(tbl)) {
241+
return(invisible())
242+
}
243+
p_tbl <- tbl
244+
} else if (exists(class_name, envir = p_tbl, inherits = FALSE)) {
245+
rm(list = class_name, envir = p_tbl)
246+
}
247+
}
248+
invisible()
249+
}

R/method-register.R

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
#'
4040
#' The same rules apply to S4 generics as S7 generics.
4141
#' @param value A function that implements the generic specification for the
42-
#' given `signature`.
42+
#' given `signature`, or `NULL` to unregister an existing method.
4343
#' @returns The `generic`, invisibly.
4444
#' @export
4545
#' @examples
@@ -54,8 +54,15 @@
5454
#'
5555
#' # Using a generic calls the methods automatically
5656
#' bizarro(head(mtcars))
57+
#'
58+
#' # Unregister a method by assigning `NULL`
59+
#' method(bizarro, class_numeric) <- NULL
5760
`method<-` <- function(generic, signature, value) {
58-
register_method(generic, signature, value, env = parent.frame())
61+
if (is.null(value)) {
62+
unregister_method(generic, signature, env = parent.frame())
63+
} else {
64+
register_method(generic, signature, value, env = parent.frame())
65+
}
5966
invisible(generic)
6067
}
6168

@@ -96,6 +103,38 @@ register_method <- function(
96103
invisible(generic)
97104
}
98105

106+
unregister_method <- function(
107+
generic,
108+
signature,
109+
env = parent.frame(),
110+
package = packageName(env)
111+
) {
112+
generic <- as_generic(generic)
113+
signature <- as_signature(signature, generic)
114+
115+
if (is_external_generic(generic) && isNamespaceLoaded(generic$package)) {
116+
generic <- as_generic(getFromNamespace(generic$name, generic$package))
117+
}
118+
119+
# Unregister in current session
120+
if (is_S7_generic(generic)) {
121+
unregister_S7_method(generic, signature)
122+
} else if (is_S3_generic(generic)) {
123+
stop("Can't unregister methods for S3 generics", call. = FALSE)
124+
} else if (is_S4_generic(generic)) {
125+
stop("Can't unregister methods for S4 generics", call. = FALSE)
126+
}
127+
128+
# If we're inside a package, also remove from the deferred external
129+
# methods table so the method isn't re-registered on package load.
130+
if (!is.null(package) && !is_local_generic(generic, package)) {
131+
generic <- as_external_generic(generic)
132+
external_methods_remove(package, generic, signature)
133+
}
134+
135+
invisible(generic)
136+
}
137+
99138
register_S3_method <- function(
100139
generic,
101140
signature,
@@ -134,6 +173,14 @@ register_S7_method <- function(generic, signature, method) {
134173
invisible()
135174
}
136175

176+
unregister_S7_method <- function(generic, signature) {
177+
signatures <- flatten_signature(signature)
178+
for (signature in signatures) {
179+
generic_remove_method(generic, signature)
180+
}
181+
invisible()
182+
}
183+
137184
flatten_signature <- function(signature) {
138185
# Unpack unions
139186
sig_is_union <- vlapply(signature, is_union)
@@ -288,6 +335,7 @@ register_S4_method <- function(
288335
S4_signature <- lapply(signature, S4_class, S4_env = S4_env)
289336
methods::setMethod(generic, S4_signature, method, where = S4_env)
290337
}
338+
291339
S4_class <- function(x, S4_env) {
292340
switch(
293341
class_type(x),

man/method-set.Rd

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/method-register.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,48 @@
3737
* An S4 class object
3838
* A base class
3939

40+
# method unregistration / removes S7 method via NULL assignment
41+
42+
Code
43+
foo("x")
44+
Condition
45+
Error:
46+
! Can't find method for `foo(<character>)`.
47+
48+
# method unregistration / removes method with multi-dispatch signature
49+
50+
Code
51+
foo(A(), B())
52+
Condition
53+
Error:
54+
! Can't find method for generic `foo(x, y)`:
55+
- x: <S7::A>
56+
- y: <S7::B>
57+
58+
# method unregistration / errors when unregistering from an S3 generic
59+
60+
Code
61+
method(sum, foo) <- NULL
62+
Condition
63+
Error:
64+
! Can't unregister methods for S3 generics
65+
66+
---
67+
68+
Code
69+
method(base_sum, foo) <- NULL
70+
Condition
71+
Error:
72+
! Can't unregister methods for S3 generics
73+
74+
# method unregistration / errors when unregistering from an S4 generic
75+
76+
Code
77+
method(removeS4, S4foo) <- NULL
78+
Condition
79+
Error:
80+
! Can't unregister methods for S4 generics
81+
4082
# as_signature() / accepts a length-1 list for single dispatch (#555)
4183

4284
Code

tests/testthat/test-external-generic.R

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@ test_that("can get and append methods", {
1818
)
1919
})
2020

21+
test_that("can remove methods", {
22+
external_methods_reset("S7")
23+
on.exit(external_methods_reset("S7"), add = TRUE)
24+
25+
bar <- new_external_generic("foo", "bar", "x")
26+
baz <- new_external_generic("foo", "baz", "x")
27+
external_methods_add("S7", bar, list("A"), function() "a")
28+
external_methods_add("S7", baz, list("B"), function() "b")
29+
expect_length(S7_methods_table("S7"), 2)
30+
31+
external_methods_remove("S7", bar, list("A"))
32+
expect_length(S7_methods_table("S7"), 1)
33+
expect_equal(S7_methods_table("S7")[[1]]$generic, baz)
34+
35+
# No-op when entry doesn't exist
36+
external_methods_remove("S7", bar, list("A"))
37+
expect_length(S7_methods_table("S7"), 1)
38+
})
39+
2140
test_that("displays nicely", {
2241
bar <- new_external_generic("foo", "bar", "x")
2342
on.exit(external_methods_reset("S7"), add = TRUE)

tests/testthat/test-method-register.R

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,67 @@ describe("method registration", {
125125
})
126126
})
127127

128+
describe("method unregistration", {
129+
it("removes S7 method via NULL assignment", {
130+
foo <- new_generic("foo", "x")
131+
method(foo, class_character) <- function(x) "c"
132+
method(foo, class_integer) <- function(x) "i"
133+
expect_length(methods(foo), 2)
134+
135+
method(foo, class_character) <- NULL
136+
expect_length(methods(foo), 1)
137+
expect_equal(foo(1L), "i")
138+
expect_snapshot(foo("x"), error = TRUE)
139+
})
140+
141+
it("removes each method in a union signature", {
142+
foo <- new_generic("foo", "x")
143+
method(foo, class_numeric) <- function(x) "n"
144+
expect_length(methods(foo), 2)
145+
146+
method(foo, class_numeric) <- NULL
147+
expect_length(methods(foo), 0)
148+
})
149+
150+
it("removes method with multi-dispatch signature", {
151+
foo <- new_generic("foo", c("x", "y"))
152+
A <- new_class("A")
153+
B <- new_class("B")
154+
method(foo, list(A, B)) <- function(x, y) "AB"
155+
expect_equal(foo(A(), B()), "AB")
156+
157+
method(foo, list(A, B)) <- NULL
158+
expect_snapshot(foo(A(), B()), error = TRUE)
159+
})
160+
161+
it("is a silent no-op when the method doesn't exist", {
162+
foo <- new_generic("foo", "x")
163+
expect_silent(method(foo, class_character) <- NULL)
164+
expect_length(methods(foo), 0)
165+
})
166+
167+
it("errors when unregistering from an S3 generic", {
168+
foo <- new_class("foo")
169+
method(sum, foo) <- function(x, ...) "foo"
170+
expect_snapshot(method(sum, foo) <- NULL, error = TRUE)
171+
172+
# External generics that resolve to S3 generics also error
173+
base_sum <- new_external_generic("base", "sum", "x")
174+
expect_snapshot(method(base_sum, foo) <- NULL, error = TRUE)
175+
})
176+
177+
it("errors when unregistering from an S4 generic", {
178+
methods::setGeneric("removeS4", function(x) standardGeneric("removeS4"))
179+
on.exit(suppressMessages(methods::removeGeneric("removeS4")), add = TRUE)
180+
S4foo <- new_class("S4foo", package = NULL)
181+
S4_register(S4foo)
182+
on.exit(S4_remove_classes("S4foo"), add = TRUE)
183+
184+
method(removeS4, S4foo) <- function(x) "foo"
185+
expect_snapshot(method(removeS4, S4foo) <- NULL, error = TRUE)
186+
})
187+
})
188+
128189
describe("as_signature()", {
129190
it("returns a list that matches length of dispatch args", {
130191
foo1 <- new_generic("foo1", "x")

0 commit comments

Comments
 (0)