Skip to content

Conversation

@jennybc
Copy link
Member

@jennybc jennybc commented Apr 3, 2023

This is functionally important for gargle, because cli_menu() is ultimately implemented on top of readline(), not utils::menu(). And Jupyter has shimmed readline() to work even though, at the time of writing, base::interactive() returns FALSE in Jupyter.

Therefore this PR makes gargle's interactive choices work in Jupyter notebooks, which notably covers Google Colab.

This is part (but not all) of making user auth "just work" on Colab (#140).

I have adapted cli_menu() from https://github.com/rstudio/rsconnect/blob/main/R/utils-cli.R. I think most of my changes might be generally positive and could be considered when/if something like this goes into cli itself (r-lib/cli#228). I also added tests.

@hadley @gaborcsardi I don't really need feedback on this (though it's welcome). This is more of an FYI request for review, to continue the discussion around r-lib/cli#228. I'll make a few comments inline.


Links to the Jupyter + readline() story:

IRkernel/IRkernel#452

https://github.com/IRkernel/IRkernel/blob/1eddb304b246c14b62949abd946e8d4ca5080d25/R/execution.r#L131-L137

https://github.com/IRkernel/IRkernel/blob/1eddb304b246c14b62949abd946e8d4ca5080d25/R/execution.r#L271-272

prompt,
choices,
not_interactive = choices,
exit = integer(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I called this exit (instead of quit) to better match the typical vocabulary here (e.g. in the docs for utils::menu()) and to avoid any confusion with actuallly quitting R.

R/utils-ui.R Outdated
Comment on lines 300 to 301
# guard against invalid mocked input
local_input <- getOption("cli_input", character())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to get into an infinite loop without this. Of course it only happens when one enters invalid mocked input, but still seemed worth accounting for.

R/utils-ui.R Outdated
Comment on lines 308 to 314
if (length(local_input) > 0) {
cli::cli_abort(
c(x = "Internal error: mocked input is invalid."),
.envir = .envir,
call = error_call
)
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second part of "avoiding an infinite loop".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do that all in one place? Or extract it out into a helper?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I consolidated things, just to make it a bit neater, at least.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it error if it’s length greater than zero? The original intent was to be able to supply multiple values that were used in turn.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Over in rsconnect, if I add this test, it causes the test suite to just hang:

test_that("cli_menu() works with multiple inputs", {
  simulate_user_input(3)

  expect_snapshot(
    cli_menu(
      "Let's talk",
      "Are you OK?",
      choices = c("Yes", "No")
    )
  )
})

Screenshot 2023-04-04 at 9 24 46 PM

I guess I haven't found an organic need to supply multiple values. But I have managed to supply an invalid one. So it seems nice for development purposes to handle that case.

I can try to think about how to preserve both.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you convinced that multiple mocked inputs is important?

It feels like we could just make multiple calls to simulate_user_input() (rsconnect) or local_user_input() (here in gargle) in such a test and let the functions be simpler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize I can pursue the simplification that's most natural here. I'm mostly talking about what we might eventually want in cli or in some standalone file for general use.

selected <- as.integer(selected)
if (selected %in% c(0, exit)) {
if (is_testing()) {
cli::cli_abort("Exiting...", call = NULL)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exiting not quitting

if (is_testing()) {
cli::cli_abort("Exiting...", call = NULL)
} else {
cli::cli_alert_danger("Exiting...")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

} else {
cli::cli_alert_danger("Exiting...")
# simulate user pressing Ctrl + C
invokeRestart("abort")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omitted the rogue cnd that the rsconnect version has here (presumably a think-o).

}
}

local_user_input <- function(x, env = caller_env()) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with a local_*()-style name

expect_snapshot(cli::cli_bullets(bulletize(letters[1:8], n_fudge = 3)))
})

# menu(), but based on readline() + cli and mockable ---------------------------
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This basically covers the cli_menu() stuff 100%, except the lines you can only hit in interactive use.

@jennybc jennybc requested review from gaborcsardi and hadley April 3, 2023 23:53
@jennybc jennybc marked this pull request as ready for review April 3, 2023 23:54
Copy link
Member

@gaborcsardi gaborcsardi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member

@hadley hadley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like some nice improvements!

R/oauth-cache.R Outdated
))
choice <- utils::menu(emails)
c(
"Select a pre-authorised account or enter '0' to obtain a new token.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an explicit bullet like "Obtain a new token" instead of using 0? (I don't think this code will work now because cli_menu() will exit to the top-level if the user enters 0)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this code will work now because cli_menu() will exit to the top-level if the user enters 0

Yes, this is true. I will deal with that 🤔

R/utils-ui.R Outdated
Comment on lines 308 to 314
if (length(local_input) > 0) {
cli::cli_abort(
c(x = "Internal error: mocked input is invalid."),
.envir = .envir,
call = error_call
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do that all in one place? Or extract it out into a helper?

R/oauth-cache.R Outdated
"Select a pre-authorised account or enter '0' to obtain a new token.",
"Press Esc/Ctrl + C to cancel."
"Enter '1' to start a new auth process or select a pre-authorized account.",
"Press Esc or Ctrl + C to cancel."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could omit this line now that 0 has its typical behaviour?

withr::local_options(
rlang_interactive = TRUE,
# trailing 0 prevents infinite loop if x only contains invalid choices
cli_input = c(x, "0"),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appending "0" here means you can't get stuck in an infinite loop, which means I can also delete the fussy check inside the repeat { ... } in cli_menu().

Maybe this makes both of us happy? 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or that could go in cli_readline()? I think I forgot how these functions were factored.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's an interesting idea. I've mentally moved on to other projects in gargle, but I think this conversation and the state of these functions here can still inform whatever a more official version looks like.

@jennybc
Copy link
Member Author

jennybc commented Apr 5, 2023

I found a solution I like to the infinite loop problem, which is to append "0" to any mocked user input. And this retains the ability to provide multiple inputs.

The original intent was to be able to supply multiple values that were used in turn.

I also added a test for this behaviour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants