diff --git a/NAMESPACE b/NAMESPACE index 67b120a1..7aaa82ea 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -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) diff --git a/R/construct_reprex.R b/R/construct_reprex.R new file mode 100644 index 00000000..333389c8 --- /dev/null +++ b/R/construct_reprex.R @@ -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) +} + diff --git a/R/construct_session.R b/R/construct_session.R new file mode 100644 index 00000000..49f8e78b --- /dev/null +++ b/R/construct_session.R @@ -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 +# } diff --git a/man/construct_session.Rd b/man/construct_session.Rd new file mode 100644 index 00000000..5c682f11 --- /dev/null +++ b/man/construct_session.Rd @@ -0,0 +1,39 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/construct_session.R +\name{construct_session} +\alias{construct_session} +\title{Construct the session} +\usage{ +construct_session(..., session_info = TRUE, library_calls = TRUE) +} +\arguments{ +\item{...}{Forwarded to \code{construct_multi()}. Should not include a \code{data} argument, +since \code{data} will be infered from attached packages.} + +\item{session_info}{Whether to describe the setup in comments} + +\item{library_calls}{Whether to include library calls} +} +\value{ +An object of class 'constructive'. +} +\description{ +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. +} +\details{ +More often than not it is not a perfectly faithful reproduction of the session, +in particular it doesn't account (yet?) for: +\itemize{ +\item Namespaces loaded by other means than \code{library()} calls +\item Modifications of namespaces +\item Other environments on the search opath +\item Options that have been set +\item And most likely anything weirder than the above +} +} +\examples{ +construct_session() +}