QuartoTools.jl

Utilities for working with Quarto notebooks in Julia

This package provides several utilities that can be used in conjuction with Quarto notebooks when using the engine: julia setting, which executes your notebook code with QuartoNotebookRunner.jl.

"Expandables"

QuartoNotebookRunner.jl has a special feature called cell expansion. It allows you to have a single code cell that outputs what looks like multiple code cells and their outputs to Quarto.

What can you use this feature for?

Quarto has many advanced options which allow you to create richer output, for example tabsets which can group several separate sections of a quarto notebook into selectable tabs. These features are controlled with markdown annotations, for example, a tabset follows this structure:

::: {.panel-tabset}

## Tab 1

Content of tab 1

## Tab 2

Content of tab 2

... possibly more tabs ...

:::

As you can see, the tabset begins and ends with a pandoc ::: div fence and consists of sections demarcated by markdown headings. This mechanism has two drawbacks for the user:

  • It can be tricky to get the syntax right, especially with multiple nested ::: fences that need to be closed correctly. (This also applies when you generate markdown programmatically by printing out snippets in loops and using the output: asis cell option.)
  • It is static. Each tab has to be written into the source markdown explicitly, so you cannot easily create a tabset with a dynamic number of tabs. For example, a tabset with one plot per tab where the number of plots depends on runtime information and therefore is not known in advance.

Cell expansion can solve both of these problems. It relies on a function called QuartoNotebookWorker.expand, which is defined within the notebook worker process for every notebook that you execute.

When a notebook cell returns a Julia value from a cell, such that it is displayed but Quarto, the expand function will first be called on that value. By default this returns nothing and so we just display the original value. But if an expand method is defined for that type, then it should return a Vector{QuartoNotebookWorker.Cell} object which is then evaluated as if they were real cells. This feature is recursive, so Cells can themselves return more vectors of Cells.

Thus we can use these cells to build the structures Quarto expects programmatically, instead of having to hardcode them into the notebook.

For example, a tabset with plots could be generated by expanding into:

  • a cell with markdown output ::: {.panel-tabset}
  • a cell with markdown output ## Tab 1
  • a cell with a plot output, for example a Makie.Figure
  • more headings and plots
  • a cell with markdown output :::
Note

Cell expansion is not code generation. We do not generate and evaluate arbitrary code. Instead, we create objects describing code cells together with their outputs which is easier to reason about and more composable.

Each QuartoNotebookWorker.Cell has three fields:

  • thunk stores a function which returns the fake cell's output value when run. This value is treated as any other code cell output value, so it may be of any type that the display system can handle, and it may even be expandable itself (allowing for recursive expansion).
  • code may hold a string which will be rendered as the code of the fake cell (this code is not run).
  • options is a dictionary of quarto cell options, for example "echo" => false to hide the source code section.

QuartoTools defines a set of helper objects that can serve as building blocks that can be composed further. For example, a Tabset may contain multiple Divs, each describing a two-column layout which is populated with two plots.

Caching

QuartoTools provides a caching mechanism that can be used to save the results of expensive function calls in your notebook cells. Once loaded into a notebook via a cell containing import QuartoTools you can annotate any subsequent cell with the julia.cache.enabled key as follows:

---
engine: julia
---

```{julia}
import QuartoTools
```

```{julia}
#| julia:
#|   cache:
#|     enabled: true
result = expensive_func(arg)
```

The first time that expensive_func is called with any specific arg value the result will be saved to disk using Serialization. Subsequent calls with the same arg value will return the cached result rather than re-running expensive_func. This can be useful for long-running computations that slow down the rendering of your notebook.

Avoid using the feature on cells that only take a few seconds that run, since the overhead of saving and loading cached results can be larger than the time saved. If you have a particularly complex cell that contains some fast calls and some slow ones, try to factor them out into separate cells and only run the caching on the slow ones.

The cache for each notebook is stored alongside it in a folder called .cache. Removing this folder will clear the cache for the notebook. Do not commit the contents of this folder to version control.

What a cache key covers

A cached call digests six things, and a change to any of them means the call runs again:

  1. The full VERSION of Julia.
  2. The active Project.toml and Manifest.toml.
  3. The source of the definition or call site itself.
  4. Every tracked definition the call depends on, however deep: their lowered code, and the structure of the types they refer to.
  5. The current values of the tracked globals that code reads.
  6. The arguments and keyword arguments.

Point 4 is what ordinary memoisation lacks. QuartoTools walks out from the call to the methods it dispatches to, then to the methods those call, to a fixpoint. Editing a function three calls below the one you cached invalidates that entry and leaves every other entry alone.

The walk digests lowered code, so a change below the cached definition that leaves behaviour alone leaves a stored result valid: a renamed local variable, an added comment, a definition moved down a file. Changing what that code does never does.

The cached definition's own source is digested as written, so editing it invalidates the entry. Renaming one of its local variables counts as an edit.

Where the walk stops

Walking everything a call can reach would mean walking most of Base, so the walk stops at code that cannot change while the manifest holds still: Base, Core, the standard libraries, and packages the package manager installed. Everything else is walked, which covers Main, notebooks, scripts, modules built at runtime, and packages checked out with Pkg.develop. Move that boundary with QuartoTools.track! and QuartoTools.untrack!, and ask where it sits with QuartoTools.is_tracked.

Two things the walk cannot see, both covered by QuartoTools.dependencies: a callable reached with no lexical mention and no inferable path, and state outside the process, such as a data file or a database.

Caching a function rather than a call

@cache also takes a definition, in which case every call to that function is cached, and a definition of a callable object, in which case the object takes part in the key:

QuartoTools.@cache function summarise(rows, passes = 1000)
    return expensive(rows, passes)
end

QuartoTools.@cache function (counter::Counter)(rows)
    return expensive(counter, rows)
end

Managing what is stored

Nothing prunes on its own, so a cache directory grows until something sweeps it. QuartoTools.usage says which function is worth sweeping, QuartoTools.entries lists the individual results, and QuartoTools.drop! deletes one:

julia> QuartoTools.usage()
function    entries      bytes  oldest use        newest use
summarise         2  1.358 GiB  2026-08-30 09:14  2026-09-09 11:02
load_table        1  8.000 MiB  2026-09-08 16:40  2026-09-08 16:40

julia> QuartoTools.drop!(last(QuartoTools.entries()))
true

A definition caches beside its own file, which need not be the directory you are working in, so all four cover the working directory's .cache and the one beside every cached definition loaded so far. Ask QuartoTools.managed_directories which those are, or pass a directory to confine any of them to one.

QuartoTools.prune! sweeps by age, count, total size, or one function's name, and QuartoTools.clear! drops everything. Every criterion given applies, so a sweep drops an entry as soon as one of them condemns it. Name none of the three and the sweep drops what has gone unused for thirty days:

QuartoTools.prune!(; older_than = Dates.Day(7))
QuartoTools.prune!(; keep = 500)
QuartoTools.prune!(; max_size = 5 * 1024^3)
QuartoTools.prune!(; name = "summarise")
QuartoTools.clear!()

QuartoTools.cache_directory! sends entries somewhere other than the .cache folder beside the notebook, and QuartoTools.disable! runs every call and stores nothing. QUARTOTOOLS_CACHE_DIRECTORY and QUARTOTOOLS_CACHE_DISABLE=1 set both for a whole session without editing code.

Serialization

When working with serialized data in Quarto notebooks users must use the QuartoTools.serialize and QuartoTools.deserialize functions provided by the QuartoTools package rather than the Serialization package. This is due to the differences in the behaviour of code evaluation between the Julia REPL and that of Quarto. These two functions are drop-in replacements for those provided by Serialization and fall back on the implementation provided by it when not run in a Quarto notebook. This means that simply replacing using Serialization with using QuartoTools should be sufficient to allow for transparent serialization and deserialization between notebooks, batch scripts, and the REPL.

Note that if both QuartoTools and Serialization are imported with using in the same session then the functions serialize and deserialize will need to be prefixed with their package name due to the name collisions between the two packages. Typically users should only need to import QuartoTools.

Docstrings

QuartoTools.CachedType
Cached(f, mod, file, name, digest)

A function paired with where it was called from, so that calling it consults the cache. @cache and the cell transform both build one of these.

source
QuartoTools.CallSiteType
CallSite

Where a cached definition was written. Built by @cache at expansion time, so digest covers the source of the definition itself.

source
QuartoTools.CellType
struct Cell

Cell(content::Function; code = nothing, options = Dict{String,Any}(), lazy = true)
Cell(content; code = nothing, options = Dict{String,Any}(), lazy = false)

The most basic expandable object, representing a single code cell with output.

If code === nothing, the code cell will be hidden by default using the quarto option echo: false. Note that code is never evaluated, merely displayed in code cell style. Only content determines the actual cell output.

All options are written into the YAML options header of the code cell, this way you can use any cell option commonly available in code cells for your generated cells. For example, options = Dict("echo" => false) will splice #| echo: false into the code cell's options header.

If lazy === true, the output will be treated as a thunk, which has to be executed by QuartoNotebookRunner to get the actual output object that should have display called on it. Accordingly, you will get an error if the output object is not a Base.Callable. If lazy === false, the output will be used as the actual output object directly by QuartoNotebookRunner. As an example, if you generate a hundred plot output cells, it is probably better to generate the plots using lazy functions, rather than storing all of them in memory at once. The lazy option is set to true by default when a Function is passed to the convenience constructor, and to false otherwise.

source
QuartoTools.DefinitionsType
Definitions

What a cached call depends on, found by reachable_definitions.

  • methods: every tracked method reachable from the entry point, sorted.
  • types: every tracked type the code refers to, sorted.
  • globals: every tracked global the code reads whose value is not itself code. Their values are hashed per call, since assigning to a global does not advance the world age.
  • digest: a digest of methods and types. Stable for as long as the world age holds.
source
QuartoTools.DivType
struct Div

Div(children::Vector; id=[], class=[], attributes=Dict())
Div(child; kwargs...)

Construct a Div which is an expandable that wraps its child cells with two markdown fence cells to create a pandoc div using ::: as the fence delimiters. Div optionally allows to specify one or more ids, classes and key-value attributes for the div.

id and class should each be either one AbstractString or an AbstractVector of those. attributes should be convertible to a Dict{String,String}.

Examples

Div(Cell(123))
Div(
    [Cell(123), Cell("ABC")];
    id = "someid",
    class = ["classA", "classB"],
    attributes = Dict("somekey" => "somevalue"),
)
source
QuartoTools.EntryType
Entry

One stored result, as found by entries.

  • name, mod, file: the definition whose call produced the result.
  • key: the digest that names the entry.
  • path: the file holding the result.
  • bytes: what the result and its metadata take up together.
  • created: when the result was stored, nothing when its metadata is gone.
  • used: when the result was last read.
  • result_type, julia: what was stored, and the Julia that stored it.
source
QuartoTools.EntryListType
EntryList

The results entries found, which display as a table. A vector of Entry in every other respect.

The table is displayed for this type rather than for a vector of entries, so that loading this package leaves the display of everything else alone.

source
QuartoTools.ExpandType
struct Expand

Expand(expandables::AbstractVector)

Construct an Expand which is an expandable that wraps a vector of other expandable. This allows to create multiple output cells using a single return value in an expanded quarto cell.

Example

Expand([Cell(123), Cell("ABC")])
source
QuartoTools.TabsetType
struct Tabset

Tabset(pairs; group = nothing)

Construct a Tabset which is an expandable that expands into multiple cells representing one quarto tabset (using the ::: {.panel-tabset} syntax).

pairs should be convertible to a Vector{Pair{String,Any}}. Each Pair in pairs describes one tab in the tabset. The first element in the pair is its title and the second element its content.

You can optionally pass some group id as a String to the group keyword which enables quarto's grouped tabset feature where multiple tabsets with the same id are switched together.

Example

Tabset([
    "Tab 1" => Cell(123),
    "Tab 2" => Cell("ABC")
])
source
QuartoTools.MarkdownCellMethod
MarkdownCell(s::String)

A convenience function which constructs a Cell that will be rendered by quarto with the output: asis option. The string s will be interpreted as markdown syntax, so the output will look as if s had been written into the quarto notebook's markdown source directly.

source
QuartoTools.cache_directoryMethod
cache_directory() -> String

The .cache directory under the working directory, which is where a call typed into a REPL stores its results. The QUARTOTOOLS_CACHE_DIRECTORY environment variable overrides it, and cache_directory! overrides both.

A definition written to a file caches beside that file instead, so managed_directories is what the reporting and sweeping functions cover by default.

source
QuartoTools.cache_keyMethod
cache_key(site, public, implementation, args, kws) -> String

The digest that identifies one cached result. Every input that can change the result takes part: the version of Julia, the manifest of the active project, the source of the definition, every tracked definition it depends on, and the arguments.

source
QuartoTools.cacheableMethod
cacheable(f) -> Bool

Determine if a function is cacheable. By default all functions are cacheable. Use this function to override that behaviour, for example to make Base.read uncacheable:

QuartoTools.cacheable(::typeof(Base.read)) = false
source
QuartoTools.callable_methodsMethod
callable_methods(@nospecialize(T::Type)) -> Vector{Method}

The methods that make T callable, in a stable order. Empty for a type that is not callable, and for Core.kwcall.

source
QuartoTools.callable_nameMethod
callable_name(tn::Core.TypeName) -> Union{Symbol,Nothing}

The name a callable is known by, or nothing when it has none.

Julia decorates the type name of every function with a leading #, so the decoration alone does not tell a named function from an anonymous one. A name exists when the defining module has a binding of it pointing back at this type, which is true of sum and false of a closure.

source
QuartoTools.clear!Method
clear!()
clear!(directory)

Delete stored results, under directory when one is given and under every directory managed_directories names otherwise. Only entries that this package writes are removed, so a directory holding anything else keeps it.

source
QuartoTools.closure_digestMethod
closure_digest(value) -> String

A digest of the code behind the closures a value's type names, empty for a value naming none.

Serialization records a closure by the name lowering gave it, and a session that redefines the code around it can hand that name to a different closure. Reading such an entry binds the value to code it was never written from, and the failure surfaces wherever the value is next called. Digesting the code when the value is stored, and again when it is read, is what catches that.

source
QuartoTools.combined_digestMethod
combined_digest(values) -> Vector{UInt8}

Digest a collection without depending on the order it came in. Each value is digested on its own and the digests are sorted, so a set found in one order here and another order there still digests alike.

source
QuartoTools.content_hashMethod
content_hash(value) -> Vector{UInt8}

Digest the content of value. Structurally identical values digest to the same bytes across processes, which is what makes the digest usable as a cache key. Values that cannot be serialized throw; a cache key is never guessed.

source
QuartoTools.deconstructMethod
deconstruct(value::T) -> S

An extension function for turning values of type T into a type S such that they can be serialized properly.

source
QuartoTools.definition_nameMethod
definition_name(name) -> (path, receiver)

The path the definition is written under, and how it names its receiver.

path runs from the outermost module down to the bare name, so Base.sum gives [:Base, :sum]. receiver is nothing for an ordinary definition, and for a callable object it is the name the object is bound to along with the declaration that binds it, so (c::Counter)(x) gives ([:Counter], (:c, :(c::Counter))).

source
QuartoTools.dependenciesMethod
dependencies(f) -> Tuple

Extra values that a cached call to f depends on, folded into its cache key. Use this for a dependency the walk cannot see, such as a callable fetched out of a container, or a data file whose contents matter:

QuartoTools.dependencies(::typeof(load_table)) = (read("input.csv"),)
source
QuartoTools.dependency_digestMethod
dependency_digest(f, argtypes::Type{<:Tuple}) -> Vector{UInt8}

Digest every definition that calling f with argtypes depends on, together with the current values of the globals it reads. Runs under the reading of the project that cache_key takes.

source
QuartoTools.deserializeFunction
deserialize(s::IO)
deserialize(filename::AbstractString)

Deserialize a value from the given IO stream or file using Julia's built-in serialization while correctly handling differences in "root" evaluation module between the REPL and Quarto notebooks.

source
QuartoTools.drop!Method
drop!(entry::Entry) -> Bool

Delete one stored result and its metadata, and return whether there was anything to delete. Use it to cut out an entry picked from entries, where prune! sweeps by age, count or size.

source
QuartoTools.drop_entryMethod
drop_entry(path)

Remove an entry and its metadata, for a file that reading has already refused. Leaving it costs a failed read on every call, and the result that runs in its place is written where it stood.

source
QuartoTools.entriesMethod
entries() -> EntryList
entries(directory) -> EntryList

Every result stored under directory, most recently used first, and under every directory managed_directories names when none is given.

julia> QuartoTools.entries()
2 entries, 1.358 GiB in /home/mike/project/.cache
function        bytes  last used         result
summarise   1.350 GiB  2026-09-09 11:02  NamedTuple{(:total,), Tuple{Float64}}
load_table  8.000 MiB  2026-09-08 16:40  Matrix{Float64}
source
QuartoTools.evict_half!Method
evict_half!(memo::AbstractDict, cap::Integer)

Drop half of memo once it holds more than cap entries.

A memo answers the question it holds for nothing, so emptying one hands a session's work back to be done again. Half of it goes instead, and the entries sit in no order worth choosing by, so the half the iteration reaches first is the half that goes.

source
QuartoTools.fit_typeMethod
fit_type(type::AbstractString, room::Int) -> String

type as the table should print it, given room characters to print it in.

A stored result can be a fitted model whose type runs to thousands of characters, and a column is as wide as its widest cell, so one of those would set the width of every row. A type that fits is printed whole, since the element type of a matrix and the field names of a named tuple are what make the column worth reading. One that does not is named by its outermost constructor, which is the part a reader is scanning for, and cut where even that overruns.

source
QuartoTools.forwardableMethod
forwardable(arguments) -> (declarations, forwarding)

Rewrite an argument list so that every argument has a name to forward under, and return the rewritten declarations alongside the expressions that pass them on. An argument written without a name gets a generated one; the digits in a generated name are stripped before hashing, so it stays stable across processes.

source
QuartoTools.global_digestMethod
global_digest(ref::GlobalRef) -> Vector{UInt8}

Digest the current value of a global.

A value nothing can write, a running task being the usual one, leaves its type standing in for it. The call keeps a key it would otherwise have lost, at the price of a change to that value going unnoticed, which is what the warning is for.

source
QuartoTools.ignore_global!Method
ignore_global!(mod::Module, name::Symbol)

Stop the value of a global taking part in cache keys.

The value of every tracked global that cached code reads is hashed on every call, since assigning to a global does not advance the world age and so nothing cheaper would notice a change. Use this for a global whose value is large and does not affect results, such as a preallocated buffer, and watch_global! to undo it.

source
QuartoTools.in_active_projectMethod
in_active_project(uuid, uuids) -> Bool

Whether a package belongs to the environment the cached code runs in.

A package the active project does not name comes from elsewhere on the load path, a development tool in a shared environment being the usual case. It can reach cached code only by replacing something that code uses, stdout being how that happens, so its own state decides nothing about a result. A module with no package identity, and any package at all when no manifest says otherwise, belongs to the project.

source
QuartoTools.is_enabledMethod
is_enabled() -> Bool

Whether cached definitions consult their cache. Set at load time from the QUARTOTOOLS_CACHE_DISABLE environment variable, and changed by enable! and disable!.

source
QuartoTools.is_liveMethod
is_live(m::Method) -> Bool

Whether m is the definition in force rather than one a later definition replaced.

Redefining a method leaves the one it replaced in its table, so a session that edits a function keeps every version it has passed through. Counting them all puts the history of the session into the key, which stops a function returned to what it said before from keying the way it did then.

source
QuartoTools.is_trackedMethod
is_tracked(mod::Module) -> Bool

Whether the content of code defined in mod participates in cache keys.

Defaults to true for Main, for modules created at runtime, for the types Serialization rebuilds when reading a stored value, and for packages whose source lies outside the read-only parts of the depot. Defaults to false for Base, Core, the standard libraries, and packages installed by the package manager. Use track! and untrack! to override.

source
QuartoTools.load_resultMethod
load_result(path)

Read back a value written by store_result.

Throws when the file is not a serialized stream, and when the value it holds would bind to code that has moved on since it was written. Serialization reads a foreign file as whatever its bytes happen to say rather than refusing it, so a file holding anything else would otherwise come back as a result. A caller that cannot use a stored result runs the call instead, so refusing here is what keeps a wrong value from reaching one.

source
QuartoTools.mentions_trackedMethod
mentions_tracked(T) -> Bool

Whether any tracked type appears anywhere in T. A call signature made only of foreign types cannot dispatch back into tracked code.

source
QuartoTools.module_nameMethod
module_name(mod::Module) -> Tuple{Symbol, Vararg{Symbol}}

The name to digest for mod, after storage_module has had its say. Every digest goes through this, so that code mapped onto another module keys the same way there.

source
QuartoTools.normalize_symbolMethod
normalize_symbol(sym::Symbol) -> Symbol

Strip the counters out of generated names. Symbol("#foo##3") becomes Symbol("#foo##"), so that a name generated during lowering digests the same way in every process regardless of how many names were generated before it.

Only a generated name is stripped. A symbol a caller built is data, and Symbol("Item #1") has to keep the digit that tells it from Symbol("Item #2").

source
QuartoTools.open_typesMethod
open_types() -> Vector{Core.TypeName}

The type names on the path this task is part way through writing, which is what stops a type that reaches itself from recursing.

The path does not live in the serializer, because a nested digest starts a serializer of its own and the guard has to span the two. It lives per task rather than per process because nothing stops two tasks hashing at once, and one task's path is not the other's.

source
QuartoTools.own_method_tableMethod
own_method_table(primary::DataType) -> Union{Core.MethodTable,Nothing}

The method table belonging to primary alone, or nothing when it has none.

A function's own table holds exactly its own methods, and walking it beats intersecting a signature against every method in the system. Everything that is not a function shares one table, and a callable struct can inherit its call method from an abstract parent, so neither gets the shortcut.

source
QuartoTools.project_digestMethod
project_digest(project, stamp) -> Vector{UInt8}

Digest the project and manifest that refresh_project! read, from the stamp it read them at. Code from a package that the manifest pins cannot change without this digest changing, which is what lets the dependency walk stop at the boundary of tracked code.

source
QuartoTools.prune!Method
prune!(; directory = nothing, older_than = nothing, keep = nothing,
       max_size = nothing, name = nothing)

Delete stored entries that are no longer worth keeping, and return how many went.

An entry's modification time is when it was last read, so older_than drops what has gone unused for that long. keep drops everything but that many most recently used entries, whatever their age. max_size drops the least recently used until the entries left fit in that many bytes, and drops an entry that overruns the budget on its own.

Every criterion given applies, so an entry that any of them condemns goes, and a call naming none of the three drops what has gone unused for thirty days. Asking to keep five hundred entries therefore keeps five hundred, however old.

name confines the sweep to the results of one function, and directory to one directory rather than every directory managed_directories names. Reach for usage to see what is worth sweeping, and drop! to cut out a single entry.

source
QuartoTools.qualified_pathMethod
qualified_path(name) -> Union{Vector{Symbol},Nothing}

The modules a name is written under, followed by the bare name, so Base.sum gives [:Base, :sum]. nothing when the expression names nothing that results can be stored under.

source
QuartoTools.reachable_definitionsMethod
reachable_definitions(f, argtypes::Type{<:Tuple}) -> Definitions

The tracked definitions that calling f with argtypes depends on, transitively. Memoised for as long as the world age holds, so a redefinition anywhere recomputes it and nothing else does.

source
QuartoTools.reading_timeMethod
reading_time(;
    words_per_minute::Integer=238,
    progress_bar::Bool=true,
    progress_bar_color::String="#0066cc",
    reading_time_template::Function
)

Add an estimated reading time to the top of the rendered HTML Quarto document.

When progress_bar is true then also add a thin progress bar to the top edge of the document that shows the current scroll position. progress_bar_color can be used to customize the color of the progress bar.

reading_time_template is a function that lets you customize the HTML that is generated for the reading time message. It takes a single argument minutes which is the JavaScript interpolation variable that will be replaced with the actual minutes in the browser. It is not the actual number of minutes.

This function has no effect when used in non-HTML output formats.

source
QuartoTools.reconstructMethod
reconstruct(value::S) -> T

An extension function for turning values of type S back into a type T after they have been deserialized.

source
QuartoTools.refresh_project!Method
refresh_project!() -> (project, stamp)

Take a reading of the active project, dropping every decision taken under the project as it stood whenever the reading has moved. Adding a dependency mid-session moves the boundary with it.

A reading costs a walk of the load path and two stat calls, so a cached call takes one and every decision under it answers from the memos that reading leaves current. The path and the stamp come back for a caller keying a memo of its own on the same reading.

source
QuartoTools.resolveMethod
resolve(signature) -> Resolution

Resolve signature and find what the methods it resolves to call.

Resolving costs a method lookup. Finding the calls costs inference, which is several orders more, so the result is kept and reused for as long as the same lookup gives back the same methods. A redefinition anywhere in the closure shows up as a different method here, and only then is inference run again.

source
QuartoTools.run_cachedMethod
run_cached(site, public, implementation, args, kws)

Return the stored result for this call, or run implementation and store what it returns. Any failure to key, read or write falls back to running the call: a wrong result is never returned in place of a slow one. An interrupt is not such a failure, and stops the call where it was.

source
QuartoTools.runtime_moduleMethod
runtime_module(mod::Module) -> Module

The module to use in this process in place of the mod that was recorded when a result was stored. The inverse of storage_module, and the default maps Main back onto the module a notebook evaluates in.

source
QuartoTools.serializeFunction
serialize(s::IO, x)
serialize(filename::AbstractString, x)

Serialize x to the given IO stream or file using Julia's built-in serialization while correctly handling differences in "root" evaluation module between the REPL and Quarto notebooks.

source
QuartoTools.sink_digestMethod
sink_digest(sink::HashSink) -> Vector{UInt8}

The 16 bytes digesting everything written to sink.

The two halves of the hash are laid down in the library's canonical order: the high half first, each half most significant byte first. Every XXH3 implementation writes those bytes in that order, so a digest taken here says the same thing as a digest taken anywhere else, on a machine of either endianness.

source
QuartoTools.statement_typeMethod
statement_type(x, code::Core.CodeInfo) -> Type

The inferred type of x inside code, widened out of the inference lattice into an ordinary type.

source
QuartoTools.storage_moduleMethod
storage_module(mod::Module) -> Module

The module to record in place of mod when storing a result or keying a call.

A notebook evaluates its cells in a module of its own, and a script evaluates in Main, which on its own is enough to stop one from reading what the other wrote. The default maps a notebook's module onto Main so that the two agree. Override it to map another pair of modules onto each other, and pair every mapping with the inverse in runtime_module:

QuartoTools.storage_module(m::Module) = m === Main.Analysis ? Main : m
QuartoTools.runtime_module(m::Module) = m === Main ? Main.Analysis : m
source
QuartoTools.store_resultMethod
store_result(path, value)

Write value to path, through a temporary file so that an interrupted write never leaves a half-written entry for the next process to read.

source
QuartoTools.trackedMethod
tracked(mod::Module) -> Bool

is_tracked answered from the memos alone, without a reading of the project.

A walk asks this of every method, type and global it meets, several hundred times over. The one reading that keeps the memos current is taken by whatever starts the walk.

source
QuartoTools.untrack!Method
untrack!(mod::Module)

Exclude the content of code defined in mod from cache keys, overriding the default policy described by is_tracked. Only do this for code that cannot change while the manifest stays fixed.

source
QuartoTools.unused_forMethod
unused_for(now::Float64, used::Float64) -> Float64

How long an entry last used at used has gone unused, in seconds.

A file is stamped at a finer resolution than time() reports, NTFS to a hundred nanoseconds against a clock Windows runs to about a millisecond, so an entry stored moments ago carries a modification time a fraction ahead of the reading taken after it. An entry cannot be used in the future, so that reads as no age rather than a negative one, and a sweep of no age at all condemns it the way it condemns the rest.

source
QuartoTools.unused_spanMethod
unused_span(older_than::Dates.Period) -> Float64

How many seconds older_than stands for, counted from now.

A month and a year have no fixed length, so the answer comes from calendar arithmetic rather than from a count of milliseconds, and Dates.Month(1) is as usable a cutoff as Dates.Day(30).

source
QuartoTools.usageMethod
usage() -> Vector{Usage}
usage(directory) -> Vector{Usage}
usage(entries::AbstractVector{Entry}) -> Vector{Usage}

What each cached function has stored, largest first, over the same directories entries covers. Use it to find which function is worth pruning before pruning anything, and pass a filtered list of entries to group only part of what is stored.

julia> QuartoTools.usage()
function    entries      bytes  oldest use        newest use
summarise         2  1.358 GiB  2026-08-30 09:14  2026-09-09 11:02
load_table        1  8.000 MiB  2026-09-08 16:40  2026-09-08 16:40
source
QuartoTools.@cacheMacro
@cache function f(args...; kws...) ... end
@cache f(args...; kws...) = ...
@cache f(args...; kws...)

Cache the results of calls to f.

On a definition, every call to it is cached. On a call, that one call is cached and f itself is left alone. A definition of a callable object, (c::Counter)(x), works too, and the object takes part in the key.

A call digests the definition, every tracked definition it transitively depends on, and its arguments. A result stored under that digest is returned without running the body; otherwise the body runs and its result is stored.

Change the body, or anything the body calls, at any depth, and the next call runs again. Definitions from Base, the standard libraries, and packages the manifest pins are not walked: they cannot change without the version of Julia or the manifest changing, and both take part in the digest already. See is_tracked for that boundary and track! to move it.

Results are stored with Serialization, under a .cache directory beside the file holding the definition. See cache_directory, disable!, cacheable and clear!.

@cache function summarise(rows, passes = 1000)
    return expensive(rows, passes)
end

result = @cache summarise(rows, 5000)
source
QuartoTools.@nc_cmdMacro
nc`variable_name` = func(args...)

Mark the given variable as non-cachable. This means that assigning to this variable from a function call will not cache the function call. This is equivalent to using the julia.cache.ignored array in cell options or notebook frontmatter in a Quarto notebook.

source