0byt3m1n1
Path:
C:
/
Program Files
/
RStudio
/
resources
/
app
/
R
/
modules
/
[
Home
]
File: SessionReticulate.R
# # SessionReticulate.R # # Copyright (C) 2022 by Posit Software, PBC # # Unless you have received this program directly from Posit Software pursuant # to the terms of a commercial license agreement with Posit Software, then # this program is licensed to you under the terms of version 3 of the # GNU Affero General Public License. This program is distributed WITHOUT # ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, # MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the # AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. # # # synchronize with ReticulateEvent.java .rs.setVar("reticulateEvents", list( PYTHON_INITIALIZED = "python_initialized", REPL_INITIALIZED = "repl_initialized", REPL_ITERATION = "repl_iteration", REPL_BUSY = "repl_busy", REPL_TEARDOWN = "repl_teardown" )) .rs.setVar("reticulate.hookedMatplotlibModules", new.env(parent = emptyenv())) .rs.setVar("python.moduleCache", new.env(parent = emptyenv())) .rs.addJsonRpcHandler("python_get_completions", function(line, ctx) { if (!requireNamespace("reticulate", quietly = TRUE)) return(.rs.emptyCompletions()) completions <- .rs.tryCatch(.rs.python.getCompletions(line, ctx)) if (inherits(completions, "error")) return(.rs.emptyCompletions(language = "Python")) .rs.makeCompletions( token = attr(completions, "token"), results = as.character(completions), type = attr(completions, "types"), packages = attr(completions, "source"), quote = FALSE, helpHandler = "reticulate:::help_handler", language = "Python" ) }) .rs.addJsonRpcHandler("python_go_to_definition", function(line, offset) { result <- .rs.python.goToDefinition(line, offset) .rs.scalar(result) }) .rs.addFunction("python.goToDefinition", function(line, offset) { # extract the line providing the object definition we're looking for text <- .rs.python.extractCurrentExpression(line, offset) if (!nzchar(text)) return(FALSE) # try extracting this object object <- .rs.tryCatch(reticulate::py_eval(text, convert = FALSE)) if (inherits(object, "error")) return(FALSE) # TODO: use object explorer to view modules? or navigate # to their module path? or something similar? if (inherits(object, "python.builtin.module")) return(FALSE) # check to see if 'inspect' can find the object sources inspect <- reticulate::import("inspect", convert = TRUE) info <- .rs.tryCatch( list( source = inspect$getsourcefile(object), line = inspect$findsource(object)[[2]] ) ) if (inherits(info, "error")) return(FALSE) .rs.api.navigateToFile(info$source, info$line + 1L, 1L) return(TRUE) }) .rs.addJsonRpcHandler("python_go_to_help", function(line, offset) { result <- .rs.python.goToHelp(line, offset) .rs.scalar(result) }) .rs.addFunction("python.goToHelp", function(line, offset) { text <- .rs.python.extractCurrentExpression(line, offset) if (!nzchar(text)) return(FALSE) .Call("rs_showPythonHelp", text, PACKAGE = "(embedding)") return(TRUE) }) .rs.addFunction("reticulate.enqueueClientEvent", function(type, data) { data <- list(type = .rs.scalar(type), data = data) .rs.enqueClientEvent("reticulate_event", data) }) .rs.addFunction("reticulate.initialize", function() { # try to default to the tkAgg backend (note that we'll # force a vanilla 'agg' backend when required otherwise) engine <- tolower(Sys.getenv("MPLENGINE")) if (engine %in% c("", "qt5agg")) Sys.setenv(MPLENGINE = "tkAgg") # update default version of Python to be used when reticulate is loaded .rs.registerPackageLoadHook("reticulate", function(...) { python <- .rs.readUiPref("python_path") .rs.reticulate.usePython(python) if (packageVersion("reticulate") >= "1.23") { .rs.addFunction("reticulate.describeObjectLength", function(object) { reticulate::py_len(object, -1L) }) } }) }) .rs.addFunction("reticulate.onPythonInitialized", function() { builtins <- reticulate::import_builtins(convert = FALSE) # override help method (Python's interactive help does # not play well with RStudio) help <- builtins$help .rs.setVar("reticulate.help", builtins$help) builtins$help <- function(...) { dots <- list(...) if (length(dots) == 0) { message("Error: Interactive Python help not available within RStudio") return() } help(...) } # ensure matplotlib hooks are injected on load setHook( "reticulate::matplotlib.pyplot::load", function(...) .rs.reticulate.matplotlib.onLoaded() ) setHook( "reticulate::matplotlib.pylab::load", function(...) .rs.reticulate.matplotlib.onLoaded() ) # NOTE: Depending on how pyplot is loaded, the reticulate hooks might # not 'see' it. For that reason, we also hook the base 'matplotlib' # load hook, and override show when that is loaded. # # https://github.com/rstudio/rstudio/issues/12053 setHook( "reticulate::matplotlib::load", function(...) .rs.reticulate.matplotlib.onLoaded() ) }) .rs.addFunction("reticulate.matplotlib.onLoaded", function() { # install matplotlib hook if available canInstallHooks <- requireNamespace("png", quietly = TRUE) && reticulate::py_module_available("matplotlib") if (!canInstallHooks) return() matplotlib <- reticulate::import("matplotlib", convert = TRUE) # force the "Agg" backend (this is necessary as other backends may # fail with RStudio if requisite libraries are not available) backend <- matplotlib$get_backend() if (!identical(tolower(backend), "agg")) { sys <- reticulate::import("sys", convert = TRUE) if ("matplotlib.backends" %in% names(sys$modules)) matplotlib$pyplot$switch_backend("agg") else matplotlib$use("agg", warn = FALSE, force = TRUE) } moduleNames <- c("matplotlib.pyplot", "matplotlib.pylab") for (moduleName in moduleNames) { # if we've already hooked this module, nothing to do if (exists(moduleName, envir = .rs.reticulate.hookedMatplotlibModules)) next # if the requisite module hasn't been loaded yet, nothing to do sys <- reticulate::import("sys", convert = TRUE) if (is.null(sys$modules[[moduleName]])) next # otherwise, install our hook module <- reticulate::import(moduleName, convert = TRUE) module$show <- .rs.reticulate.matplotlib.showHook assign(moduleName, TRUE, envir = .rs.reticulate.hookedMatplotlibModules) } }) .rs.addFunction("reticulate.matplotlib.showHook", function(...) { # read device size size <- dev.size(units = "in") width <- size[1] height <- size[2] dpi <- 92 # adjust for pixel ratio ratio <- .Call("rs_devicePixelRatio", PACKAGE = "(embedding)") dpi <- dpi * ratio # TODO: get device requested from matplotlib? # TODO: handle HTML content? path <- tempfile("matplotlib-plot-", fileext = ".png") plt <- reticulate::import("matplotlib.pyplot", convert = TRUE) # resize the figure figure <- plt$gcf() figure$set_dpi(dpi) figure$set_size_inches(width, height) plt$savefig(path, dpi = figure$dpi) # now, read that in and create an R plot using e.g. image data <- png::readPNG(path, native = TRUE, info = TRUE) # don't display margins and ensure R doesn't nudge margin size opar <- par( xaxt = "n", yaxt = "n", xaxs = "i", yaxs = "i", mar = c(0, 0, 0, 0), oma = c(0, 0, 0, 0), xpd = NA ) on.exit(par(opar), add = TRUE) # generate raster plot.new() graphics::rasterImage(data, 0, 0, 1, 1) }) .rs.addFunction("reticulate.replInitialize", function() { # compute interpreter info (only needs to be done once as the Python # interpreter cannot be re-initialized again in the same session) info <- .rs.getVar("python.activeInterpreterInfo") if (is.null(info)) { config <- reticulate::py_config() info <- .rs.python.describeInterpreter(config$python) .rs.setVar("python.activeInterpreterInfo", info) } # signal a switch to Python context .rs.reticulate.enqueueClientEvent( .rs.reticulateEvents$REPL_INITIALIZED, info ) }) .rs.addFunction("reticulate.replHook", function(buffer, contents, trimmed) { # ensure we call repl_iteration hook on exit on.exit( .rs.reticulate.enqueueClientEvent( .rs.reticulateEvents$REPL_ITERATION, list() ), add = TRUE ) # special handling for commands when buffer is currently empty if (buffer$empty()) { # detect help requests, and route to Help pane if (grepl("^[?]", trimmed)) { text <- substring(trimmed, 2) .Call("rs_showPythonHelp", text, PACKAGE = "(embedding)") return(TRUE) } reHelp <- "help\\s*\\((.*)\\)" if (grepl(reHelp, trimmed)) { text <- .rs.trimWhitespace(gsub(reHelp, "\\1", trimmed)) .Call("rs_showPythonHelp", text, PACKAGE = "(embedding)") return(TRUE) } # detect View calls, and use View hook instead pattern <- "^View\\s*\\((.*)\\)$" matches <- regmatches(trimmed, regexec(pattern, trimmed)) if (length(matches) && length(matches[[1]]) == 2) { name <- gsub("^\\s*|\\s*$", "", matches[[1]][[2]]) object <- reticulate::py_eval(name, convert = FALSE) .rs.reticulate.viewHook(object, name) return(TRUE) } } FALSE }) .rs.addFunction("reticulate.replBusy", function(busy) { .rs.reticulate.enqueueClientEvent( .rs.reticulateEvents$REPL_BUSY, list(busy = .rs.scalar(busy)) ) }) .rs.addFunction("reticulate.replTeardown", function() { # client event .rs.reticulate.enqueueClientEvent( .rs.reticulateEvents$REPL_TEARDOWN, list() ) }) .rs.addFunction("reticulate.replIsActive", function() { if (.rs.isBrowserActive()) return(FALSE) if (!"reticulate" %in% loadedNamespaces()) return(FALSE) active <- tryCatch(reticulate:::py_repl_active(), error = identity) if (inherits(active, "error")) return(FALSE) active }) .rs.addFunction("python.tokenizationRules", function() { list( list( pattern = sprintf("(?:%s)\\b", paste(.rs.python.keywords(), collapse = "|")), type = "keyword" ), list( pattern = "[[:alpha:]_][[:alnum:]_]*\\b", type = "identifier" ), list( pattern = "((\\d+[jJ]|((\\d+\\.\\d*|\\.\\d+)([eE][-+]?\\d+)?|\\d+[eE][-+]?\\d+)[jJ])|((\\d+\\.\\d*|\\.\\d+)([eE][-+]?\\d+)?|\\d+[eE][-+]?\\d+)|(0[xX][\\da-fA-F]+[lL]?|0[bB][01]+[lL]?|(0[oO][0-7]+)|(0[0-7]*)[lL]?|[1-9]\\d*[lL]?))\\b", type = "number" ), list( pattern = '["]{3}(.*?)(?:["]{3}|$)', type = "string" ), list( pattern = "[']{3}(.*?)(?:[']{3}|$)", type = "string" ), list( pattern = '["](?:(?:\\\\.)|(?:[^"\\\\]))*?(?:["]|$)', type = "string" ), list( pattern = "['](?:(?:\\\\.)|(?:[^'\\\\]))*?(?:[']|$)", type = "string" ), list( pattern = "\\*\\*=?|>>=?|<<=?|<>|!+|//=?|[%&|^=<>*/+-]=?|~", type = "operator" ), list( pattern = "[:;.,`@]", type = "special" ), list( pattern = "[][)(}{]", type = "bracket" ), list( pattern = "#[^\n]*", type = "comment" ), list( pattern = "[[:space:]]+", type = "whitespace" ) ) }) .rs.addFunction("python.token", function(value, type, offset) { list(value = value, type = type, offset = offset) }) .rs.addFunction("python.tokenize", function( code, exclude = character(), keep.unknown = TRUE) { # vector of tokens tokens <- list() # rules to use rules <- .rs.python.tokenizationRules() # convert to raw vector so we can use 'grepRaw', # which supports offset-based search raw <- charToRaw(code) n <- length(raw) # record current offset offset <- 1 while (offset <= n) { # record whether we successfully matched a rule matched <- FALSE # iterate through rules, looking for a match for (rule in rules) { # augment pattern to search only from start of requested offset pattern <- paste("^(?:", rule$pattern, ")", sep = "") match <- grepRaw(pattern, raw, offset = offset, value = TRUE) if (length(match) == 0) next # we found a match; record that matched <- TRUE # update our vector of tokens token <- .rs.python.token(rawToChar(match), rule$type, offset) if (!token$type %in% exclude) tokens[[length(tokens) + 1]] <- token # update offset and break offset <- offset + length(match) break } # if we failed to match anything, consume a single character if (!matched) { # update tokens token <- .rs.python.token(rawToChar(raw[[offset]]), "unknown", offset) if (keep.unknown) tokens[[length(tokens) + 1]] <- token # update offset offset <- offset + 1 } } class(tokens) <- "tokens" tokens }) .rs.addFunction("python.tokenCursor", function(tokens) { .tokens <- tokens .offset <- 1L .n <- length(tokens) .lbrackets <- c("(", "{", "[") .rbrackets <- c(")", "}", "]") .complements <- list( "(" = ")", "[" = "]", "{" = "}", ")" = "(", "]" = "[", "}" = "{" ) tokenValue <- function() { .tokens[[.offset]]$value } tokenType <- function() { .tokens[[.offset]]$type } tokenOffset <- function() { .tokens[[.offset]]$offset } cursorOffset <- function() { .offset } moveToOffset <- function(offset) { if (offset < 1L) .offset <<- 1L else if (offset > .n) .offset <<- .n else .offset <<- offset } moveToNextToken <- function(i = 1L) { offset <- .offset + i if (offset > .n) return(FALSE) .offset <<- offset return(TRUE) } moveToPreviousToken <- function(i = 1L) { offset <- .offset - i if (offset < 1L) return(FALSE) .offset <<- offset return(TRUE) } moveRelative <- function(i = 1L) { offset <- .offset + i if (offset < 1L || offset > .n) return(FALSE) .offset <<- offset return(TRUE) } fwdToMatchingBracket <- function() { token <- .tokens[[.offset]] value <- token$value if (!value %in% .lbrackets) return(FALSE) lhs <- value rhs <- .complements[[lhs]] count <- 1 while (moveToNextToken()) { value <- tokenValue() if (value == lhs) { count <- count + 1 } else if (value == rhs) { count <- count - 1 if (count == 0) return(TRUE) } } return(FALSE) } bwdToMatchingBracket <- function() { token <- .tokens[[.offset]] value <- token$value if (!value %in% .rbrackets) return(FALSE) lhs <- value rhs <- .complements[[lhs]] count <- 1 while (moveToPreviousToken()) { value <- tokenValue() if (value == lhs) { count <- count + 1 } else if (value == rhs) { count <- count - 1 if (count == 0) return(TRUE) } } return(FALSE) } peek <- function(i = 0L) { offset <- .offset + i if (offset < 1L || offset > .n) return(.rs.python.token("", "unknown", -1L)) return(.tokens[[offset]]) } find <- function(predicate, forward = TRUE) { if (forward) { offset <- .offset + 1L while (offset <= .n) { token <- .tokens[[offset]] if (predicate(token)) { .offset <<- offset return(TRUE) } offset <- offset + 1L } return(FALSE) } else { offset <- .offset - 1L while (offset >= 1L) { token <- .tokens[[offset]] if (predicate(token)) { .offset <<- offset return(TRUE) } offset <- offset - 1L } return(FALSE) } } # move to the start of a Python statement, e.g. # # alpha.beta["gamma"] # ^~~~~~~~~<~~~~~~~~^ # moveToStartOfEvaluation <- function() { repeat { # skip matching brackets if (bwdToMatchingBracket()) { if (!moveToPreviousToken()) return(TRUE) next } # if the previous token is an identifier or a '.', move on to it previous <- peek(-1L) if (previous$value %in% "." || previous$type %in% "identifier") { moveToPreviousToken() next } break } TRUE } list( tokenValue = tokenValue, tokenType = tokenType, tokenOffset = tokenOffset, cursorOffset = cursorOffset, moveToNextToken = moveToNextToken, moveToPreviousToken = moveToPreviousToken, fwdToMatchingBracket = fwdToMatchingBracket, bwdToMatchingBracket = bwdToMatchingBracket, moveToOffset = moveToOffset, moveRelative = moveRelative, peek = peek, find = find, moveToStartOfEvaluation = moveToStartOfEvaluation ) }) .rs.addFunction("python.completions", function(token, candidates, source = NULL, type = NULL, reorder = TRUE) { # figure out the completions to keep pattern <- paste("^\\Q", token, "\\E", sep = "") indices <- grep(pattern, candidates, perl = TRUE) if (reorder) indices <- indices[order(candidates[indices])] # extract our completions completions <- candidates[indices] # re-order source and type if they were provided if (!is.null(source) && length(source) == length(candidates)) source <- source[indices] if (!is.null(type) && length(type) == length(candidates)) type <- type[indices] attr(completions, "token") <- token attr(completions, "source") <- source attr(completions, "types") <- type attr(completions, "helpHandler") <- "reticulate:::help_handler" completions }) .rs.addFunction("python.emptyCompletions", function() { character() }) .rs.addFunction("python.getCompletionsImports", function(token) { # split into pieces (note that strsplit drops an empty final match # so we need to add it back if the token is e.g. 'a.b.') pieces <- strsplit(token, ".", fixed = TRUE)[[1]] if (grepl("[.]$", token)) pieces <- c(pieces, "") # no '.' implies we're completing top-level modules if (length(pieces) < 2) { completions <- .rs.python.listModules() return(.rs.python.completions(token, completions)) } # we're completing a sub-module. try to import that module, and # then list things we can import from that module. note that importing # a module does imply running a load of Python code but other Python # front-ends (e.g. IPython) do this as well. module <- paste(head(pieces, n = -1), collapse = ".") imported <- tryCatch(reticulate::import(module), error = identity) if (inherits(imported, "error")) return(.rs.emptyCompletions()) exports <- sort(unique(names(imported))) postfix <- pieces[length(pieces)] completions <- .rs.python.completions(postfix, exports) # now, bring back full prefix for completions if (length(completions)) { prefix <- paste(pieces[-length(pieces)], collapse = ".") completions <- paste(prefix, completions, sep = ".") } # add in metadata attr(completions, "token") <- token attr(completions, "types") <- 21 completions }) .rs.addFunction("python.getCompletionsImportsFrom", function(module, token) { # request completions as though this were <module>.<token> pasted <- paste(module, token, sep = ".") completions <- .rs.python.getCompletionsImports(pasted) # fix up the completions (remove the module prefix) if (length(completions)) { prefix <- paste(module, ".", sep = "") completions <- sub(prefix, "", completions, fixed = TRUE) } attr(completions, "token") <- token completions }) .rs.addFunction("python.getCompletionsFiles", function(token) { os <- reticulate::import("os", convert = TRUE) token <- gsub("^['\"]|['\"]$", "", token) expanded <- path.expand(token) # find the index of the last slash -- everything following is # the completion token; everything before is the directory to # search for completions in indices <- gregexpr("/", expanded, fixed = TRUE)[[1]] if (!identical(c(indices), -1L)) { lhs <- substring(expanded, 1, tail(indices, n = 1)) rhs <- substring(expanded, tail(indices, n = 1) + 1) files <- paste(lhs, list.files(lhs), sep = "") } else { lhs <- "." rhs <- expanded files <- list.files(os$getcwd()) } # form completions (but add extra metadata after) completions <- .rs.python.completions(expanded, files) attr(completions, "token") <- token info <- file.info(completions) attr(completions, "types") <- ifelse(info$isdir, 16, 15) completions }) .rs.addFunction("python.getCompletionsKeys", function(source, token) { builtins <- reticulate::import_builtins(convert = TRUE) object <- tryCatch(reticulate::py_eval(source, convert = FALSE), error = identity) if (inherits(object, "error")) return(.rs.python.emptyCompletions()) method <- reticulate::py_get_attr(object, "keys", silent = TRUE) if (!inherits(method, "python.builtin.object")) return(.rs.python.emptyCompletions()) keys <- reticulate::py_to_r(method) candidates <- if (.rs.python.isPython3()) as.character(builtins$list(reticulate::py_to_r(keys()))) else reticulate::py_to_r(keys()) .rs.python.completions(token, candidates) }) .rs.addFunction("python.getCompletionsArguments", function(source, token) { object <- tryCatch(reticulate::py_eval(source, convert = FALSE), error = identity) if (inherits(object, "error")) return(.rs.python.emptyCompletions()) arguments <- .rs.python.getFunctionArguments(object) # paste on an '=' for completions (Python users seem to prefer no # spaces between the argument name and value) .rs.python.completions( token = token, candidates = paste(arguments, "=", sep = ""), source = source, type = .rs.acCompletionTypes$ARGUMENT, reorder = FALSE ) }) .rs.addFunction("python.getFunctionArguments", function(object) { inspect <- reticulate::import("inspect", convert = TRUE) # for class objects, we'll look up arguments on the associated # __init__ method instead if (inspect$isclass(object)) { init <- .rs.tryCatch(reticulate::py_get_attr(object, "__init__")) if (inherits(init, "error")) return(.rs.python.emptyCompletions()) arguments <- .rs.tryCatch(inspect$getargspec(init)$args) if (inherits(arguments, "error")) return(.rs.python.emptyCompletions()) return(setdiff(arguments, "self")) } # try a set of methods for extracting these arguments methods <- list( function() inspect$getargspec(object)$args, function() .rs.python.getNumpyFunctionArguments(object) ) for (method in methods) { arguments <- .rs.tryCatch(method()) if (!inherits(arguments, "error")) return(arguments) } character() }) .rs.addFunction("python.getNumpyFunctionArguments", function(object) { # extract the docstring docs <- reticulate::py_get_attr(object, "__doc__") if (inherits(docs, "python.builtin.object")) docs <- reticulate::py_to_r(docs) pieces <- strsplit(docs, "\n", fixed = TRUE)[[1]] first <- pieces[[1]] # try munging so that it 'looks' like an R function definition, # and then parse it that way. this will obviously fail for certain # kinds of Python default arguments but this seems to catch the # most common cases for now munged <- paste(gsub("[^(]*[(]", "function (", first), "{}") parsed <- parse(text = munged)[[1]] # extract the formal names names(parsed[[2]]) }) .rs.addFunction("python.getCompletionsMain", function(token, ctx) { dots <- gregexpr(".", token, fixed = TRUE)[[1]] if (identical(c(dots), -1L)) { # provide completions for main, builtins, keywords main <- reticulate::import_main(convert = FALSE) builtins <- reticulate::import_builtins(convert = FALSE) # figure out object types for main, builtins keywords <- .rs.python.keywords() candidates <- c(names(main), names(builtins), keywords) source <- c( rep("reticulate:::import_main(convert = FALSE)", length(names(main))), rep("reticulate:::import_builtins(convert = FALSE)", length(names(builtins))), rep("", length(keywords)) ) # figure out object types type <- c( .rs.python.inferObjectTypes(main, names(main)), .rs.python.inferObjectTypes(builtins, names(builtins)), rep(.rs.acCompletionTypes$KEYWORD, length(keywords)) ) completions <- .rs.python.completions( token = token, candidates = candidates, source = source, type = type ) return(completions) } # we had dots; try to evaluate each component piece-by-piece to get # to the relevant object providing us with completions pieces <- .rs.strsplit(token, ".", fixed = TRUE) # for the first piece, check to see if it might be a module first <- .rs.python.sanitizeForCompletion(pieces[[1]]) object <- if (first %in% names(ctx$aliases)) { module <- ctx$aliases[[first]] .rs.tryCatch(reticulate::import(module, convert = FALSE)) } else { reticulate::py_eval(first, convert = FALSE) } # now, try to extract sub-pieces from the module / object we received i <- 2 while (i < length(pieces)) { if (inherits(object, "error")) break code <- .rs.python.sanitizeForCompletion(pieces[[i]]) object <- .rs.tryCatch(reticulate::py_get_attr(object, code)) i <- i + 1 } if (inherits(object, "error")) return(.rs.python.emptyCompletions()) # attempt to get completions candidates <- if (inherits(object, "__main__.R")) { # for the custom 'R' object, return objects in the global environment # we might want to consider whether data objects should be returned too ls(envir = globalenv()) } else { # otherwise, use object attributes instead tryCatch(reticulate::py_list_attributes(object), error = identity) } # split string into source (module or sub-module providing object) # and token token <- tail(pieces, n = 1L) source <- paste(head(pieces, n = -1L), collapse = ".") # build completions object completions <- .rs.python.completions( token = token, candidates = candidates, source = source, type = .rs.python.inferObjectTypes(object, candidates) ) completions }) .rs.addFunction("python.getCompletions", function(line, ctx) { # check for completion of a module name in e.g. 'import nu' or 'from nu' re_import <- paste( "^[[:space:]]*", # leading whitespace "(?:from|import)", # from or import "[[:space:]]+", # separating spaces "([[:alnum:]._]*)$", # module name sep = "" ) matches <- regmatches(line, regexec(re_import, line, perl = TRUE))[[1]] if (length(matches) == 2) return(.rs.python.getCompletionsImports(matches[[2]])) # check for completion of submodule re_import_from <- paste( "^[[:space:]]*", # leading space "from", # 'from' "[[:space:]]+", # separating spaces "([[:alnum:]._]+)", # module name "[[:space:]]+", # separating spaces "import", # 'import' "[[:space:]]+", # separating spaces "\\(?", # an optional opening bracket (tuple style) "[[:space:]]*", # optional whitespace "([^)]*)", # the rest (including whitespace) sep = "" ) matches <- regmatches(line, regexec(re_import_from, line, perl = TRUE))[[1]] if (length(matches) == 3) { # extract module from which imports are being drawn module <- matches[[2]] imports <- matches[[3]] # figure out the text following the last comma (if any) token <- "" if (nzchar(imports)) token <- gsub(".*[[:space:],]", "", imports) return(.rs.python.getCompletionsImportsFrom(module, token)) } # tokenize the line and grab the last token tokens <- .rs.python.tokenize( code = line, exclude = c("whitespace", "comment"), keep.unknown = FALSE ) if (length(tokens) == 0) return(.rs.python.emptyCompletions()) # construct token cursor cursor <- .rs.python.tokenCursor(tokens) cursor$moveToOffset(length(tokens)) token <- cursor$peek() # for strings, we may be either completing dictionary keys or files if (token$type %in% "string") { # if there's no prior token, assume this is a file name if (!cursor$moveToPreviousToken()) return(.rs.python.getCompletionsFiles(token$value)) # if the prior token is an open bracket, assume we're completing # a dictionary key if (cursor$tokenValue() == "[") { saved <- cursor$peek() if (!cursor$moveToPreviousToken()) return(.rs.python.emptyCompletions()) if (!cursor$moveToStartOfEvaluation()) return(.rs.python.emptyCompletions()) # grab text from this offset lhs <- substring(line, cursor$tokenOffset(), saved$offset - 1) rhs <- gsub("^['\"]|['\"]$", "", token$value) # bail if there are any '(' tokens (avoid arbitrary function eval) # in theory this screens out tuples but that's okay for now tokens <- .rs.python.tokenize(lhs) lparen <- Find(function(token) token$value == "(", tokens) if (!is.null(lparen)) return(.rs.python.emptyCompletions()) return(.rs.python.getCompletionsKeys(lhs, rhs)) } # doesn't look like a dictionary; perform filesystem completion return(.rs.python.getCompletionsFiles(token$value)) } # try to guess if we're trying to autocomplete function arguments maybe_function <- cursor$peek(0 )$value %in% c("(", ",") || cursor$peek(-1)$value %in% c("(", ",") if (maybe_function) { offset <- cursor$cursorOffset() # try to find an opening bracket repeat { # skip matching brackets if (cursor$bwdToMatchingBracket()) { if (!cursor$moveToPreviousToken()) break next } # if we find an opening bracket, check to see if the token to the # left is something that is, or could produce, a function if (cursor$tokenValue() == "(" && cursor$moveToPreviousToken() && (cursor$tokenValue() == "]" || cursor$tokenType() %in% "identifier")) { # find code to be evaluated that will produce function endToken <- cursor$peek() cursor$moveToStartOfEvaluation() startToken <- cursor$peek() # extract the associated text start <- startToken$offset end <- endToken$offset + nchar(endToken$value) - 1 source <- substring(line, start, end) # get argument completions rhs <- if (token$type %in% "identifier") token$value else "" return(.rs.python.getCompletionsArguments(source, rhs)) } if (!cursor$moveToPreviousToken()) break } # if we got here, our attempts to find a function failed, so # go home and fall back to the default completion solution cursor$moveToOffset(offset) } # start looking backwards repeat { # skip matching brackets if (cursor$bwdToMatchingBracket()) { if (!cursor$moveToPreviousToken()) break next } # consume identifiers, strings, '.' if (cursor$tokenType() %in% c("string", "identifier") || cursor$tokenValue() %in% ".") { lastType <- cursor$tokenType() # if we can't move to the previous token, we must be at the # start of the token stream, so just consume from here if (!cursor$moveToPreviousToken()) break # if we moved on to a token of the same type, move back and break if (lastType == cursor$tokenType()) { cursor$moveToNextToken() break } next } # if this isn't a matched token, then move back up a single # token and break if (!cursor$moveToNextToken()) return(.rs.python.emptyCompletions()) break } source <- substring(line, cursor$tokenOffset()) .rs.python.getCompletionsMain(source, ctx) }) .rs.addFunction("python.isPython3", function() { config <- reticulate::py_config() grepl("^3", config$version) }) .rs.addFunction("python.listModules", function() { pkgutil <- reticulate::import("pkgutil", convert = FALSE) builtins <- reticulate::import_builtins(convert = FALSE) modules <- tryCatch( builtins$list(pkgutil$iter_modules()), error = identity ) if (inherits(modules, "error")) return(character()) # convert to R object and extract module names modules <- reticulate::py_to_r(modules) key <- if (.rs.python.isPython3()) "name" else 2L names <- vapply(modules, `[[`, key, FUN.VALUE = character(1)) sort(unique(names)) }) .rs.addFunction("python.inferObjectTypes", function(object, names) { vapply(names, function(name) { # attempt to grab attribute (note that this can fail if # the object as implemented a custom __getattr__ or similar) item <- .rs.tryCatch(reticulate::py_get_attr(object, name)) if (inherits(item, "error")) return(.rs.acCompletionTypes$UNKNOWN) # try to infer the completion type if (inherits(item, "python.builtin.module")) .rs.acCompletionTypes$ENVIRONMENT else if (.rs.reticulate.isFunction(item)) .rs.acCompletionTypes$FUNCTION else if (inherits(item, "pandas.core.frame.DataFrame")) .rs.acCompletionTypes$DATAFRAME else .rs.acCompletionTypes$UNKNOWN }, numeric(1)) }) .rs.addFunction("python.generateHtmlHelp", function(code) { Encoding(code) <- "UTF-8" # remove a '.html' suffix if present code <- sub("[.]html$", "", code) # check for pre-existing generated HTML dir <- file.path(tempdir(), "reticulate-python-help") if (!.rs.ensureDirectory(dir)) { warning("Failed to create Python help directory", call. = FALSE) return("") } stem <- utils::URLencode(code, reserved = TRUE) path <- file.path(dir, paste(stem, "html", sep = ".")) if (file.exists(path)) return(path) # no HTML file exists; attempt to generate it. try # to evaluate the Python code supplied to gain access # to the associated object. first attempt to just py_eval # it; if that fails, try the more generic pydoc resolver pydoc <- reticulate::import("pydoc", convert = TRUE) methods <- list( function() reticulate::py_eval(code), function() pydoc$resolve(code)[[1]] ) resolved <- NULL for (method in methods) { resolved <- .rs.tryCatch(method()) if (!inherits(resolved, "error")) break } if (inherits(resolved, "error")) { fmt <- "No Python documentation found for '%s'." warning(sprintf(fmt, code), call. = FALSE) return("") } # the text provided by the user is likely an alias for # the true definition location, so attempt to recover # that from the object. # # TODO: should we maintain our cache of help topics # within a session state directory, so they can be # reloaded when RStudio is restarted? module <- "<unknown>" if (reticulate::py_has_attr(resolved, "__module__")) module <- resolved[["__module__"]] name <- "<unknown>" if (reticulate::py_has_attr(resolved, "__name__")) name <- resolved[["__name__"]] # if we don't know the module, try to guess based on the class if (identical(module, "<unknown>") && reticulate::py_has_attr(resolved, "__class__")) { builtins <- reticulate::import_builtins(convert = TRUE) output <- builtins$repr(resolved[["__class__"]]) if (grepl("<type '(.*)'>", output)) { class <- gsub("<type '(.*)'>", "\\1", output) splat <- .rs.strsplit(class, ".", fixed = TRUE) module <- paste(head(splat, n = 1), collapse = ".") } } # we have a Python object: generate HTML help for it. we # monkey-patch our own HTMLDoc instance so that the heading # comes out a little more cleanly # # TODO: this might fit more naturally as a helper class # in the reticulate package # # NOTE: we explicitly use `py_run_string(..., local = FALSE)` to avoid # issues with reticulate 1.20 -- normally we'd just update the version # of reticulate on CRAN, but because R 4.1.0 was just published a number # of people will only be able to install the reticulate 1.20 binary and # so it behooves us to support that version for now reticulate::py_run_string(" # Create HTML documentation object def _rstudio_html_generator_(): import pydoc html = pydoc.HTMLDoc() # Override the heading function def _heading(title, fgcol, bgcol, extra = ''): return ''' <table width=\"100%%\" cellspacing=0 cellpadding=2 border=0 summary=\"heading\"> <tr><td><h2>%s</h2></td></tr> </table> ''' % (title) html.heading = _heading return html ", local = FALSE) # create html object, then remove generator function main <- reticulate::import_main(convert = TRUE) generator <- reticulate::py_to_r(reticulate::py_get_attr(main, "_rstudio_html_generator_")) html <- generator() reticulate::py_del_attr(main, "_rstudio_html_generator_") # generate page (handle numpy specially) page <- if (inherits(resolved, "numpy.ufunc")) { html$page(paste("numpy function", name), html$docroutine(resolved, name)) } else { pydoc <- reticulate::import("pydoc", convert = TRUE) html$page(pydoc$describe(resolved), html$document(resolved, name)) } # remove hard-coded background colors for rows page <- gsub("\\s?bgcolor=\"#[0-9a-fA-F]{6}\"", "", page, perl = TRUE) writeLines(page, con = path) path }) .rs.addFunction("python.extractCurrentExpression", function(line, offset) { # tokenize the line tokens <- .rs.python.tokenize(line, exclude = c("whitespace", "comment")) if (length(tokens) == 0) return("") # find the current token n <- length(tokens); index <- n while (index >= 1) { if (tokens[[index]]$offset <= offset) break index <- index - 1 } cursor <- .rs.python.tokenCursor(tokens) cursor$moveToOffset(index) # try to move to the start of an expression while (TRUE) { # move over matching brackets while (cursor$bwdToMatchingBracket()) if (!cursor$moveToPreviousToken()) break # bail if we hit an operator or a ';' -- these # are tokens that 'stop' a previous expression if (cursor$tokenType() %in% c("operator", "keyword") || cursor$tokenValue() %in% c(";", ",")) { cursor$moveToNextToken() break } # if we hit an opening bracket, check to see # if there's an identifier before it. if not, # this is defining a tuple or a list, and we # should bail if (cursor$tokenType() %in% "bracket" && cursor$cursorOffset() > 1) { peek <- tokens[[cursor$cursorOffset() - 1]] if (peek$type %in% c("operator", "bracket", "keyword") || peek$value %in% c(";", ",")) { cursor$moveToNextToken() break } } # move back a token if (!cursor$moveToPreviousToken()) break } startOffset <- cursor$tokenOffset() # now find the end of the expression while (TRUE) { # bail if we hit a '(' if (cursor$tokenValue() %in% c("(")) { cursor$moveToPreviousToken() break } # skip other brackets if (cursor$fwdToMatchingBracket()) if (!cursor$moveToNextToken()) break # bail if we hit an operator or a ';' -- these # are tokens that 'stop' a previous expression if (cursor$tokenType() %in% c("operator", "keyword") || cursor$tokenValue() %in% c(";", ",")) { cursor$moveToPreviousToken() break } # move up a token if (!cursor$moveToNextToken()) break } endOffset <- cursor$tokenOffset() + nchar(cursor$tokenValue()) - 1 # extract line of text providing the object to be looked at substring(line, startOffset, endOffset) }) .rs.addFunction("python.keywords", function() { keywords <- .rs.getVar("python.keywordList") if (length(keywords)) return(keywords) keyword <- reticulate::import("keyword", convert = TRUE) kwlist <- keyword$kwlist .rs.setVar("python.keywordList", kwlist) kwlist }) # $ title : chr "DataFrame" # $ signature : chr "DataFrame()" # $ description: chr "Two-dimensional size-mutable, potentially heterogeneous tabular data" .rs.addFunction("python.getHelp", function(topic, source) { object <- .rs.tryCatch(reticulate::py_eval(source, convert = FALSE)) if (inherits(object, "error")) return(NULL) handler <- reticulate:::help_completion_handler.python.builtin.object .rs.tryCatch(reticulate::py_suppress_warnings(handler(topic, object))) }) # $ args : chr [1:9] "cls" "path" "header" "sep" ... # $ arg_descriptions: Named chr [1:9] "cls" "path" "header" "sep" ... .rs.addFunction("python.getParameterHelp", function(source) { error <- list(args = character(), arg_descriptions = character()) object <- .rs.tryCatch(reticulate::py_eval(source, convert = FALSE)) if (inherits(object, "error")) return(error) # extract argument names using inspect (note that this can fail for # some Python function types; e.g. builtin Python functions) inspect <- reticulate::import("inspect", convert = TRUE) spec <- .rs.tryCatch(inspect$getargspec(object)) if (inherits(spec, "error")) return(error) args <- spec$args # attempt to scrape parameter documentation docs <- reticulate::py_get_attr(object, "__doc__", silent = TRUE) if (inherits(docs, "python.builtin.object")) docs <- reticulate::py_to_r(docs) if (is.null(docs)) docs <- "" lines <- strsplit(docs, "\n", fixed = TRUE)[[1]] arg_descriptions <- lapply(args, function(arg) { tryCatch({ # try to find the line where the parameter documentation starts pattern <- sprintf("^\\s*%s\\s*:", arg) index <- grep(pattern, lines) if (!length(index)) return("") index <- index[[1]] line <- lines[[index]] # split into argument name, initial part of description desc <- "" colon <- regexpr(":", line, fixed = TRUE) if (colon != -1L) desc <- substring(line, colon + 1) # now, look to see if the documentation spans multiple lines # consume lines of greater indent that the current indent <- regexpr("(?:\\S|$)", line) start <- end <- index + 1 while (TRUE) { if (regexpr("(?:\\S|$)", lines[[end]]) <= indent) break end <- end + 1 } if (start != end) desc <- c(desc, lines[start:(end - 1L)]) paste(gsub("^\\s*|\\s*$", "", desc), collapse = "\n") }, error = function(e) "") }) list( args = as.character(args), arg_descriptions = as.character(arg_descriptions) ) }) .rs.addFunction("python.sanitizeForCompletion", function(item) { if (.rs.startsWith(item, "[") && .rs.endsWith(item, "]")) "[]" else if (.rs.startsWith(item, "{") && .rs.endsWith(item, "}")) "{}" else if (.rs.startsWith(item, "(") && .rs.endsWith(item, ")")) "()" else item }) # > str(.rs.describeObject(globalenv(), "a")) # List of 10 # $ name : 'rs.scalar' chr "a" # $ type : 'rs.scalar' chr "numeric" # $ clazz : chr [1:2] "numeric" "double" # $ is_data : 'rs.scalar' logi FALSE # $ value : 'rs.scalar' chr "1" # $ description : 'rs.scalar' chr " num 1" # $ size : 'rs.scalar' num 56 # $ length : 'rs.scalar' int 1 # $ contents : list() # $ contents_deferred: 'rs.scalar' logi FALSE .rs.addFunction("reticulate.describeObject", function(name, parent) { object <- if (inherits(parent, "python.builtin.dict")) reticulate::py_get_item(parent, name) else if (inherits(parent, "python.builtin.object")) reticulate::py_get_attr(parent, name) else get(name, envir = parent) # is this a null pointer? if so, handle that up-front if (reticulate:::py_is_null_xptr(object)) { result <- list( name = .rs.scalar(name), type = .rs.scalar("<unknown>"), clazz = "<unknown>", is_data = .rs.scalar(TRUE), value = .rs.scalar("<Null pointer>"), description = .rs.scalar("<Null pointer>"), size = .rs.scalar(0L), length = .rs.scalar(0L), contents = list(), contents_deferred = .rs.scalar(FALSE) ) return(result) } # is this object 'data'? consider non-callable, non-module objects as data isData <- !( grepl("^__.*__$", name) || reticulate:::py_is_callable(object) || reticulate:::py_is_module(object) ) # NOTE: there isn't really a distinction between an objects "type" # and an objects "class" in Python 3; whereas R objects might have # some internal type and multiple (S3, S4, R6) classes, depending # on what form of OOP is used for dispatch # get object type, value type <- if (.rs.reticulate.isFunction(object)) "function" else .rs.reticulate.describeObjectType(object) value <- .rs.reticulate.describeObjectValue(object) # get object size sys <- reticulate::import("sys") size <- sys$getsizeof(object) # get object length (note: not all objects in Python have a length) length <- .rs.reticulate.describeObjectLength(object) # get object summary when appropriate contents <- .rs.reticulate.describeObjectContents(object) list( name = .rs.scalar(name), type = .rs.scalar(type), clazz = type, is_data = .rs.scalar(isData), value = .rs.scalar(value), description = .rs.scalar(value), size = .rs.scalar(size), length = .rs.scalar(length), contents = contents, contents_deferred = .rs.scalar(FALSE) ) }) .rs.addFunction("reticulate.describeObjectType", function(object) { builtins <- reticulate::import_builtins(convert = TRUE) builtins$type(object)$`__name__` }) .rs.addFunction("reticulate.describeObjectValue", function(object) { if (inherits(object, "pandas.core.frame.DataFrame")) { builtins <- reticulate::import_builtins(convert = TRUE) rows <- builtins$len(object) columns <- builtins$len(object$columns) fmt <- "DataFrame: [%i rows x %i columns]" sprintf(fmt, rows, columns) } else if (inherits(object, "__main__.R")) { "[R interface object]" } else { pprint <- reticulate::import("pprint", convert = TRUE) printer <- pprint$PrettyPrinter( indent = 1L, width = as.integer(getOption("width")), depth = 1L ) formatted <- printer$pformat(object) .rs.truncate(formatted) } }) .rs.addFunction("reticulate.describeObjectLength", function(object) { # this function is overwritten in .rs.registerPackageLoadHook if # reticulate version >= 1.23 builtins <- reticulate::import_builtins(convert = TRUE) tryCatch( builtins$len(object), error = function(e) -1L ) }) .rs.addFunction("reticulate.describeObjectContents", function(object) { tryCatch( .rs.reticulate.describeObjectContentsImpl(object), error = warning ) }) .rs.addFunction("reticulate.describeObjectContentsImpl", function(object) { if (inherits(object, "pandas.core.frame.DataFrame")) { text <- reticulate::py_to_r(object$to_string(max_rows = 150L, show_dimensions = FALSE)) strsplit(text, "\n", fixed = TRUE)[[1]] } else { list() } }) .rs.addFunction("reticulate.resolveModule", function(module) { # return module objects as-is if (inherits(module, "python.builtin.object")) return(module) # resolve modules by name otherwise if (module %in% c("main", "__main__")) reticulate::import_main(convert = FALSE) else if (module %in% c("builtins", "__builtins__")) reticulate::import_builtins(convert = FALSE) else reticulate::import(module, convert = FALSE) }) # "result": { # "language": "r", # "environment_monitoring": true, # "environment_list": [], # "context_depth": 0, # "call_frames": [], # "function_name": "", # "environment_name": ".GlobalEnv", # "environment_is_local": false, # "use_provided_source": false, # "function_code": "" # } .rs.addFunction("reticulate.environmentState", function(module) { # resolve the requested module module <- .rs.reticulate.resolveModule(module) # update detect changes cache .rs.reticulate.detectChanges(module, cacheOnly = TRUE) # list objects within the requested module builtins <- reticulate::import_builtins() objects <- builtins$dir(module) # don't include double-under (dunder; e.g __foo__) objects from main name <- as.character(reticulate::py_get_attr(module, "__name__", silent = TRUE)) if (identical(name, "__main__")) objects <- grep("^__.*__$", objects, perl = TRUE, value = TRUE, invert = TRUE) # obtain a description of each Python object, using the already-understood # format used for R objects descriptions <- lapply(objects, .rs.reticulate.describeObject, parent = module) # try to get the module name (if any) name <- tryCatch( as.character(reticulate::py_get_attr(module, "__name__")), error = function(e) as.character(module) ) list( language = .rs.scalar("Python"), environment_monitoring = .rs.scalar(TRUE), environment_list = descriptions, # included for compatibility with existing R environment list code context_depth = .rs.scalar(0L), call_frames = list(), function_name = .rs.scalar(""), environment_name = .rs.scalar(name), environment_is_local = .rs.scalar(FALSE), use_provided_source = .rs.scalar(FALSE), function_code = .rs.scalar("") ) }) # > str(.rs.environmentList(globalenv())) # List of 9 # $ :List of 3 # ..$ name : 'rs.scalar' chr "R_GlobalEnv" # ..$ frame: 'rs.scalar' int 0 # ..$ local: 'rs.scalar' logi FALSE # < ... > .rs.addFunction("reticulate.listLoadedModules", function(includeBuiltins = FALSE) { if (!requireNamespace("reticulate", quietly = TRUE)) return(list()) # read available loaded modules stack <- .rs.listBuilder() globals <- reticulate::py_run_string("globals()", convert = FALSE) reticulate::iterate(globals, function(variable) { object <- reticulate::py_get_item(globals, variable, silent = TRUE) if (!inherits(object, "python.builtin.module")) return(FALSE) # take module name rather than binding name discovered in globals name <- .rs.nullCoalesce( reticulate::py_get_attr(object, "__name__", silent = TRUE), variable ) # ignore builtins if requested name <- as.character(name) if (!includeBuiltins && name %in% c("builtins", "__builtins__")) return(FALSE) stack$append(name) TRUE }) # retrieve modules modules <- stack$data() # ensure main module is always included first modules <- c("__main__", setdiff(modules, "__main__")) # return in format suitable for environment pane lapply(modules, function(module) { list( name = .rs.scalar(module), frame = .rs.scalar(0L), local = .rs.scalar(FALSE) ) }) }) .rs.addFunction("reticulate.detectChanges", function(moduleName, cacheOnly = FALSE) { # resolve module module <- .rs.reticulate.resolveModule(moduleName) # list objects within this module newObjects <- reticulate::py_get_attr(module, "__dict__") # retrieve previously-cached objects in this module oldObjects <- .rs.nullCoalesce( .rs.getVar("reticulate.monitoredModuleObjects"), newObjects ) # create copy of dictionary copy <- reticulate::import("copy", convert = FALSE) newObjects <- copy$copy(newObjects) # update cached globals .rs.setVar("reticulate.monitoredModuleObjects", newObjects) # if we're only updating the cache, bail now if (cacheOnly) return() # collect all vars vars <- sort(union(names(oldObjects), names(newObjects))) # iterate and check for changes changedObjects <- .rs.listBuilder() removedObjects <- .rs.listBuilder() for (var in vars) { old <- reticulate::py_get_item(oldObjects, var, silent = TRUE) new <- reticulate::py_get_item(newObjects, var, silent = TRUE) if (is.null(old) && is.null(new)) { # shouldn't happen, but this implies no object before or after } else if (is.null(old)) { # an object was added (treat as 'changed') changedObjects$append(.rs.reticulate.describeObject(var, newObjects)) } else if (is.null(new)) { # an object was removed removedObjects$append(var) } else if (!.rs.reticulate.objectsEqual(old, new)) { # an object was changed changedObjects$append(.rs.reticulate.describeObject(var, newObjects)) } } # bail if nothing to report if (changedObjects$empty() && removedObjects$empty()) return() # emit change event .rs.enqueClientEvent("environment_changed", list( changed = changedObjects$data(), removed = as.character(removedObjects$data()) )) }) .rs.addFunction("reticulate.isPythonInitialized", function() { "reticulate" %in% loadedNamespaces() && reticulate::py_available(initialize = FALSE) }) .rs.addFunction("reticulate.objectsEqual", function(lhs, rhs) { # compare Pandas DataFrame objects with special method pandas <- inherits(lhs, "pandas.core.frame.DataFrame") && inherits(rhs, "pandas.core.frame.DataFrame") if (pandas) { pandas <- reticulate::import("pandas", convert = TRUE) return(pandas$DataFrame$equals(lhs, rhs)) } # default comparison method # otherwise, fall back to default '!=' comparison method tryCatch( reticulate:::py_compare(lhs, rhs, "=="), error = function(e) FALSE ) }) .rs.addFunction("reticulate.isFunction", function(object) { inherits(object, c( "python.builtin.builtin_function_or_method", "python.builtin.function", "python.builtin.instancemethod" )) }) .rs.addFunction("reticulate.viewHook", function(object, name) { # TODO: assign complex expressions to temporary variable before view reticulate:::disable_conversion_scope(object) # convert Pandas DataFrames to R data.frames for now # (consider adapting data viewer to arbitrary tabular data in future?) if (inherits(object, "pandas.core.frame.DataFrame")) { # create object object <- reticulate::py_to_r(object) # assign as 'name', then view that assign(name, object, envir = environment()) eval(call("View", as.name(name)), envir = environment()) } else { # create dummy environment for this object envir <- new.env(parent = emptyenv()) assign(name, object, envir = envir) # view object .rs.explorer.viewObject( object = object, title = name, envir = envir ) } }) .rs.addFunction("reticulate.isStructSeq", function(object) { all( reticulate::py_has_attr(object, "n_sequence_fields"), reticulate::py_has_attr(object, "n_fields"), reticulate::py_has_attr(object, "n_unnamed_fields") ) }) .rs.addFunction("reticulate.listAttributes", function(object, includeDunderMethods = TRUE) { attributes <- reticulate::py_list_attributes(object) # remove dunder methods if requested if (!includeDunderMethods) attributes <- grep("^__", attributes, value = TRUE, invert = TRUE) # sort so that dunder methods are shown last indices <- order( .rs.startsWith(attributes, "__"), .rs.startsWith(attributes, "_") ) # return sorted attributes attributes[indices] }) .rs.addFunction("reticulate.explorerCache", function() { key <- "reticulate.explorerCacheDictionary" if (!.rs.hasVar(key)) .rs.setVar(key, reticulate::dict()) .rs.getVar(key) }) .rs.addFunction("reticulate.usePython", function(python) { # sanity check value of param ok <- is.character(python) && length(python) == 1 && file.exists(python) if (!ok) return(FALSE) # no-op if Python has already been initialized if (reticulate::py_available(initialize = FALSE)) return(FALSE) # if we're working in an renv project that is already # managing the default version of python, then do nothing renvPython <- Sys.getenv("RENV_PYTHON", unset = NA) if (!is.na(renvPython)) return(FALSE) # ok, request use of Python reticulate::use_python(python, required = TRUE) }) # hook to be invoked when the Python session has been initialized by reticulate options(reticulate.initialized = function() { # clear hook options(reticulate.initialized = NULL) # call R hook .rs.reticulate.onPythonInitialized() # notify client that Python is being initialized .rs.reticulate.enqueueClientEvent( .rs.reticulateEvents$PYTHON_INITIALIZED, list() ) # invoke lower-level callbacks .Call("rs_reticulateInitialized", PACKAGE = "(embedding)") }) options(reticulate.repl.initialize = function() { .rs.reticulate.replInitialize() }) options(reticulate.repl.hook = function(buffer, contents, trimmed) { .rs.reticulate.replHook(buffer, contents, trimmed) }) options(reticulate.repl.busy = function(busy) { .rs.reticulate.replBusy(busy) }) options(reticulate.repl.teardown = function() { .rs.reticulate.replTeardown() }) # Attempts to infer the current Python interpreter used by reticulate .rs.addFunction("inferReticulatePython", function() { # Use existing RETICULATE_PYTHON if set python <- Sys.getenv("RETICULATE_PYTHON", unset = NA) if (!is.na(python)) return(python) # if reticulate is already loaded, check and see if it's already # configured to use a particular copy of Python loaded <- "reticulate" %in% loadedNamespaces() && reticulate::py_available(initialize = FALSE) if (loaded) { config <- reticulate::py_config() return(config$python) } # if the user has configured RStudio to use a particular version # of Python, then use that python <- .rs.readUiPref("python_path") if (!is.null(python) && !identical(python, "")) return(path.expand(python)) # if reticulate is installed, then try to load it in a child process and # ask what version of Python it would choose to bind to. # Use existing RETICULATE_PYTHON_FALLBACK if set python <- Sys.getenv("RETICULATE_PYTHON_FALLBACK", unset = NA) if (!is.na(python)) return(python) if (.rs.isPackageInstalled("reticulate")) { # avoid miniconda prompts prev_miniconda <- Sys.getenv("RETICULATE_MINICONDA_ENABLED") Sys.setenv(RETICULATE_MINICONDA_ENABLED = "FALSE") # Then perform a Python version scan/discovery # do this in child process so we don't pollute the namespace # callback must be passed as a func within a func to avoid loading reticulate py_config <- NULL tryCatch({ py_config <- .rs.executeFunctionInChildProcess( callback = function() { suppressWarnings(tryCatch(reticulate::py_discover_config(), error = function(e) NULL)) }) }, finally = { Sys.setenv(RETICULATE_MINICONDA_ENABLED = prev_miniconda) }) # Return a Python binary if we found one if (!is.null(py_config) && !is.null(py_config$python)) { return(py_config$python) } } # we didn't find any indication of the python version "" })