Configuration
A project’s wiring goes in the [tool.research-helpers] section in the pyproject.toml file.
No keys are required—defaults are provided out-of-the-box—so a new project can be started with
minimal or no configuration.
The keys are written in kebab-case and map to the snake_case attribute of the same name, so
tables-dir in TOML is paper.tables_dir in Python. An unknown key raises a
ConfigWarning and is ignored, as does a key with the wrong
value type or with a value outside the allowed set.
Note
Wiring lives in pyproject.toml because that file is also the project-root marker.
Finding the configuration and finding the root are therefore one operation, and every
path in the configuration is resolved relative to the directory containing it.
Three layers of resolution
Each layer overrides the one before it:
Package defaults: everything works with no configuration at all.
[tool.research-helpers]: a project’s wiring, read once per root and cached.Explicit keyword arguments: always take precedence.
A value of None in an override is read as not specified at this layer, so a function may forward its own
optional arguments straight through to resolve() without
filtering them first:
def save_something(path, *, dpi=None):
settings = resolve(current_project().figures, dpi=dpi) # dpi=None leaves the project's value
Every entry point is fully callable with plain arguments and no project file.
Complete example
[tool.research-helpers.paper]
main = "tex/paper.tex"
tables-dir = "tex/tables"
figures-dir = "tex/figures"
bbl = "tex/out_dir/paper.bbl"
build-dir = "build"
text-width-in = 6.45 # \textwidth: letterpaper, 73pt margins
column-width-in = 3.04 # \columnwidth
[tool.research-helpers.figures]
profile = "print"
palette = "colorblind"
font = "TeX Gyre Pagella"
dpi = 300
[tool.research-helpers.log]
directory = "data/logs" # omit for console output only
console-level = "INFO"
file-level = "DEBUG"
library-level = "WARNING" # other libraries' loggers
colour = "auto"
[tool.research-helpers.sweep]
runs-dir = "runs"
contention-factor = 1.8 # no default is provided, has to be measured
max-array = 1000
[tool.research-helpers.arxiv]
engine = "xelatex"
texlive = 2025
bbl-format = "3.3"
[tool.research-helpers.latex]
text-math = true # tables' maths set as text (needs rhtextmath.sty)
[tool.research-helpers.documents.note]
main = "tex/note/note.tex" # additional document (besides paper)
Subsections
paper
Describes the document’s geometry and the locations of its different components. All paths are relative to the project root.
Key |
Type |
Default |
Description |
|---|---|---|---|
|
path |
|
The manuscript. |
|
path |
|
Location of generated |
|
path |
|
Location of generated |
|
path |
|
The biblatex file. |
|
path |
|
Scratch space for generated artefacts. |
|
float |
|
|
|
float |
|
|
|
bool |
|
Whether the manuscript is part of this repository. See without the manuscript. |
Note
Both figures and tables are laid out against the document’s width, so a figure or a table is authored at the size it will be printed rather than scaled afterwards.
To find the real values, put \showthe\textwidth and \showthe\columnwidth in your document and
read them off the log. They come out in points, which you can write down as they are (see
Units below).
Units
Any length may be written in inches, millimetres, centimetres, or TeX points, by changing the key’s suffix. These four all set the same field to the same value:
text-width-in = 6.4757
text-width-mm = 164.48
text-width-cm = 16.448
text-width-pt = 468.0
Suffix |
Unit |
In one inch |
|---|---|---|
|
inch |
1 |
|
millimetre |
25.4 |
|
centimetre |
2.54 |
|
TeX point |
72.27 |
-pt is the default unit LaTeX uses, i.e., the one \showthe\textwidth prints.
Settings are stored in inches internally, because that is the unit matplotlib figure sizes use.
research-helpers doctor shows the converted value.
figures
Key |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
|
|
str |
|
Any seaborn palette name. |
|
str |
|
A matplotlib family name. |
|
int |
|
Used for saving figures. On-screen display is separate and fixed. |
|
float |
|
Height of a |
log
Key |
Type |
Default |
Description |
|---|---|---|---|
|
path |
(unset) |
Location where log files should be written. Omit it for console output only. |
|
str |
|
Any level name accepted by the standard library, including custom ones. |
|
str |
|
Same as above. Usually more verbose than the console. |
|
str |
|
The level of every logger not obtained from |
|
|
|
|
sweep
Key |
Type |
Default |
Description |
|---|---|---|---|
|
path |
|
Location where a sweep’s manifest, parts, and results are written. |
|
float |
(unset) |
A value indicating how much slower a task runs when the node is full. |
|
int |
|
The most elements one array should carry. Caps a sweep’s planned width, and splits a queue across files. |
Note
contention-factor is the ratio between a task’s runtime alone on a node and its runtime with every
core busy, and it depends on the specifics of the setup (i.e. the code, the node, and the scheduler).
No default is provided since a wrong one would lead to incorrect wall-time estimates that either waste
a queue slot or get the job killed before completion. Left unset, estimate_runtime()
reports its numbers as optimistic and lists instructions on how to measure the factor.
arxiv
Key |
Type |
Default |
Description |
|---|---|---|---|
|
str |
|
The engine you select on arXiv. Used to check for engine-specific hazards, e.g. microtype font expansion under XeTeX. |
|
int |
|
The TeX Live year selected on arXiv. Used in diagnostic messages. |
|
str |
|
The biblatex |
|
list of str |
|
Text that must not reach a submission, such as |
latex
Key |
Type |
Default |
Description |
|---|---|---|---|
|
bool |
|
Set the inline maths of every generated table, in its headers, cells, and caption, as text in the table’s own typeface. The manuscript then loads the |
documents
Further manuscripts beside the paper, one table each, named by the document: a technical note, a
supplement, a second paper. The paper itself is always configured under [paper].
[tool.research-helpers.documents.note]
main = "tex/note/note.tex"
Each takes the same keys as paper. Only main is required, unless the document is configured
without its manuscript, and the rest are derived from it:
Key |
Default |
|---|---|
|
|
|
|
|
|
|
the paper’s |
|
the paper’s |
doctor marks a value that follows from main as [derived], and one taken from the paper as
[inherited]. See several documents.
Without the manuscript
A repository can publish a document’s tables and figures without its source using manuscript = false:
[tool.research-helpers.paper]
manuscript = false
tables-dir = "build/tables"
figures-dir = "build/figures"
--check then compares every registered float with its installed copy, and reports an installed
file without an emitter. Those items that require a manuscript are reported as not checked:
whether the manuscript uses each float, whether it carries one written in by hand, and the numbers
its prose states. main and bbl are not used, and doctor shows them as [no manuscript].
Without a manuscript tables-dir and figures-dir default to the build directory’s tables/ and figures/,
and --install has nothing to copy. A document under [documents] can also have a manuscript key, and needs no
main if it is set to false.
The setting is declared to ensure that a repository that does carry its manuscript still fails the check when the manuscript goes missing. See publishing the floats without the paper.
Checking settings resolution
The doctor utility can be used to check settings resolution:
research-helpers doctor
It prints every setting, the value in force, where it came from, and flags any configured path that does not exist:
root /Users/john/my-project
pyproject pyproject.toml
paper.main tex/paper.tex [pyproject]
paper.tables_dir tex/tables [pyproject]
paper.figures_dir tex/figures [pyproject]
paper.bbl tex/out_dir/paper.bbl [pyproject]
paper.build_dir build [pyproject]
paper.text_width_in 6.48 [pyproject]
paper.column_width_in 3.04 [pyproject]
figures.profile screen [pyproject]
figures.palette husl [pyproject]
figures.font DejaVu Sans [default]
figures.dpi 300 [pyproject]
figures.print_height_in 3.2 [default]
log.directory None [default]
log.console_level INFO [default]
log.file_level DEBUG [default]
log.library_level WARNING [default]
log.colour auto [default]
sweep.runs_dir runs [default] MISSING
sweep.contention_factor None [default]
sweep.max_array 1000 [default]
arxiv.engine xelatex [pyproject]
arxiv.texlive 2025 [pyproject]
arxiv.bbl_format 3.3 [pyproject]
Checking [default] versus [pyproject] is useful when trying to troubleshoot keys,
for example when a setting appears to have no effect—usually a typo in the key name, which also
produces a warning you can see with python -W always::UserWarning -c 'import ...'.
MISSING indicates a path that is configured but does not exist. In the example above, runs is
simply a directory that sweeps have not created yet. The same value on paper.main would mean that
the manuscript path is wrong.
Overriding the root
find_project_root() searches upward for pyproject.toml or
.git. Set RESEARCH_HELPERS_ROOT to skip the search—necessary in a scheduler job whose
working directory is outside the tree:
export RESEARCH_HELPERS_ROOT=/scratch/$USER/my-project
If no root is found, current_project() returns a project
carrying pure defaults rather than raising an exception so, for example, a Jupyter notebook
in a scratch directory still works.
Reading settings
Settings can be easily read by any code in the project:
from research_helpers.project import current_project
paper = current_project().paper
width = paper.text_width_in
current_project() caches per root, so calling it in a loop is
free.