Leadsheet is a Python library that wraps LaTeX compilation for creating musical leadsheets. It ships a custom LaTeX document class (leadsheet.cls) and provides a Python CLI and API for compiling .tex files to PDF.
leadsheet/
├── leadsheet/ # Python package
│ ├── __init__.py # Public API exports
│ ├── __main__.py # CLI entry point (python -m leadsheet)
│ ├── compiler.py # Compilation engine
│ ├── converter.py # PDF-to-PNG conversion (pymupdf)
│ └── latex/ # LaTeX class files (bundled as package data)
│ ├── leadsheet.cls
│ ├── leadsheet-core.sty
│ ├── leadsheet-chords.sty
│ └── leadsheet-sections.sty
├── examples/ # Example leadsheets
│ ├── maniac.tex
│ ├── maniac.pdf # Built by `just build-examples`
│ └── maniac.png # Built by `just build-examples`
├── tests/ # Test suite
│ ├── conftest.py # Shared fixtures and helpers
│ └── test_compiler.py # Integration tests
├── .github/workflows/
│ └── ci.yml # GitHub Actions CI (lint, typecheck, test)
├── pyproject.toml # Project metadata, dependencies, tool config
└── justfile # Task runner commands
| Module | Responsibility |
|---|---|
compiler.py |
Locate LaTeX class files, build environment, invoke latexmk, collect outputs |
converter.py |
Convert a compiled PDF to PNG using pymupdf |
__main__.py |
Parse CLI arguments (--format, --engine), call compile_latex, handle errors |
__init__.py |
Re-export public API: compile_latex, CompilationError |
CLI / Python API
│
▼
compile_latex(input_path, output_path, formats, engine, verbose)
│
├── _find_latex_dir() # Locate leadsheet/latex/ inside the package
│
├── _build_env(latex_dir) # Set TEXINPUTS so TeX finds class files
│
├── _build_cmd(engine, ...) # Assemble latexmk command
│
├── subprocess.run(cmd, ...) # Execute latexmk in a temp directory
│
├── shutil.copy2(tmp_pdf, ...) # Move PDF to requested location
│
└── pdf_to_png(pdf, png) ──────── If "png" in formats
│ (converter.py, uses pymupdf/fitz)
└── returns png path
FileNotFoundError— input file not found, or LaTeX class files not foundCompilationError— latexmk exited with non-zero code; carries.stdoutand.stderrfor debugging
The LaTeX class files live in leadsheet/latex/ — the single source of truth, both in the repository and in the installed wheel. Hatchling includes all files under the leadsheet/ directory automatically, so no special force-include configuration is needed.
The TEXINPUTS environment variable is set at compilation time so lualatex finds leadsheet.cls regardless of the working directory.
leadsheet.cls
│
├─→ leadsheet-core.sty (no internal deps)
│ └─→ Provides: preprocessing engine, barline commands, layout utils
│
├─→ leadsheet-chords.sty (depends on core)
│ └─→ Provides: chord parsing, chord formatting, progression storage
│
└─→ leadsheet-sections.sty (depends on core + chords)
└─→ Provides: lstabular, songsection, hlsongsection environments
User writes: \begin{songsection}{Verse 1}
& ^Cm7 & ^F7 \\
<> Hello & world \\
\end{songsection}
│
▼
[leadsheet-sections.sty] songsection captures body → lstabular receives content
│
▼
[leadsheet-core.sty] __leadsheet_preprocess:n transforms:
"<>" → \lsfill
"|" → \lsbarline
"^Cm7" → \__leadsheet_chord_draw:n{Cm7}
"^{Name}" → \__leadsheet_progression_use:n{Name}
│
▼
[leadsheet-chords.sty] __leadsheet_chord_draw:n parses "Cm7":
Root: C | Quality: m | Extension: 7
│
▼
Final output: bold C with superscript ⁷
| Type | Pattern | Examples |
|---|---|---|
| Document commands | \name |
\chord, \key, \lsfill |
| Environments | lowercase |
lstabular, songsection |
| Public L3 functions | \leadsheet_name:spec |
\leadsheet_chord_format_root:n |
| Internal functions | \__leadsheet_name:spec |
\__leadsheet_preprocess:n |
| Local variables | \l__leadsheet_name_type |
\l__leadsheet_chord_input_tl |
| Constants | \c__leadsheet_name_type |
\c__leadsheet_progression_register_* |
| Tool | Purpose |
|---|---|
| uv | Python package and virtual environment management |
| just | Task runner (just test, just lint, just typecheck, etc.) |
| ruff | Linting and formatting |
| pyright | Static type checking |
| pytest | Test framework |
| pymupdf | PDF-to-PNG conversion (runtime dependency) |
| latexmk | Multi-pass LaTeX build tool (backend for compilation) |
| hatchling | Build backend for the Python wheel |
Tests are integration tests that compile real .tex files via the CLI and Python API:
TestPDFOutput— compileexamples/maniac.texonce per session (session-scoped fixture), then check that the PDF contains expected text (title, section headings, lyrics).TestCompilerAPI— testcompile_latex()directly: default output paths, explicit paths, missing input, invalid LaTeX, nested output directory creation, and format validation.TestPNGFormat— test PNG output:formats="png",formats=["pdf", "png"], PNG magic bytes, and the standalonepdf_to_png()converter.TestPageNumbers— verify page numbers appear only on multi-page documents by compiling a minimal single-page and a long multi-page document and inspecting extracted text per page.TestCLI— testpython -m leadsheetvia subprocess:--format,--engine, error exit codes, success messages.
Shared helpers in conftest.py (extract_pdf_text, extract_pdf_pages, pdf_page_count, run_cli) prevent duplication across test classes.
\ExplSyntaxOn
\cs_set_protected:Npn \leadsheet_chord_format_root:n #1 {
% Custom root formatting
}
\ExplSyntaxOffThe --engine CLI flag and engine parameter of compile_latex accept any engine supported by latexmk: lualatex, xelatex, pdflatex.
leadsheet-tablature.sty— Guitar tabsleadsheet-diagrams.sty— Chord diagramsleadsheet-transposition.sty— Key transposition utilities