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

rsconnect::writeManifest for Quarto content #572

Merged
merged 26 commits into from
Apr 15, 2022

Conversation

toph-allen
Copy link
Contributor

@toph-allen toph-allen commented Mar 18, 2022

This draft PR adds Quarto support to rsconnect::writeManifest. I think that some of the compatibility affordances should also be added to rsconnect::deployApp, for consistency's sake.

Intent

Add support for Quarto content to rsconnect::writeManifest() and expands support in rsconnect::deployApp(). It adds support for Quarto documents with the following runtimes:

  • Markdown-only
  • Using R
  • Using R and Python (via reticulate)
  • Using R and Python (both knitr and jupyter engines)

It also adds partial support for Python-only Quarto projects, using the Jupyter engine. However, these manifests contain packages objects listing R packages, and they should not.

Approach

To avoid adding a dependency on the Quarto package, we call quarto inspect ourselves in a function named quartoInspect(). We only attempt this if the user supplies a quarto parameter containing the full path to a Quarto executable.

rsconnect::deployApp() continues to support deployment via quarto::quarto_publish_app() via quarto_version and quarto_engines fields on metadata objects. This information is only used if the quarto path is not supplied.

Additions and changes to internal functions

  • inferQuartoInfo(appDir, appPrimaryDoc, quarto, metadata = NULL)
    • Attempts to gather Quarto info from quartoInspect().
    • If Quarto info is NULL and metadata was passed, attempts to find Quarto info in metadata.
    • Returns extracted info or NULL.
    • metadata defaults to NULL because metadata doesn't exist in some locations this function is called; it exists for quarto-r compatibility in deployApp().
  • getQuartoManifestDetails(inspect = list(), metadata = list())
    • Given either inspect or metadata, will return Quarto info ready for insertion into manifest.
  • quartoInspect(appDir = NULL, appPrimaryDoc = null, quarto = NULL)
    • If quarto is not NULL, attempts to call quarto inspect first on appDir, and if that fails (which it will for single documents), on appDir/appPrimaryDoc.
    • Returns parsed inspect results or NULL.

Changes to main deployment functions

  • writeManifest()
    • Gained a quarto parameter.
    • Calls inferQuartoInfo() early on. Results are assigned to quartoInfo object, following pyInfo. This is passed into other infer functions, and then to createAppManifest.
    • If no contentCategory is provided, and quartoInfo is not NULL, sets contentCategory to "site", following current treatment of Quarto content.
  • deployApp()
    • Gains a quarto parameter.
    • Passes the quarto parameter, along with metadata, to bundleApp()
  • bundleApp()
    • Gains quarto and metadata parameters.
    • Calls inferQuartoInfo() etc. exactly like writeManifest().

Regarding the deployApp() => bundleApp() call chain, I decided to have the inferQuartoInfo() call occur within bundleApp() because in many ways, that function is analogous to writeManifest(), and I thought that it would be much easier to maintain the shared parts of their code if replicated functionality looked the same, between them, rather than being spread between deployApp() and bundleApp(). But because deployApp() needs to support determining Quarto info from metadata, bundleApp() had to gain it as an optional argument.

In bundle.R, the variable name quarto was used interchangeably to refer to a Quarto path or to the extracted Quarto info. It now uses the name quartoInfo anywhere that the object must be the quartoInfo list, and uses quarto anywhere it must be a path, and left that name in places where the code doesn't care (e.g. inferAppMode() just checks that the object is not NULL).

Documentation

  • Some documentation seems to have reflowed at some point, perhaps when saving in the IDE. I'm not sure.
  • Added documentation for the quarto parameter.
  • Moved the on.failure parameter in deployApp to match its position in the function documentation.

Testing & QA

  • Added tests for quartoInspect(), inferQuartoInfo(), and writeManifest() on Quarto content.
  • Added new test bundles for writeManifest() tests, using cleaned up items from connect-content: quarto-doc-none, quarto-proj-r-shiny, quarto-website-py, quarto-website-r-py, quarto-website-r.

The most direct way to validate these changes for a piece of content is to run writeManifest() on a piece of content and examine the generated manifest. The changes in this PR only affect the output when the full path to a Quarto binary is passed to the quarto parameter (which doesn't exist in the version of rsconnect on main). The easiest way to pass that binary requires you to have the quarto R package installed. You'll also want the latest rmarkdown and shiny installed. I don't think there will be any other dependencies — I've omitted the extra-dependency packages from the list below.

remotes::install_github("quarto-dev/quarto-r")
remotes::install_github("rstudio/rmarkdown")
remotes::install_github("rstudio/shiny")
# One piece of content also requires flexdashboard
install.packages("flexdashboard")

If you see any other dependency errors from the content, you should just "install.packages()" them.

Inspecting writeManifest changes

If you work within the connect-content repo, you can take advantage of git to directly diff the changes. The manifests of Quarto packages in connect-content were generated with the IDE, so they aren't a direct comparison to rsconnect main.

I've found that the easiest way to make direct comparisons is to work on a new local branch of connect-content Then, you can:

  1. Install the main version of rsconnect. Check out the main branch; open the rsconnect.Rroj project in the IDE; click "Install" under the "Build" tab in the top-right pane.
  2. In the connect-content repo, run rsconnect::writeManifest() on all the content you want to check. Commit those changes to your local branch.

For example, running R in the bundles directory of connect-content:

rsconnect::writeManifest(appDir = "quarto-proj-r-shiny")

Run this command again for the appDir of each content bundle you want to examine. Commit all of the changes.

  1. Install the PR branch of rsconnect. Check out the main branch; open the rsconnect.Rroj project in the IDE; click "Install" under the "Build" tab in the top-right pane. After you've reinstalled the dev version, you should quit and reopen all your R sessions.
  2. In the connect-content repo, in your new R session, run rsconnect::writeManifest() on all the content you want to check. Don't commit the changes.

To examine the Quarto-mode changes, you'd run:

rsconnect::writeManifest(appDir = "quarto-proj-r-shiny", quarto = quarto::quarto_path())

You could also run without the Quarto argument to ensure that nothing has changed.

  1. You can now use git status and git diff to see what files have changed between the versions.
❯ git diff
diff --git a/bundles/quarto-proj-r-shiny/manifest.json b/bundles/quarto-proj-r-shiny/manifest.json
index e87ffb7..ed96084 100644
--- a/bundles/quarto-proj-r-shiny/manifest.json
+++ b/bundles/quarto-proj-r-shiny/manifest.json
@@ -3,12 +3,16 @@
   "locale": "en_US",
   "platform": "3.6.3",
   "metadata": {
-    "appmode": "rmd-shiny",
+    "appmode": "quarto-shiny",
     "primary_rmd": "quarto-proj-r-shiny.qmd",
     "primary_html": null,
     "content_category": null,
     "has_parameters": false
   },
+  "quarto": {
+    "version": "0.9.80",
+    "engines": ["knitr"]
+  },
   "packages": {
     "R6": {
       "Source": "CRAN",

If you run R from the bundles directory, you should be able to just run rsconnect for all the bundles you want to check. I've included code snippets below.

What To Look For

Compared to the main version, you should see the following changes in the writeManifest tests:

  • When called with a quarto argument:
    • Quarto bundles that used to get rmd-static as their appmode should get quarto-static as their appmode.
    • Quarto bundles that used to get rmd-shiny as their appmode should get quarto-shiny as their appmode.
    • Quarto content should see a top-level quarto object in with version and engines.
  • When called without a quarto argument, no changes should occur.
  • On non-Quarto content, no changes should occur. You don't need to test on all content. I think a few R Markdown and Shiny RMD content items should be enough.

For the main version, run:

# Quarto content
rsconnect::writeManifest("quarto-book-none")
rsconnect::writeManifest("quarto-doc-none")
rsconnect::writeManifest("quarto-multidoc-proj-none")
rsconnect::writeManifest("quarto-proj-none")
rsconnect::writeManifest("quarto-proj-none-ojs")
rsconnect::writeManifest("quarto-proj-py")
rsconnect::writeManifest("quarto-proj-r")
rsconnect::writeManifest("quarto-proj-r-py")
rsconnect::writeManifest("quarto-proj-r-shiny")
rsconnect::writeManifest("quarto-website-none")
rsconnect::writeManifest("quarto-website-py")
rsconnect::writeManifest("quarto-website-r")
rsconnect::writeManifest("quarto-website-r-py")
rsconnect::writeManifest("quarto-website-r-py-separate-files")

# Non-quarto content

rsconnect::writeManifest("rmd-shiny-runtime-shinyrmd-complete")
rsconnect::writeManifest("rmd-shiny-runtime-shinyrmd-simple")
rsconnect::writeManifest("rmd-site")
rsconnect::writeManifest("shinyapp")

For this branch, run:

# Quarto content
rsconnect::writeManifest("quarto-book-none", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-doc-none", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-multidoc-proj-none", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-proj-none", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-proj-none-ojs", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-proj-py", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-proj-r", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-proj-r-py", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-proj-r-shiny", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-website-none", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-website-py", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-website-r", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-website-r-py", quarto = quarto::quarto_path())
rsconnect::writeManifest("quarto-website-r-py-separate-files", quarto = quarto::quarto_path())

# Non-quarto content

rsconnect::writeManifest("rmd-shiny-runtime-shinyrmd-complete")
rsconnect::writeManifest("rmd-shiny-runtime-shinyrmd-simple")
rsconnect::writeManifest("rmd-site")
rsconnect::writeManifest("shinyapp")

Testing `deployApp()

The same code changes are used in deployApp. You can run the following code snippet to deploy the listed content to Connect.

Set your working directory to connect-content/bundles, and open a fresh R session. Make sure the dev branch of rsconnect is installed to your R library.

# Assuming you have a server named "localhost" configured in the IDE / in the rsconnect package
server <- "localhost"

# Quarto content
rsconnect::deployApp("quarto-book-none", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-doc-none", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-multidoc-proj-none", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-proj-none", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-proj-none-ojs", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-proj-py", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-proj-r", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-proj-r-py", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-proj-r-shiny", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-website-none", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-website-py", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-website-r", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-website-r-py", quarto = quarto::quarto_path(), server = server)
rsconnect::deployApp("quarto-website-r-py-separate-files", quarto = quarto::quarto_path(), server = server)

# Non-quarto content

rsconnect::deployApp("rmd-shiny-runtime-shinyrmd-complete", server = server)
rsconnect::deployApp("rmd-shiny-runtime-shinyrmd-simple", server = server)
rsconnect::deployApp("rmd-site", server = server)
rsconnect::deployApp("shinyapp", server = server)

Quarto content should show "Quarto *" under "Content Type".

Screen Shot 2022-04-04 at 03 36 00 PM@2x

Other things to test

  • deployApp() also uses the changed workflow. Deploying Quarto and non-quarto content should work the same, e.g. deployApp(appDir = "quarto-proj-r-shiny", quarto = quarto::quarto_path().
  • quarto::quarto_publish_app() calls deployApp to deploy Quarto content. Instead of passing in a quarto path, it passes in a metadata object. To test that this method of deployment still works, you can use quarto_publish_app() to publish some Quarto content (ensuring that it runs correctly on the server) or pass in a metadata object yourself.

This code assumes that you have set up a local Connect dev instance as the server localhost in R. You can do this in the RStudio IDE, under the "Publishing" section in preferences.

metadata <- list(
  "quarto_version" = "1.1.0.9000",
  "quarto_engines" = I(c("knitr"))
)
rsconnect::deployApp("quarto-proj-r", server = "localhost", metadata = metadata)

Appendix: Performance experiments

One of the reasons we gated quarto inspect behind the quarto parameter is its performance impact: about little over half a second on average.

  • In the plumber bundle, running vanilla rsconnect::writeManifest after initial package load takes about 0.45 seconds. With quarto inspect it takes about 1.05 seconds. Running quarto inspect at the command line takes about 0.58 seconds, so this tracks.
  • In the quarto-website-r-py bundle, running rsconnect::writeManifest with a metadata option takes about 1.55 seconds (skips the quarto inspect codepath). rsconnect::writeManifest with quarto inspect takes about 2.1 seconds. Again, a penalty of about 0.6 seconds, which corresponds to how long quarto inspect takes to run.

@toph-allen toph-allen changed the base branch from main to toph-remove-quarto-compatibility-shim-code March 18, 2022 22:35
@toph-allen toph-allen changed the title rsconnect::writeManifest for Quarto content [DRAFT] rsconnect::writeManifest for Quarto content Mar 22, 2022
@toph-allen toph-allen force-pushed the toph-writeManifest-for-quarto-content branch from 583fdd7 to 052e689 Compare March 22, 2022 17:31
@toph-allen toph-allen requested a review from aronatkins March 22, 2022 18:45
Base automatically changed from toph-remove-quarto-compatibility-shim-code to main March 22, 2022 18:57
Quarto inspection
- now is gated by passing in a full Quarto path
- should also work for single quarto documents
- move duplicated complex functions to an inferQuartoInfo function
- “quarto” used to mean either the path to the executable or the ready-for-manifest list. Now, the list is referred to as “quartoInfo”, matching “pyInfo”. Lookup doesn’t happen in the same place as “pyInfo”, but naming is more consistent.
@toph-allen toph-allen changed the title [DRAFT] rsconnect::writeManifest for Quarto content rsconnect::writeManifest for Quarto content Mar 24, 2022
@toph-allen toph-allen marked this pull request as ready for review March 24, 2022 15:59
@toph-allen
Copy link
Contributor Author

CI should be fixed now. The check on Travis turned up an "unstated dependency" on quarto-r in the new tests. However, my local check succeeded.

✓  checking for unstated dependencies in ‘tests’ ...

Weird.

@toph-allen
Copy link
Contributor Author

In its current state, this gives all Quarto content the content_category of site. I copied the behavior of quarto::quarto_publish_app(). However, after looking in more detail at Aron's rsconnect-python PR, I think it might be better to only give that property to website and book projects. @aronatkins interested to hear your thoughts.

Copy link
Contributor

@aronatkins aronatkins left a comment

Choose a reason for hiding this comment

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

When we have incoming Quarto metadata, let's avoid calling inspect.

DESCRIPTION Outdated Show resolved Hide resolved
R/bundle.R Outdated Show resolved Hide resolved
R/bundle.R Outdated Show resolved Hide resolved
R/bundle.R Outdated Show resolved Hide resolved
R/bundle.R Outdated Show resolved Hide resolved
@toph-allen toph-allen requested a review from aronatkins March 28, 2022 16:10
R/bundle.R Outdated

# Extract the Quarto version and engines, either from a parsed "quarto inspect"
# result or from metadata provided by the quarto-r package.
getQuartoManifestDetails <- function(inspect = list(), metadata = list()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I would prefer if this were split into two "constructor" functions, rather than one function that consumed the inputs differently depending upon what is present. Frankly, it might be simpler if the behavior were just inlined into inferQuartoInfo, but I don't feel strongly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I'll inline it. I think that having two separate list() statements in two separate functions is a bit too much indirection.

@toph-allen
Copy link
Contributor Author

If you don't have quarto on your path, but do have the IDE installed, this code block will work for validation:

For writeManifest

# If quarto::quarto_path() doesn't find Quarto, you need to use the IDE quarto
QUARTO_PATH <- "/Applications/RStudio.app/Contents/MacOS/quarto/bin/quarto"

# Quarto content
rsconnect::writeManifest("quarto-book-none", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-doc-none", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-multidoc-proj-none", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-proj-none", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-proj-none-ojs", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-proj-py", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-proj-r", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-proj-r-py", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-proj-r-shiny", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-website-none", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-website-py", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-website-r", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-website-r-py", quarto = QUARTO_PATH)
rsconnect::writeManifest("quarto-website-r-py-separate-files", quarto = QUARTO_PATH)

# Non-quarto content

rsconnect::writeManifest("rmd-shiny-runtime-shinyrmd-complete")
rsconnect::writeManifest("rmd-shiny-runtime-shinyrmd-simple")
rsconnect::writeManifest("rmd-site")
rsconnect::writeManifest("shinyapp")

For deployApp

# Assuming you have a server named "localhost" configured in the IDE / in the rsconnect package
server <- "localhost"

# If quarto::quarto_path() doesn't find Quarto, you need to use the IDE quarto
QUARTO_PATH <- "/Applications/RStudio.app/Contents/MacOS/quarto/bin/quarto"

# Quarto content
rsconnect::deployApp("quarto-book-none", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-doc-none", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-multidoc-proj-none", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-proj-none", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-proj-none-ojs", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-proj-py", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-proj-r", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-proj-r-py", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-proj-r-shiny", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-website-none", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-website-py", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-website-r", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-website-r-py", quarto = QUARTO_PATH, server = server)
rsconnect::deployApp("quarto-website-r-py-separate-files", quarto = QUARTO_PATH, server = server)

# Non-quarto content

rsconnect::deployApp("rmd-shiny-runtime-shinyrmd-complete", server = server)
rsconnect::deployApp("rmd-shiny-runtime-shinyrmd-simple", server = server)
rsconnect::deployApp("rmd-site", server = server)
rsconnect::deployApp("shinyapp", server = server)

- Added test for null Quarto on content that Quarto can’t render
@toph-allen toph-allen merged commit f19621c into main Apr 15, 2022
@toph-allen toph-allen deleted the toph-writeManifest-for-quarto-content branch April 15, 2022 18:27
@toph-allen toph-allen mentioned this pull request May 1, 2023
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.

2 participants