Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draft construct_reprex() and construct_session() #100

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export(construct_diff)
export(construct_dump)
export(construct_issues)
export(construct_multi)
export(construct_session)
export(deparse_call)
export(opts_Date)
export(opts_POSIXct)
Expand Down
129 changes: 129 additions & 0 deletions R/construct_reprex.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# what to do with dots ?
# they need to be evaled in different environments and might contain NSE too
# we night try to eval what we can in the correct env
# if env is not parent env we should store those in lists, maybe named ...1 etc
# so we'll have foo(x = ...1$x + ...$y) or foo(x = with(...1, x + y))


# FIXME: calling on a call or without arg in the call should be exactly the same and use more of the same code
# FIXME: have a safe mode, arg name TBD so we construct only quosures, or list of quosures for dots,
# and then call inject, so we don't force any evaluation.
# * If the expr is not a call or a symbol we can construct it as is without the quo
# * If env is .GlobalEnv the object can also be defined as is and the expression directly fed to the call
# * If env of all dots is .GlobalEnv we can define the object as
construct_reprex <- function(x, quote = TRUE, ..., check = NULL, template = NULL) {
env <- parent.frame()
nse_msgs <- character()

if (missing(x)) {
call <- sys.call(-1)
call_matched <- match.call(sys.function(-1), call, envir = parent.frame(2))
call_expanded <- eval.parent(bquote(substitute(.(call))))
call_for_nms <- call_matched
dot_pos <- which(sapply(call_matched[-1], function(x) is.symbol(x) && grepl("^\\.\\.\\d+$", as.character(x))))
for(dot in dot_pos) {
val <- try(eval(call_matched[[dot + 1]], env), silent = TRUE)
if (inherits(val, "try-error")) {
call_matched[[dot + 1]] <- call_expanded[[dot + 1]]
call_for_nms[[dot + 1]] <- NULL
}
}
nms <- all.names(call_for_nms[-1], unique = TRUE)
call <- call_matched
} else {
call <- if (quote) substitute(x) else x
nms <- all.names(call, unique = TRUE)
}
objs <- sapply(nms, function(x) try(eval(as.symbol(x), env), silent = TRUE), simplify = FALSE)
errors_lgl <- sapply(objs, inherits, "try-error")
objs <- objs[!errors_lgl]
data <- preprocess_data(list("base", "methods", "datasets", "utils", "grDevices", "stats")) # FIXME: graphics doesn't work because plot's env is base
code <- lapply(objs, construct_raw, template = template, data = data, ...)
useful_code <- code[!mapply(code, names(code), FUN = identical)]
useful_code_with_assignments <- Map(
useful_code, names(useful_code),
f = function(x, y) {
x[[1]] <- paste(y, "<-", x[[1]])
c(x, "")
})
out <- c(unlist(useful_code_with_assignments), deparse_call(call))
if (any(errors_lgl)) {
msg <- "Some variable bindings couldn't be found so objects were not reproduced."
info <- sprintf("Non existent binding for %s", toString(shQuote(nms[errors_lgl])))
rlang::inform(c(msg, i = info))
}
out <- styler::style_text(out)
if (missing(x)) do.call("return", list(out), envir = sys.frame(1))
out
}

construct_reprex <- function(x, quote = TRUE, ..., check = NULL, template = NULL) {
## was a call provided ?
if (missing(x)) {
## fetch call and fun from stack, fetch env 2 frames above
call <- sys.call(-1)
env <- parent.frame(2)
fun <- sys.function(-2)
} else {
## capture call, fetch env 1 frame above, eval fun
call <- substitute(x)
env <- parent.frame()
#fun <- sys.function(-1)
eval(call[[1]], env)
}
## create matched call, will contain all named arg and ..n for dots, deduce args
call_matched <- match.call(fun, call, expand = FALSE, envir = env)
args <- as.list(call_matched[-1])
# all args are named since the call is matched and expand is FALSE
objs <- list()
#browser()
for (arg_nm in names(args)) {
if (arg_nm == "...") {
objs[["..."]] <- evalq(rlang::enquos(...), env)
args[["..."]] <- quote(!!!..args..$...)
names(args)[names(args) == "..."] <- ""
next
}
objs[[arg_nm]] <- eval(substitute(enquo(ARG), list(ARG = .(sym(arg_nm)))), env)
args[[arg_nm]] <- substitute(!!..args..$ARG, list(ARG = .(sym(arg_nm))))
}

# update call
new_call <- as.call(c(call_matched[[1]], args))
new_call <- as.call(c(quote(rlang::inject), new_call))
new_call

# build code
multi_construct

code <- styler::style_text(code)
do.call("return", list(code), envir = sys.frame(1))
}



if (FALSE) {
f <- function(x, ...) {
construct_reprex()
c(x, ...)
}

# f <- function(x, y) {
# construct_reprex()
# x
# }

x <- 3
y <- 4
foo <- function(...) {
bar(...)
}

bar <- function(...) {
construct_reprex()
subset(...)
}

foo(cars, speed > 4)
}

205 changes: 205 additions & 0 deletions R/construct_session.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#' Construct the session
#'
#' This builds code to reproduce the session. By default it prints comments describing
#' the setup, attaching all non base packages present in the search path,
#' and constructs all objects present in the global environment. Objects exported
#' by attached package are used as data to sinplify object construction.
#'
#' More often than not it is not a perfectly faithful reproduction of the session,
#' in particular it doesn't account (yet?) for:
#' * Namespaces loaded by other means than `library()` calls
#' * Modifications of namespaces
#' * Other environments on the search opath
#' * Options that have been set
#' * And most likely anything weirder than the above
#'
#' @param ... Forwarded to `construct_multi()`. Should not include a `data` argument,
#' since `data` will be infered from attached packages.
#' @param session_info Whether to describe the setup in comments
#' @param library_calls Whether to include library calls
#'
#' @return An object of class 'constructive'.
#' @export
#' @examples
#' construct_session()
construct_session <- function(..., session_info = TRUE, library_calls = TRUE) {
# session info ---------------------------------------------------------------
if (session_info) {
si <- sessionInfo()
.rs.api.versionInfo <- NULL # to avoid note
si_code <- paste("#", c(
paste(si$R.version$version.string, if (Sys.getenv("RSTUDIO") == "1") paste("in RStudio", .rs.api.versionInfo()$version)),
paste("running", si$running, "on", si$platform),
paste("locale:", si$locale)
))
}
libs <- search()

# library calls --------------------------------------------------------------
if (library_calls){

base_pkgs <- c("stats", "graphics", "grDevices", "utils", "datasets", "methods", "base")
ignore_libs <- c(paste0("package:", base_pkgs), "devtools_shims", "tools:rstudio", "Autoloads")
libs <- setdiff(libs, ignore_libs)
libs <- grep("^package:", libs, value = TRUE)
libs <- sub("package:", "", libs)
data_libs <- libs
# special case for tidyverse since it's quite common
if ("tidyverse" %in% libs) {
libs <- setdiff(libs, c("forcats", "stringr", "dplyr", "purrr", "readr", "tidyr", "tibble", "ggplot2"))
}
data_libs <- c(data_libs, base_pkgs)
lib_code <- if (length(libs)) sprintf("library(%s)", rev(libs))
} else {
data_libs <- base_pkgs
}

# namespaces -----------------------------------------------------------------
# TODO: attach namespaces that are not recursive deps of attached packages
# TODO: detach relevant namespaces

# options --------------------------------------------------------------------
# TODO

# objects --------------------------------------------------------------------
objs <- setdiff(ls(.GlobalEnv, all.names = TRUE), ".Random.seed")
objs <- mget(objs, .GlobalEnv, inherits = FALSE)
# FIXME: is there a max_size that makes sense ?
# total_size <- sum(sapply(objs, object.size))
# if (total_size > max_size) {
# msg <- "The total size of objects must be less than `max_size`"
# info1 <- sprintf(
# "`max_size` is %s bytes, adjust the argument if necessary.",
# format(max_size, scientific = FALSE, big.mark = " ")
# )
# info2 <- sprintf(
# "The total size is %s bytes.",
# format(total_size, scientific = FALSE, big.mark = " ")
# )
# abort(c(msg, i = info1, x = info2))
# }
res <- construct_multi(objs, data = as.list(data_libs), ...)
res$code <- c(si_code, lib_code, res$code)
class(res$code) <- "vertical"
res
}

get_set_options <- function() {
# get default and actual options
default_opts <- get_default_options()
opts <- options()
# ignore on both sides the options RStudio sets or changes
# note: based on RStudio 4.2.1 on R 4.2.1
rstudio_sets <- c(
"askpass", "asksecret", "buildtools.check", "buildtools.with",
"connectionObserver", "deparse.max.lines", "download.file.method",
"ggvis.renderer", "help_type", "page_viewer", "plumber.docs.callback",
"plumber.swagger.url", "profvis.keep_output", "profvis.print",
"profvis.prof_extension", "profvis.prof_output", "restart", "reticulate.initialized",
"reticulate.repl.busy", "reticulate.repl.hook", "reticulate.repl.initialize",
"reticulate.repl.teardown", "rstudio.notebook.executing", "RStudioGD.antialias",
"RStudioGD.backend", "shiny.launch.browser", "shinygadgets.showdialog",
"terminal.manager", "viewer"
)

rstudio_changes <- c(
"browser", "device", "echo", "HTTPUserAgent", "keep.source",
"max.print", "menu.graphics", "pager", "pdfviewer", "width"
)

default_opts[c(rstudio_sets, rstudio_changes)] <- NULL
opts[c(rstudio_sets, rstudio_changes)] <- NULL

for (opt in names(opts)) {
if (identical(opts[[opt]], default_opts[[opt]], ignore.environment = TRUE)) {
opts[[opt]] <- NULL
next
}
both_are_funs_with_same_body <-
is.function(opts[[opt]]) &&
is.function(default_opts[[opt]]) &&
identical(as.list(opts[[opt]]), as.list(default_opts[[opt]]))
if (both_are_funs_with_same_body) {
opts[[opt]] <- NULL
}
}
opts
}

# get_default_options <- function() {
# default_opts <- callr::r(function(libs) {
# lapply(libs, library, character.only = TRUE)
# options()
# }, list (libs = sub("^package:", "", grep("^package:", search(), value = TRUE))))
# # remove options set by {callr}
# callr_sets <- c("callr.rprofile_loaded", "error", "showErrorCalls")
# default_opts[callr_sets] <- NULL
# default_opts
# }
#
#
#
# compare_to_default <- function() {
# opts <- options()
# # ignore on both sides the options RStudio sets or changes
# # note: based on RStudio 4.2.1 on R 4.2.1
# rstudio_sets <- c(
# "askpass", "asksecret", "buildtools.check", "buildtools.with",
# "connectionObserver", "deparse.max.lines", "download.file.method",
# "ggvis.renderer", "help_type", "page_viewer", "plumber.docs.callback",
# "plumber.swagger.url", "profvis.keep_output", "profvis.print",
# "profvis.prof_extension", "profvis.prof_output", "restart", "reticulate.initialized",
# "reticulate.repl.busy", "reticulate.repl.hook", "reticulate.repl.initialize",
# "reticulate.repl.teardown", "rstudio.notebook.executing", "RStudioGD.antialias",
# "RStudioGD.backend", "shiny.launch.browser", "shinygadgets.showdialog",
# "terminal.manager", "viewer"
# )
#
# rstudio_changes <- c(
# "browser", "device", "echo", "HTTPUserAgent", "keep.source",
# "max.print", "menu.graphics", "pager", "pdfviewer", "width"
# )
#
# rstudio_opts <- c(rstudio_sets, rstudio_changes)
# opts[rstudio_opts] <- NULL
# # we pass our system info to
# diffs <- callr::r(function(si, opts, rstudio_opts) {
# # libs in the correct attaching order
# libs <- rev(c(names(si$otherPkgs), si$basePkgs))
# lapply(libs, library, character.only = TRUE)
#
# # namespaces
#
# # callr loads several namespaces
# ## Imports:
# # processx (>= 3.6.1), <- imports additionally ps
# # R6, <- also imported by RStudio
# # utils <- also imported by R
# default_ns <- loadedNamespaces()
# extra_ns <- setdiff(names(si$loadedOnly), default_ns)
# lapply(extra_ns, loadNamespace)
#
# # options
#
# default_opts <- options()
# callr_sets <- c("callr.rprofile_loaded", "error", "showErrorCalls")
# default_opts[c(callr_sets, rstudio_opts)] <- NULL
#
# for (opt in names(opts)) {
# if (identical(opts[[opt]], default_opts[[opt]], ignore.environment = TRUE)) {
# opts[[opt]] <- NULL
# next
# }
# both_are_funs_with_same_body <-
# is.function(opts[[opt]]) &&
# is.function(default_opts[[opt]]) &&
# identical(as.list(opts[[opt]]), as.list(default_opts[[opt]]))
# if (both_are_funs_with_same_body) {
# opts[[opt]] <- NULL
# }
# }
#
# list(opts = opts, extra_ns = extra_ns)
# }, list (si = sessionInfo(), opts = opts, rstudio_opts = rstudio_opts))
# diffs
# }
39 changes: 39 additions & 0 deletions man/construct_session.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.