Skip to content

Commit

Permalink
Merge pull request #17 from ucbds-infra/pdfs
Browse files Browse the repository at this point in the history
  • Loading branch information
chrispyles authored Nov 17, 2022
2 parents e6a6a00 + 56e68bf commit f559def
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 19 deletions.
4 changes: 3 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ RoxygenNote: 7.2.1
Depends:
R (>= 4.0.0)
Imports:
testthat,
jsonlite,
testthat,
tools,
R6,
zip,
methods
Suggests:
IRdisplay,
mockery,
rmarkdown,
stringr,
withr
Config/testthat/edition: 3
52 changes: 44 additions & 8 deletions R/export.R
Original file line number Diff line number Diff line change
@@ -1,30 +1,66 @@
#' Export a JupyterNotebook to a zip file
#' Export a submission to a zip file
#'
#' @description Export a Jupyter Notebook to a zip file for submission.
#' @description Export a submission to a zip file for submitting. If indicated, a PDF of the
#' submission is generated and included in the zip file. (PDF generation is only supported for Rmd
#' and ipynb files.)
#'
#' @param notebook_path The path to the notebook
#' @param submission_path The path to the submission
#' @param export_path The path at which to write the zip file (optional)
#' @param display_link Whether to display a download link with `IRdisplay`
#' @param pdf Whether to include a PDF of the submission (only works for Rmd and ipynb files)
#'
#' @export
#'
#' @examples
#' \dontrun{
#' export("hw01.ipynb")
#'
#' # with pdf
#' export("hw01.ipynb", pdf = TRUE)
#' }
export <- function(notebook_path, export_path = NULL, display_link = TRUE) {
export <- function(submission_path, export_path = NULL, display_link = TRUE, pdf = FALSE) {
timestamp <- format(Sys.time(), "%Y_%m_%dT%H_%M_%S")

if (is.null(export_path)) {
notebook_name <- tools::file_path_sans_ext(basename(notebook_path))
export_path <- paste0(notebook_name, "_", timestamp, ".zip")
subm_name <- tools::file_path_sans_ext(basename(submission_path))
export_path <- paste0(subm_name, "_", timestamp, ".zip")
}

pdf_path <- NULL
if (pdf) {
ext <- tools::file_ext(submission_path)
pdf_path <- paste0(tools::file_path_sans_ext(basename(submission_path)), ".pdf")
if (ext == "ipynb") {
system2("jupyter", c("nbconvert", "--to=pdf", paste0("--output=", pdf_path), submission_path))

# move the PDF to the working directory because nbconvert outputs it in the directory
# containing the notebook
move_from <- paste0(file.path(dirname(submission_path), pdf_path))
file.rename(move_from, pdf_path)
} else if (ext == "Rmd") {
# add metadata to allow errors in the RMarkdown
new_subm_path <- tempfile(fileext = ".Rmd")
contents <- c(
"```{r cache = F, include = F}\nknitr::opts_chunk$set(error = TRUE)\n```",
readLines(submission_path))
writeLines(contents, new_subm_path)

rmarkdown::render(new_subm_path, "pdf_document", pdf_path)
file.remove(new_subm_path)
} else {
stop("Only Rmd and ipynb files can be converted to PDFs")
}
}

zip_filename_file_name <- "__zip_filename__"
writeLines(c(export_path), zip_filename_file_name, sep = "")

zip_files <- c(zip_filename_file_name, notebook_path)
zip::zip(export_path, zip_files)
zip_files <- c(zip_filename_file_name, submission_path)
if (!is.null(pdf_path)) {
zip_files <- c(zip_files, pdf_path)
}
zip::zip(export_path, zip_files, mode = "cherry-pick")

file.remove(zip_filename_file_name)

if (display_link) {
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ dependencies:
- pip
- pip:
- radian
- nbconvert
13 changes: 10 additions & 3 deletions man/export.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 80 additions & 7 deletions tests/testthat/test_export.R
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
library(mockery)

get_pdf_path <- function(p) paste0(tools::file_path_sans_ext(basename(p)), ".pdf")

test_that("writes a zip file with the correct contents", {
mock_display_html <- mock()
stub(export, "IRdisplay::display_html", mock_display_html)

notebook_path <- "my_notebook.ipynb"
writeLines(c("This is a notebook!"), notebook_path, sep = "")
withr::defer(file.remove(notebook_path))
subm_path <- "my_notebook.ipynb"
writeLines(c("This is a notebook!"), subm_path, sep = "")
withr::defer(file.remove(subm_path))

export(notebook_path)
export(subm_path)

expect_equal(length(Sys.glob("*.zip")), 1)
zip_path <- Sys.glob("*.zip")[1]
Expand All @@ -17,15 +19,86 @@ test_that("writes a zip file with the correct contents", {
# check that the zip file name has the correct format
expect_true(stringr::str_detect(zip_path, paste0(
"^",
tools::file_path_sans_ext(basename(notebook_path)),
tools::file_path_sans_ext(basename(subm_path)),
"_\\d{4}_\\d{2}_\\d{2}T\\d{2}_\\d{2}_\\d{2}\\.zip$")))

# check the zip file contents
expect_equal(zip::zip_list(zip_path)$filename, c("__zip_filename__", notebook_path))
expect_equal(zip::zip_list(zip_path)$filename, c("__zip_filename__", subm_path))

# check that __zip_filename__ was deleted
expect_false(file.exists("__zip_filename__"))

# check that IRdisplay::display_html was called
expect_called(mock_display_html, 1)
})

test_that("supports PDF exports for ipynb files", {
stub(export, "IRdisplay::display_html", mock())

mock_system2 <- mock()
stub(export, "system2", mock_system2)

stub(export, "file.rename", mock())

subm_path <- "my_notebook.ipynb"
writeLines(c("This is a notebook!"), subm_path, sep = "")
withr::defer(file.remove(subm_path))

pdf_path <- get_pdf_path(subm_path)
writeLines(c("This is a pdf!"), pdf_path, sep = "")
withr::defer(file.remove(pdf_path))

export(subm_path, pdf = TRUE)

expect_equal(length(Sys.glob("*.zip")), 1)
zip_path <- Sys.glob("*.zip")[1]
withr::defer(file.remove(zip_path))

# check the zip file contents
expect_equal(
zip::zip_list(zip_path)$filename,
c("__zip_filename__", subm_path, pdf_path))

# check that __zip_filename__ was deleted
expect_false(file.exists("__zip_filename__"))

# check that IRdisplay::display_html was called
expect_called(mock_system2, 1)
expect_args(mock_system2, 1, "jupyter", c("nbconvert", "--to=pdf", paste0("--output=", pdf_path), subm_path))
})

test_that("supports PDF exports for Rmd files", {
stub(export, "IRdisplay::display_html", mock())

mock_render <- mock()
stub(export, "rmarkdown::render", mock_render)

tempfile_path <- "foo.Rmd"
stub(export, "tempfile", tempfile_path)

subm_path <- "my_submission.Rmd"
writeLines(c("This is an Rmd file!\n"), subm_path, sep = "")
withr::defer(file.remove(subm_path))

pdf_path <- get_pdf_path(subm_path)
writeLines(c("This is a pdf!"), pdf_path, sep = "")
withr::defer(file.remove(pdf_path))

export(subm_path, pdf = TRUE)

expect_equal(length(Sys.glob("*.zip")), 1)
zip_path <- Sys.glob("*.zip")[1]
withr::defer(file.remove(zip_path))

# check the zip file contents
expect_equal(
zip::zip_list(zip_path)$filename,
c("__zip_filename__", subm_path, pdf_path))

# check that __zip_filename__ was deleted
expect_false(file.exists("__zip_filename__"))

# check that IRdisplay::display_html was called
expect_equal(length(mock_display_html), 1)
expect_called(mock_render, 1)
expect_args(mock_render, 1, tempfile_path, "pdf_document", pdf_path)
})

0 comments on commit f559def

Please sign in to comment.