-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathtree.R
More file actions
452 lines (416 loc) · 13.1 KB
/
tree.R
File metadata and controls
452 lines (416 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
#' Pretty tree-like object printing
#'
#' A cleaner and easier to read replacement for `str` for nested list-like
#' objects
#'
#' @param x A tree like object (list, etc.)
#' @param index_unnamed Should children of containers without names have indices
#' used as stand-in?
#' @param max_depth How far down the tree structure should be printed. E.g. `1`
#' means only direct children of the root element will be shown. Useful for
#' very deep lists.
#' @param max_vec_len How many elements should be printed for vectors?
#' @param show_environments Should environments be treated like normal lists and
#' recursed into?
#' @param hide_scalar_types Should atomic scalars be printed with type and
#' length like vectors? E.g. `x <- "a"` would be shown as `x<char [1]>: "a"`
#' instead of `x: "a"`.
#' @param max_length How many elements should be printed? This is useful in case
#' you try and print an object with 100,000 items in it.
#' @param val_printer Function that values get passed to before being drawn to
#' screen. Can be used to color or generally style output.
#' @param class_printer Same as `val_printer` but for the the class types of
#' non-atomic tree elements.
#' @param show_attributes Should attributes be printed as a child of the list or
#' avoided?
#' @param remove_newlines Should character strings with newlines in them have
#' the newlines removed? Not doing so will mess up the vertical flow of the
#' tree but may be desired for some use-cases if newline structure is
#' important to understanding object state.
#' @param tree_chars List of box characters used to construct tree. Needs
#' elements `$h` for horizontal bar, `$hd` for dotted horizontal bar, `$v` for
#' vertical bar, `$vd` for dotted vertical bar, `$l` for l-bend, and `$j` for
#' junction (or middle child).
#' @param ... Ignored (used to force use of names)
#'
#' @return console output of structure
#'
#' @examples
#'
#' x <- list(
#' list(id = "a", val = 2),
#' list(
#' id = "b",
#' val = 1,
#' children = list(
#' list(id = "b1", val = 2.5),
#' list(
#' id = "b2",
#' val = 8,
#' children = list(
#' list(id = "b21", val = 4)
#' )
#' )
#' )
#' ),
#' list(
#' id = "c",
#' val = 8,
#' children = list(
#' list(id = "c1"),
#' list(id = "c2", val = 1)
#' )
#' )
#' )
#'
#' # Basic usage
#' tree(x)
#'
#' # Even cleaner output can be achieved by not printing indices
#' tree(x, index_unnamed = FALSE)
#'
#' # Limit depth if object is potentially very large
#' tree(x, max_depth = 2)
#'
#' # You can customize how the values and classes are printed if desired
#' tree(x, val_printer = function(x) {
#' paste0("_", x, "_")
#' })
#' @export
tree <- function(
x,
...,
index_unnamed = FALSE,
max_depth = 10L,
max_length = 1000L,
max_vec_len = 10L,
show_environments = TRUE,
hide_scalar_types = TRUE,
val_printer = crayon::blue,
class_printer = crayon::silver,
show_attributes = FALSE,
remove_newlines = TRUE,
tree_chars = box_chars()
) {
rlang::check_dots_empty()
# Pack up the unchanging arguments into a list and send to tree_internal
termination_type <- tree_internal(
x,
opts = list(
index_unnamed = index_unnamed,
max_depth = max_depth,
max_length = max_length,
max_vec_len = max_vec_len,
show_envs = show_environments,
hide_scalar_types = hide_scalar_types,
val_printer = val_printer,
class_printer = class_printer,
show_attributes = show_attributes,
remove_newlines = remove_newlines,
tree_chars = tree_chars
)
)
if (termination_type == "early") {
cat("...", "\n")
}
invisible(x)
}
# Tree printing internal function
#
# This is the internal function for the main tree printing code. It wraps the
# static options arguments from the user-facing `tree()` into a single opts
# list to make recursive calls cleaner. It also has arguments that as it is
# called successively but the end-user shouldn't see or use.
tree_internal <- function(
x,
x_id = NULL,
branch_hist = character(0),
opts,
attr_mode = FALSE,
counter_env = rlang::new_environment(
data = list(n_printed = 0, envs_seen = c())
)
) {
counter_env$n_printed <- counter_env$n_printed + 1
# Stop if we've reached the max number of times printed desired
if (counter_env$n_printed > opts$max_length) {
return("early")
}
# Since self-loops can occur in environments check to see if we've seen any
# environments before
already_seen <- FALSE
if (rlang::is_environment(x)) {
already_seen <- any(vapply(
counter_env$envs_seen,
identical,
x,
FUN.VALUE = logical(1)
))
if (!already_seen) {
# If this environment is new, add it to the seen
counter_env$envs_seen[[length(counter_env$envs_seen) + 1]] <- x
}
}
depth <- length(branch_hist)
# Build branch string from branch history
# Start with empty spaces
branch_chars <- rep_len(" ", depth)
branch_chars[branch_hist == "child"] <- paste0(opts$tree_chars$v, " ")
branch_chars[grepl("attr", branch_hist, fixed = TRUE)] <- paste0(
opts$tree_chars$vd,
" "
)
# Next update the final element (aka the current step) with the correct branch type
last_step <- branch_hist[depth]
root_node <- length(branch_hist) == 0
branch_chars[depth] <- if (root_node) {
""
} else {
paste0(
if (grepl("last", last_step)) opts$tree_chars$l else opts$tree_chars$j,
if (grepl("attribute", last_step)) {
opts$tree_chars$hd
} else {
opts$tree_chars$h
}
)
}
# Build label
label <- paste0(
x_id,
make_type_abrev(x, opts$hide_scalar_types),
if (!rlang::is_null(x_id) && x_id != "") ": ",
tree_label(x, opts),
if (already_seen) " (Already seen)"
)
# Figure out how many children we have (plus attributes if they are being
# printed) so we can setup how to proceed
x_attributes <- attributes(x)
if (attr_mode) {
# Filter out "names" attribute as this is already shown by tree
x_attributes <- x_attributes[names(x_attributes) != "names"]
}
has_attributes <- length(x_attributes) > 0 && opts$show_attributes
has_children <- has_attributes || length(x) > 1
max_depth_reached <- depth >= opts$max_depth && has_children
# Do the actual printing to the console with an optional ellipses to indicate
# we've reached the max depth and won't recurse more
cat(
paste(branch_chars, collapse = ""),
label,
if (max_depth_reached) "...",
"\n",
sep = ""
)
# ===== Start recursion logic
if (already_seen || max_depth_reached) {
return("Normal finish")
}
if (rlang::is_list(x) || is_printable_env(x)) {
# Coerce current object to a plain list. This is necessary as some s3
# classes override `[[` and return funky stuff like themselves (see s3 class
# "package_version")
children <- if (is_printable_env(x)) {
# Environments are funky as they don't have names before conversion to list
# but do after, so let them handle their conversion.
# We use all.names = TRUE in an effort to fully explain the object
as.list.environment(x, all.names = TRUE)
} else {
# By wiping all attributes except for the names we force the object to be
# a plain list. This is inspired by the (now depreciated) rlang::as_list().
attributes(x) <- list(names = names(x))
as.list(x)
}
# Traverse children, if any exist
n_children <- length(children)
child_names <- names(children)
# If children have names, give them the names
for (i in seq_along(children)) {
id <- child_names[i]
if ((rlang::is_null(id) || id == "") && opts$index_unnamed) {
id <- crayon::italic(i)
}
child_type <- if (i < n_children) {
"child"
} else if (has_attributes) {
# We use "attrs" here instead of full "attribute" so a grep for
# attributes just gets plain "attribute" or "last-attribute" but a grep
# for "attr" gets all attribute related types
"pre-attrs"
} else {
"last-child"
}
termination_type <- Recall(
x = children[[i]],
x_id = id,
branch_hist = c(branch_hist, child_type),
opts = opts,
counter_env = counter_env
)
if (termination_type == "early") {
return(termination_type)
}
}
}
# ===== End recursion logic
# Add any attributes as an "attr" prefixed children at end
if (has_attributes) {
n_attributes <- length(x_attributes)
for (i in seq_len(n_attributes)) {
termination_type <- Recall(
x = x_attributes[[i]],
x_id = crayon::italic(paste0(
"attr(,\"",
names(x_attributes)[i],
"\")"
)),
opts = opts,
branch_hist = c(
branch_hist,
paste0(if (i == n_attributes) "last-", "attribute")
),
attr_mode = TRUE, # Let tree know this is an attribute
counter_env = counter_env
)
if (termination_type == "early") {
return(termination_type)
}
}
}
# If all went smoothly we reach here
"Normal finish"
}
# There are a few environments we don't want to recurse into
is_printable_env <- function(x) {
is_environment(x) &&
!(identical(x, rlang::global_env()) ||
identical(x, rlang::empty_env()) ||
identical(x, rlang::base_env()) ||
rlang::is_namespace(x))
}
#' Build element or node label in tree
#'
#' These methods control how the value of a given node is printed. New methods
#' can be added if support is needed for a novel class
#'
#' @inheritParams tree
#' @param opts A list of options that directly mirrors the named arguments of
#' [tree]. E.g. `list(val_printer = crayon::red)` is equivalent to
#' `tree(..., val_printer = crayon::red)`.
#'
#' @export
tree_label <- function(x, opts) {
UseMethod("tree_label")
}
#' @export
tree_label.function <- function(x, opts) {
func_args <- collapse_and_truncate_vec(methods::formalArgs(x), 5)
crayon::italic(paste0("function(", func_args, ")"))
}
#' @export
tree_label.environment <- function(x, opts) {
format.default(x)
}
#' @export
tree_label.NULL <- function(x, opts) {
"<NULL>"
}
#' @export
tree_label.character <- function(x, opts) {
# Get rid of new-line so they don't break tree flow
if (opts$remove_newlines) {
x <- gsub("\\n", replacement = "\u21B5", x = x, perl = TRUE)
}
# Shorten strings if needed
max_standalone_length <- 35
max_vec_length <- 15
max_length <- if (length(x) == 1) max_standalone_length else max_vec_length
x <- truncate_string(x, max_length)
tree_label.default(paste0("\"", x, "\""), opts)
}
#' @export
tree_label.default <- function(x, opts) {
if (rlang::is_atomic(x)) {
opts$val_printer(collapse_and_truncate_vec(x, opts$max_vec_len))
} else if (rlang::is_function(x)) {
# Lots of times function-like functions don't actually trigger the s3 method
# for function because they dont have function in their class-list. This
# catches those.
tree_label.function(x, opts)
} else if (rlang::is_environment(x)) {
# Environments also tend to have the same trouble as functions. For instance
# the srcobject attached to a function's attributes is an environment but
# doesn't report as one to s3.
tree_label.environment(x, opts)
} else if (rlang::is_expression(x) || rlang::is_formula(x)) {
paste0(label_class(x, opts), " ", crayon::italic(deparse(x)))
} else {
# The "base-case" is simply a list-like object.
label_class(x, opts)
}
}
collapse_and_truncate_vec <- function(vec, max_length) {
vec <- as.character(vec)
too_long <- length(vec) > max_length
if (too_long) {
vec <- utils::head(vec, max_length)
vec <- c(vec, "...")
}
paste0(vec, collapse = ", ")
}
truncate_string <- function(char_vec, max_length) {
ifelse(
nchar(char_vec) > max_length,
# Since we add an elipses we need to take a bit more than the max length
# off. The gsub adds elipses but also makes sure we dont awkwardly end on
# a space.
gsub(
x = substr(char_vec, start = 1, max_length - 3),
pattern = "\\s*$",
replacement = "...",
perl = TRUE
),
char_vec
)
}
make_type_abrev <- function(x, omit_scalars) {
if (!rlang::is_atomic(x) || (rlang::is_scalar_atomic(x) && omit_scalars)) {
return("")
}
type_abrev <- switch(
typeof(x),
logical = "lgl",
integer = "int",
double = "dbl",
character = "chr",
complex = "cpl",
expression = "expr",
raw = "raw",
"unknown"
)
paste0("<", type_abrev, " [", format(length(x), big.mark = ","), "]>")
}
# Inspired by waldo:::friendly_type_of(). Prints the class name and hierarchy
# encased in angle brackets along with a prefix that tells you what OO system
# the object belongs to (if it does.)
label_class <- function(x, opts) {
if (is_missing(x)) {
return("absent")
}
oo_prefix <- ""
class_list <- if (!is.object(x)) {
typeof(x)
} else if (isS4(x)) {
oo_prefix <- "S4"
methods::is(x)
} else if (inherits(x, "R6")) {
oo_prefix <- "R6"
setdiff(class(x), "R6")
} else {
oo_prefix <- "S3"
class(x)
}
opts$class_printer(
paste0(oo_prefix, "<", paste(class_list, collapse = "/"), ">")
)
}