-
Notifications
You must be signed in to change notification settings - Fork 0
CLI and Input Modes
The pytex command takes one input file and dispatches on its extension. There's
no mode flag — the suffix decides how the file is read.
| Extension | What happens |
|---|---|
.py |
Imported as a module; its module-level __pytex__ node is rendered. Name it <doc>.tex.py by convention. |
.tex |
Wrapped in IncludeTeX; inline \iffalse{pytex(...)}\fi markers are evaluated, then the whole thing is rendered. Name it <doc>.py.tex. |
.md / .markdown
|
Converted to nodes and wrapped in a document per --variant. Without --variant, the style is auto-detected. See Markdown to PDF. |
The file is ordinary Python. Build a tree and assign it to __pytex__:
from pytex.commands.builtin import Section, Title, MakeTitle
from pytex.model.concat import Concat
from pytex.model.document import Document
__pytex__ = Document(
preamble=Title("Report"),
body=Concat(MakeTitle(), Section("Intro"), "Body text."),
)Because it's just Python, you can compute the document: read a CSV with pandas,
loop to emit rows, pull values from the environment. Whatever __pytex__ ends up
holding is what gets rendered.
Start from a .tex file that already compiles on its own, and splice PyTeX into
it through \iffalse{pytex(...)}\fi markers. The \iffalse … \fi pair is a
LaTeX no-op, so the source still builds with a plain LaTeX toolchain — PyTeX just
fills in the markers when it renders.
Today is \iffalse{pytex(Today())}\fi.
A fraction: $\iffalse{pytex(Frac("1", "2"))}\fi$.
Plain Python works too: $3^2 = \iffalse{pytex(3 ** 2)}\fi$.Any registered factory is in scope inside a marker — the same names you'd import
in a .tex.py file. The expression's result is rendered in place.
On 3.14, pytex.tex takes a PEP 750 template string and builds a tree from it.
Static parts are literal LaTeX; interpolations are escaped when they're plain
values and spliced as-is when they're TeX nodes (nested t-strings and lists
work too):
from pytex import tex
from pytex.commands.builtin import Bold
name = "Q&A: 50%"
body = tex(t"{Bold('Heading')} — {name}")
# -> \textbf{Heading} — Q\&A: 50\%tex is only exported on 3.14+; everything else runs on 3.13.
pytex example.tex.py # render only -> build/example.out.tex
pytex example.tex.py --build # render + compile -> build/example.out.pdf--build runs tectonic, then makeindex if the document uses
glossaries/acronyms, then reruns tectonic when an index changed. Shell-escape is
on by default because inline images decode their base64 payloads at compile time;
turn it off with --no-shell-escape if you don't need that.
Output is minimal and color-tagged (==>, note:, warning:, error:) in
tectonic's style; on failure it points at the likely cause and the log file. Set
NO_COLOR to disable color.
| Flag | Default | Meaning |
|---|---|---|
-o, --output
|
<build-dir>/<input>.out.tex |
rendered LaTeX output path |
-b, --build
|
off | compile the rendered .tex to PDF with tectonic |
--build-dir DIR |
build |
directory for artifacts and tectonic output |
--no-shell-escape |
shell-escape on | disable shell-escape |
-t, --tree
|
off | print the input's TeX-node tree before rendering |
-f, --force
|
off | skip the optimize + analysis pass and build anyway |
--variant STYLE |
auto | Markdown output style (see Markdown to PDF) |
--config JSON |
none | JSON object of document-class params, merged over the frontmatter |
--untrusted |
off | render foreign input through the trust policy (see Trust) |
--trust-level LEVEL |
trusted |
trusted, sandboxed, or untrusted
|
The CLI is a trusted tool. Everything above — importing .py files,
evaluating pytex(...) markers, running eval comments in Markdown, leaving
shell-escape on — is code execution, and that's the whole point: a PyTeX
document is a program. So treat the default CLI like you'd treat
python somefile.py: run it on your own documents, never on a file someone
handed you.
When you do need to render input from an untrusted source, don't use the trusted
path. Pass --untrusted (shorthand for --trust-level untrusted), which sends
the build through the same trust policy the Blob API uses. On that
path the dangerous surfaces are closed:
-
.py/.tex.pyinputs are refused outright — no module import, no exec. -
.texpytex(...)markers and Markdownevalcomments stay inert; they render as literal text instead of running. - Shell-escape is forced off, and the document may only pull packages from the
allowlist — code-/file-surface packages like
mintedorpythontexare rejected. - CPU, memory, and output-size limits apply to the compile.
--trust-level sandboxed is the middle ground: same locked-down code surface,
a wider package allowlist, and — for PDF builds — it runs tectonic inside a
rootless Podman sandbox (and refuses rather than downgrade if Podman isn't
set up). trusted is the default, so nothing changes for documents you already
build.
If you're wiring PyTeX into a service that accepts uploads, prefer the Blob API directly — it's bytes-in / bytes-out with the same trust levels and never touches your filesystem.
Before rendering, the builder runs two render-equivalent passes. Optimize
tidies the tree (flattens nested Concats, drops empty nodes, turns whole-Raw
LaTeX — including \[...\], \(...\), $...$ and comments — into native
nodes, and expands inline pytex(...) markers). Then pytex_analyze flags
problems LaTeX would otherwise surface late or silently:
- references (
\ref,\cref,\autoref, …) to a label that's never defined, - labels defined more than once,
-
\includegraphicspaths that don't exist on disk.
Missing images are errors and abort the build; the rest are warnings. -f skips
both passes.
--tree is the debugging view: it prints the parsed node tree (then renders as
usual), with package-requiring nodes tagged [+package]:
$ pytex example.tex.py --tree
Document (article)
├── ControlSequence \title
│ └── Parameter { }
│ └── Raw "PyTeX Example"
└── Concat
├── ControlSequence \maketitle
├── ControlSequence \cref [+cleveref]
└── ...
pytex-tex2py turns a .tex file into an equivalent .tex.py. It reads the
file, runs Optimize over it, and serialises the result back to Python that
rebuilds the same tree:
pytex-tex2py paper.tex # -> paper.tex.py
pytex-tex2py paper.tex -o out.pyRendering the generated .tex.py reproduces the original byte-for-byte —
anything the serialiser doesn't special-case falls back to a literal Raw, so
the round-trip always holds.