- Note
-
This is one of a few posts on new features from a recent four-day knitr backlog sprint; see that post for the full details of the sprint.
The concept of spin() is like the reverse of purl(). Instead of pulling code
out of a document, it turns a plain script—code with special #' comments for
prose and #+ comments for chunk options—into a full report. It’s a nice way
to write a quick reproducible document when you can’t be bothered to create an
.Rmd: just write an R script, sprinkle in some #' comments, and spin it.
This cycle it got a few nice upgrades.
Spin scripts in other languages
spin() assumed R. But if you keep your analysis in a Python script, you
shouldn’t have to annotate every single chunk with #+ engine="python" just to
spin it. The new engine argument sets a default engine for all chunks:
knitr::spin("analysis.py", engine = "python")
Better still, if you leave engine unset, it’s guessed from the file
extension—so a .py file spins as Python without any argument at all. A chunk
that sets its own engine still overrides the default. (This applies to the
Markdown output formats.) It makes spin() a genuinely language-agnostic way to
turn an annotated script into a report
(#1773).
Keeping roxygen2 blocks intact
Here’s a subtle collision. spin() treats #' lines as prose. But
roxygen2—the tool that generates R package documentation—also uses
#' lines, for tags like #' @param. So if you tried to spin a script that
doubled as roxygen-documented package code, your @param tags got swept up and
turned into prose, and the script stopped being valid roxygen input.
The new roxygen = TRUE argument fixes the conflict: a block of consecutive
#' lines that contains a roxygen tag is kept verbatim, inside the code chunk,
instead of being converted to prose. Blocks with no tag are still treated as
documentation:
knitr::spin("myfun.R", roxygen = TRUE)
So the same file can be both a spinnable report and a source file roxygen2 is happy to parse (#2317).
Round-tripping with purl()
purl(documentation = 2) emits chunk labels as ## ----label tokens, and
spin() reads those back. But it used to glue the label straight onto the
engine name, producing an invalid header like ```{rlabel}—and on the
next purl(), rlabel was mistaken for an engine name and the chunk got
mangled. spin() now inserts the missing space (```{r label}), so
purl() → spin() → purl() round-trips cleanly
(#2014).
Catching mismatched delimiters
spin() uses # /* and # */ to mark a block of lines to drop. It used to
only compare the counts of start and end delimiters, so a mis-ordered pair (an
end before its start) could silently swallow the lines in between. It now checks
that the delimiters are properly paired and ordered, and reports the line number
of any unmatched one instead of quietly eating your content
(#1801,
#1802).

