Skip to content

Commit 664a828

Browse files
authored
Implement basic support for S7 (#1843)
* Methods get useful default names, aliases, and usage * New `@prop` tag for documenting non-constructor properties Fixes #1484
1 parent ab616a4 commit 664a828

18 files changed

Lines changed: 554 additions & 9 deletions

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Imports:
3737
Suggests:
3838
covr,
3939
R.methodsS3,
40+
S7,
4041
R.oo,
4142
rmarkdown (>= 2.16),
4243
testthat (>= 3.1.2),

NAMESPACE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ S3method(format,rd_section_name)
4545
S3method(format,rd_section_note)
4646
S3method(format,rd_section_package)
4747
S3method(format,rd_section_param)
48+
S3method(format,rd_section_prop)
4849
S3method(format,rd_section_rawRd)
4950
S3method(format,rd_section_rcmethods)
5051
S3method(format,rd_section_reexport)
@@ -64,6 +65,7 @@ S3method(merge,rd_section_inherit_dot_params)
6465
S3method(merge,rd_section_inherit_section)
6566
S3method(merge,rd_section_minidesc)
6667
S3method(merge,rd_section_param)
68+
S3method(merge,rd_section_prop)
6769
S3method(merge,rd_section_reexport)
6870
S3method(merge,rd_section_section)
6971
S3method(merge,rd_section_seealso)
@@ -80,6 +82,9 @@ S3method(object_defaults,s3method)
8082
S3method(object_defaults,s4class)
8183
S3method(object_defaults,s4generic)
8284
S3method(object_defaults,s4method)
85+
S3method(object_defaults,s7class)
86+
S3method(object_defaults,s7generic)
87+
S3method(object_defaults,s7method)
8388
S3method(object_defaults,value)
8489
S3method(object_format,default)
8590
S3method(object_name,"function")
@@ -94,6 +99,9 @@ S3method(object_usage,default)
9499
S3method(object_usage,s3method)
95100
S3method(object_usage,s4generic)
96101
S3method(object_usage,s4method)
102+
S3method(object_usage,s7class)
103+
S3method(object_usage,s7generic)
104+
S3method(object_usage,s7method)
97105
S3method(object_usage,value)
98106
S3method(print,object)
99107
S3method(print,rd)
@@ -166,6 +174,7 @@ S3method(roxy_tag_parse,roxy_tag_noRd)
166174
S3method(roxy_tag_parse,roxy_tag_note)
167175
S3method(roxy_tag_parse,roxy_tag_order)
168176
S3method(roxy_tag_parse,roxy_tag_param)
177+
S3method(roxy_tag_parse,roxy_tag_prop)
169178
S3method(roxy_tag_parse,roxy_tag_rawNamespace)
170179
S3method(roxy_tag_parse,roxy_tag_rawRd)
171180
S3method(roxy_tag_parse,roxy_tag_rdname)
@@ -208,6 +217,7 @@ S3method(roxy_tag_rd,roxy_tag_inheritSection)
208217
S3method(roxy_tag_rd,roxy_tag_keywords)
209218
S3method(roxy_tag_rd,roxy_tag_note)
210219
S3method(roxy_tag_rd,roxy_tag_param)
220+
S3method(roxy_tag_rd,roxy_tag_prop)
211221
S3method(roxy_tag_rd,roxy_tag_rawRd)
212222
S3method(roxy_tag_rd,roxy_tag_references)
213223
S3method(roxy_tag_rd,roxy_tag_return)

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# roxygen2 (development version)
2+
* Added initial support for S7 classes, generics, and methods (#1484).
3+
* S7 generics are documented like regular functions.
4+
* S7 classes are documented like regular functions, but you can use `@prop` to document additional properties that are not constructor parameters. If multiple classes share one page, use `@prop ClassName@prop_name description` to group properties by class.
5+
* S7 methods registered with `method(generic, class) <- fn` are detected automatically and generate usage with `## S7 method for class <ClassName>`.
26
* roxygen2 no longer depends on stringr/stringi. This means that no package in the devtools constellation depends on stringr, which in turn means you no longer need stringi, making it a bit easier to install in constrained Linux environments.
37
* roxygen2 options can now be set using `Config/roxygen2/` fields in DESCRIPTION (e.g. `Config/roxygen2/markdown: TRUE`) instead of the `Roxygen` field. The old `Roxygen` field is still supported. Similarly, the roxygen2 version is now stored in `Config/roxygen2/version` instead of `RoxygenNote` (#1328).
48
* Tags that expect single-line input now warn when they span multiple lines, catching common mistakes. Affected tags: `@aliases`, `@concept`, `@encoding`, `@exportClass`, `@exportMethod`, `@exportPattern`, `@exportS3Method`, `@importFrom`, `@importClassesFrom`, `@importMethodsFrom`, `@include`, `@inheritParams`, `@keywords`, `@method`, `@name`, `@order`, `@rdname`, `@S3method`, `@template`, and `@useDynLib` (#1642, #1688). This may break some existing usage, but it prevents a wide class of otherwise silent errors.

R/field.R

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,16 @@ rd_section_description <- function(name, dt, dd) {
7373
if (length(dt) == 0) {
7474
return("")
7575
}
76+
paste0("\\section{", name, "}{\n\n", rd_enumerate(dt, dd), "}\n")
77+
}
78+
rd_subsection_description <- function(name, dt, dd) {
79+
if (length(dt) == 0) {
80+
return("")
81+
}
82+
paste0("\\subsection{", name, "}{\n\n", rd_enumerate(dt, dd), "\n}\n")
83+
}
7684

85+
rd_enumerate <- function(dt, dd) {
7786
items <- paste0("\\item{\\code{", dt, "}}{", dd, "}", collapse = "\n\n")
78-
paste0("\\section{", name, "}{\n\n", "\\describe{\n", items, "\n}}\n")
87+
paste0("\\describe{\n", items, "\n}")
7988
}

R/object-defaults.R

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ object_defaults.import <- function(x, block) {
120120
)
121121
}
122122

123+
#' @export
124+
object_defaults.s7class <- object_defaults.function
125+
126+
#' @export
127+
object_defaults.s7generic <- object_defaults.function
128+
129+
#' @export
130+
object_defaults.s7method <- function(x, block) {
131+
list(
132+
roxy_generated_tag(block, "usage", object_usage(x)),
133+
roxy_generated_tag(block, ".formals", names(formals(x$value$fn)))
134+
)
135+
}
136+
123137
#' @export
124138
object_defaults.s4class <- function(x, block) {
125139
list(

R/object-from-call.R

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ object_from_call <- function(call, env, block, file) {
88
} else if (is_set_call(call)) {
99
parser_r6_set(call, env)
1010
} else if (is.call(call)) {
11+
if (is_s7_method_call(call)) {
12+
return(parser_s7_method(call, env, block))
13+
}
14+
1115
call <- call_match(call, eval(call[[1]], env))
1216
name <- deparse(call[[1]])
1317
switch(
@@ -74,6 +78,10 @@ object_from_name <- function(name, env, block) {
7478
type <- "s4method"
7579
} else if (methods::is(value, "standardGeneric")) {
7680
type <- "s4generic"
81+
} else if (inherits(value, "S7_class")) {
82+
type <- "s7class"
83+
} else if (inherits(value, "S7_generic")) {
84+
type <- "s7generic"
7785
} else if (is.function(value)) {
7886
# Potential S3 methods/generics need metadata added
7987
method <- block_get_tag_value(block, "method")
@@ -225,6 +233,63 @@ parser_setConstructorS3 <- function(call, env, block) {
225233
object(get(name, env), name, "function")
226234
}
227235

236+
# method(generic, class) <- fn
237+
# `<-`(method(generic, class), fn)
238+
is_s7_method_call <- function(call) {
239+
is_call(call, "<-", n = 2) && is_call(call[[2]], "method", ns = c("", "S7"))
240+
}
241+
242+
parser_s7_method <- function(call, env, block) {
243+
generic_call <- call[[2]][[2]]
244+
class_call <- call[[2]][[3]]
245+
method_call <- call[[3]]
246+
247+
generic <- eval(generic_call, env)
248+
generic_name <- generic@name
249+
250+
# Evaluate class spec: either a single class, a union, or list() for
251+
# multi-dispatch
252+
classes <- eval(class_call, env)
253+
if (!is_bare_list(classes)) {
254+
classes <- list(classes)
255+
}
256+
class_names <- lapply(classes, s7_class_name, block = block)
257+
258+
fn <- eval(method_call, env)
259+
260+
value <- list(fn = fn, generic = generic_name, classes = class_names)
261+
aliases <- s7_method_aliases(generic_name, class_names)
262+
object(value, aliases, "s7method")
263+
}
264+
265+
s7_method_aliases <- function(generic, classes) {
266+
if (!any(lengths(classes) > 1)) {
267+
return(NULL)
268+
}
269+
270+
combos <- expand.grid(classes, stringsAsFactors = FALSE)
271+
apply(combos, 1, function(row) {
272+
paste0(generic, ",", paste0(row, collapse = ","), "-method")
273+
})
274+
}
275+
276+
# https://github.com/RConsortium/S7/issues/594
277+
s7_class_name <- function(cls, block) {
278+
name <- nameOfClass(cls)
279+
if (!is.null(name)) {
280+
# Regular S7 class + base wrappers
281+
name
282+
} else if (inherits(cls, "S7_union")) {
283+
# Unions return vector of member names, recursing for nested types
284+
unlist(lapply(cls$classes, s7_class_name, block = block))
285+
} else if (inherits(cls, "S7_S3_class")) {
286+
cls$class
287+
} else {
288+
warn_roxy_block(block, "Unknown S7 class type")
289+
paste0(deparse(cls), collapse = " ")
290+
}
291+
}
292+
228293
# helpers -----------------------------------------------------------------
229294

230295
add_s3_metadata <- function(val, name, env, block) {
@@ -332,18 +397,16 @@ print.object <- function(x, ...) {
332397
object_topic <- function(value, alias, type) {
333398
switch(
334399
type,
335-
s4method = paste0(
336-
value@generic,
337-
",",
338-
paste0(value@defined, collapse = ","),
339-
"-method"
340-
),
400+
s4method = method_topic(value@generic, value@defined),
341401
s4class = paste0(value@className, "-class"),
342402
s4generic = value@generic,
343403
rcclass = paste0(value@className, "-class"),
344404
r6class = alias,
345405
r6method = alias,
346406
rcmethod = value@name,
407+
s7class = alias,
408+
s7generic = alias,
409+
s7method = method_topic(value$generic, value$classes),
347410
s3generic = alias,
348411
s3method = alias,
349412
import = alias,
@@ -355,6 +418,11 @@ object_topic <- function(value, alias, type) {
355418
)
356419
}
357420

421+
method_topic <- function(generic, classes) {
422+
class_strings <- vapply(classes, paste0, character(1), collapse = "/")
423+
paste0(generic, ",", paste0(class_strings, collapse = ","), "-method")
424+
}
425+
358426
call_to_object <- function(code, env = pkg_env(), file = NULL) {
359427
code <- enexpr(code)
360428

R/rd-s7.R

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#' @export
2+
roxy_tag_parse.roxy_tag_prop <- function(x) {
3+
x <- tag_two_part(x, "a property name", "a description")
4+
5+
# Optionally specify a class
6+
if (grepl("@", x$val$name, fixed = TRUE)) {
7+
pieces <- strsplit(x$val$name, "@", fixed = TRUE)[[1]]
8+
if (length(pieces) != 2 || pieces[[1]] == "" || pieces[[2]] == "") {
9+
warn_roxy_tag(x, "must have form class@prop")
10+
return()
11+
}
12+
x$val$class <- pieces[[1]]
13+
x$val$name <- pieces[[2]]
14+
}
15+
16+
x
17+
}
18+
19+
#' @export
20+
roxy_tag_rd.roxy_tag_prop <- function(x, base_path, env) {
21+
rd_section(
22+
x$tag,
23+
data.frame(
24+
class = x$val$class %||% NA_character_,
25+
name = x$val$name,
26+
description = x$val$description
27+
)
28+
)
29+
}
30+
31+
#' @export
32+
merge.rd_section_prop <- function(x, y, ...) {
33+
stopifnot(identical(class(x), class(y)))
34+
rd_section(x$type, rbind(x$value, y$value))
35+
}
36+
37+
#' @export
38+
format.rd_section_prop <- function(x, ...) {
39+
props <- x$value
40+
classes <- unique(props$class)
41+
42+
if (identical(classes, NA_character_)) {
43+
return(rd_section_description(
44+
"Additional properties",
45+
paste0("@", props$name),
46+
props$description
47+
))
48+
}
49+
50+
sections <- map_chr(classes, function(cls) {
51+
rows <- props[props$class == cls, ]
52+
rd_subsection_description(cls, paste0("@", rows$name), rows$description)
53+
})
54+
55+
paste0(
56+
"\\section{Additional properties}{\n\n",
57+
paste0(sections, collapse = "\n"),
58+
"}\n"
59+
)
60+
}

R/rd-usage.R

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,31 @@ object_usage.s4method <- function(x) {
7575
function_usage(x$value@generic, formals(x$value), s4method)
7676
}
7777

78+
#' @export
79+
object_usage.s7class <- object_usage.function
80+
81+
#' @export
82+
object_usage.s7generic <- object_usage.function
83+
84+
#' @export
85+
object_usage.s7method <- function(x) {
86+
generic <- x$value$generic
87+
classes <- x$value$classes
88+
89+
formatted <- map_chr(classes, \(nms) paste0("<", nms, ">", collapse = "/"))
90+
if (length(formatted) == 1) {
91+
comment <- paste0("## S7 method for class ", formatted)
92+
} else {
93+
comment <- paste0(
94+
"## S7 method for classes ",
95+
paste0(formatted, collapse = ", ")
96+
)
97+
}
98+
99+
usage <- function_usage(generic, formals(x$value$fn), identity)
100+
rd(paste0(comment, "\n", usage))
101+
}
102+
78103
# Function usage ----------------------------------------------------------
79104

80105
# Usage:

inst/roxygen2-tags.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,13 @@
307307
vignette: rd
308308
recommend: true
309309

310+
- name: prop
311+
description: >
312+
Describe an S7 class property that is not a constructor parameter.
313+
template: ' ${1:name} ${2:description}'
314+
vignette: rd-oop
315+
recommend: true
316+
310317
- name: R6method
311318
description: >
312319
Document an R6 method that can't be discovered by introspection,

man/figures/test-figure-1.png

4 Bytes
Loading

0 commit comments

Comments
 (0)