Skip to content

Commit

Permalink
Merge pull request #1681 from rstudio/fix/positron-repl-preserve-main
Browse files Browse the repository at this point in the history
Positron REPL: restore `__main__` members after ipykernel launch
  • Loading branch information
t-kalinowski authored Oct 9, 2024
2 parents 5fd3772 + c08ee61 commit 0c2ff33
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 17 deletions.
27 changes: 26 additions & 1 deletion R/thread.R
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,32 @@ py_allow_threads <- function(allow = TRUE) {
py_run_file_on_thread <- function(file, ..., args = NULL) {
if (!is.null(args))
args <- as.list(as.character(args))
import("rpytools.run")$`_launch_lsp_server_on_thread`(file, args)

# TODO: we should have a dedicated entry point in reticulate for this.
# Needs to be updated in ark and positron.
launching_lsp <- (basename(file) == 'positron_language_server.py' &&
is_positron() &&
basename(dirname(file)) == "positron")

if (launching_lsp) {
main_dict <- py_eval("__import__('__main__').__dict__.copy()", FALSE)
py_get_attr(main_dict, "pop")("__annotations__")
}

import("rpytools.run")$run_file_on_thread(file, args, ...)

if (launching_lsp) {

PositronIPKernelApp <- import("positron_ipykernel.positron_ipkernel")$PositronIPKernelApp
for(i in 1:40) { # Positron timeout is 20 seconds
if (PositronIPKernelApp$initialized()) break
Sys.sleep(.5)
}
Sys.sleep(1)

py_eval("__import__('__main__').__dict__.update", FALSE)(main_dict)
}
invisible()
}

## used in Positron:
Expand Down
19 changes: 13 additions & 6 deletions inst/python/rpytools/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __exit__(self, *_):
if self.argv is not None:
# restore sys.argv if it's unmodified from what we set it to.
# otherwise, leave it as-is.
patched_argv = [self.path] + list(self.args)
patched_argv = [self.path] + list(self.argv)
if sys.argv == patched_argv:
sys.argv = self._orig_sys_argv

Expand All @@ -50,12 +50,19 @@ def _launch_lsp_server_on_thread(path, args):
return run_file_on_thread(path, args)



def run_file_on_thread(path, args=None):
def run_file_on_thread(path, argv=None, init_globals=None, run_name="__main__"):
# for now, leave sys.argv and sys.path permanently modified.
# Later, revisit if it's desirable/safe to restore after the initial
# lsp event loop startup.
RunMainScriptContext(path, args).__enter__()
import _thread

_thread.start_new_thread(run_file, (path,))
from runpy import run_path

RunMainScriptContext(path, argv).__enter__()
_thread.start_new_thread(
run_path,
(path,),
{
"run_name": run_name,
"init_globals": init_globals,
},
)
21 changes: 11 additions & 10 deletions tests/testthat/test-python-threads.R
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ def write_to_file_from_thread(path, lines):
test_that("Python calls into R from a background thread are evaluated", {

x <- 0L
py$r_func <- function() x <<- x+1
on.exit(py_del_attr(py, "r_func"), add = TRUE)
r_func <- function() x <<- x+1
py_file <- withr::local_tempfile(lines = "r_func()", fileext = ".py")

reticulate:::py_run_file_on_thread(py_file)
reticulate:::py_run_file_on_thread(
py_file,
init_globals = list(r_func = r_func)
)

# Simulate the main R thread doing non-Python work (e.g., sleeping)
for(i in 1:10) {
Expand All @@ -62,21 +64,20 @@ test_that("Python calls into R from a background thread are evaluated", {

test_that("Errors from background threads calling into main thread are handled", {

py$signal_r_error <- function() stop("foo-bar-baz")
on.exit(py_del_attr(py, "signal_r_error"), add = TRUE)
signal_r_error <- function() stop("foo-bar-baz")

# when testing, `r` in Python resolves `getOption("rlang_trace_top_env"), not
# the R globalenv(). Avoid using it to communicate.
val <- NULL
py$set_val <- function(v) val <<- v
on.exit(py_del_attr(py, "set_val"), add = TRUE)
set_val <- function(v) val <<- v

py_file <- withr::local_tempfile(lines = "
try: signal_r_error()
except Exception as e: set_val(e.args[0])
", fileext = ".py")

reticulate:::py_run_file_on_thread(py_file)
reticulate:::py_run_file_on_thread(py_file, init_globals = list(
signal_r_error = signal_r_error,
set_val = set_val
))

# Simulate the main R thread doing non-Python work (e.g., sleeping)
for(i in 1:10) {
Expand Down

0 comments on commit 0c2ff33

Please sign in to comment.