How the reference facts are generated, and why¶
pyMzLib, mzLibRust and mzLibR all project the same bridge, so they describe the same verbs. When each binding wrote those descriptions by hand, they drifted. By September 2026 Python and Rust disagreed by one about how many file types mzLib reads, the R help pages were hand copies of Python docstrings, and no binding ran any of its examples.
The fix is to write the facts about each wire verb once, and have every binding render them and check its own prose against them.
The pieces¶
bridge (private) pyMzLib (this repository)
design/verbs/*.yaml ── sync ──▶ docs/specs/*.yaml + SOURCE
│
├─ scripts/render_spec_docs.py ─▶ docs/reference/_generated/*.md
│ └─ included by docs/reference/<module>.md
├─ pkg/python/tests/test_spec_docs.py (docstring lint)
└─ pkg/python/src/conftest.py (doctest replay bridge)
The spec (<module>.<verb>.yaml) holds the facts about one wire verb:
- its parameters, with their types, defaults, units and valid ranges;
- its result fields, with their units and what null means;
- its error kinds, and when each one happens;
- its caveats, the mzLib code it wraps (at the pin), DOIs, the verb's name in each binding, recorded
example fixtures, and
since.
The binding owns the idiom: its names, its prose, its guides. The spec format and its rules are in
the bridge's design/verbs/README.md.
The vendored copy. The specs live in the bridge repository, which is private, and this
repository's CI cannot read it. So docs/specs/ carries a copy, and docs/specs/SOURCE records the
bridge commit it came from. Never edit that copy. A fact that is wrong is wrong in the bridge, so
fix it there and sync. Every binding then picks up the correction, and every binding's lint flags
the pages that repeated the error.
python scripts/sync_specs.py --from ../bridge/design/verbs # wherever your bridge checkout is
python scripts/render_spec_docs.py
The rendered tables. render_spec_docs.py writes one fragment per spec, for example
docs/reference/_generated/readers.read-spectra.md. A fragment holds the wraps, parameters,
returns, errors, caveats, example, other-binding spellings, references and since sections, in the
order every binding's reference page uses. A page under docs/reference/ includes each fragment
with pymdownx.snippets, under a heading the page writes itself:
## `read_spectra`
<!-- GENERATED by scripts/render_spec_docs.py from docs/specs/readers.read-spectra.yaml (bridge e76157831b15). Do not edit: fix the spec in the bridge, sync it, re-render. -->
> Read the scans of a spectra file: every scan's header always, and its peak arrays only on request.
Wire verb `readers read-spectra` · Python [`pymzlib.readers.read_spectra`][pymzlib.readers.read_spectra]
### Wraps
- [`MsDataFileReader.GetDataFile`](https://github.com/smith-chem-wisc/mzLib/blob/23c2490e/mzLib/Readers/MsDataFileReader.cs) in `mzLib/Readers/MsDataFileReader.cs` at mzLib `23c2490e`
- [`MsDataFile.GetAllScansList`](https://github.com/smith-chem-wisc/mzLib/blob/23c2490e/mzLib/MassSpectrometry/MsDataFile.cs) in `mzLib/MassSpectrometry/MsDataFile.cs` at mzLib `23c2490e`
- [`SourceFile`](https://github.com/smith-chem-wisc/mzLib/blob/23c2490e/mzLib/MassSpectrometry/SourceFile.cs) in `mzLib/MassSpectrometry/SourceFile.cs` at mzLib `23c2490e`
### Parameters
| Parameter | Type | Default | Unit | Range | Meaning |
|---|---|---|---|---|---|
| `path` | `path` | required | — | — | A spectra file: mzML, MGF, ms1/ms2 msalign, Thermo .raw, Bruker .d or timsTOF .d. |
| `ms_order` (wire `--ms-order`) | `int` | absent | — | `>= 1` | Keep only scans at this MS level. Applied BEFORE offset and limit, so ms-order 2 with limit 10 is the first ten MS2 scans. Given with no value is a usage error; absent means every level. |
| `offset` | `int` | `0` | scans | `>= 0` | Skip this many scans (after the ms-order filter). |
| `limit` | `int` | absent | scans | `>= 0` | Return at most this many scans. Absent means no limit. |
| `peaks` | `flag` | `false` | — | — | Include the mz and intensity arrays. Off by default because peaks are roughly the size of the file; combine with limit, a scan filter, or out. |
| `out` | `path` | absent | — | `.tsv only, any case (PYB-1)` | Write the table to this path as TSV instead of returning columns. Must differ from path. Peak arrays are written as ';'-joined lists. Must end in .tsv, any case (PYB-1); any other extension, or none, is a usage error before any input is read, and no extension is appended. |
### Returns
Top-level fields:
| Field | Type | Unit | Null? | Meaning |
|---|---|---|---|---|
| `path` | `string` | — | never | Absolute path of the input. |
| `file_type` | `string` | — | never | mzLib SupportedFileType name. |
| `reader` | `string` | — | never | The mzLib MsDataFile class that read it. |
| `scan_count` | `int` | scans | never | The file's true total, before any filter, so a filter matching nothing never looks like an empty file. |
| `ms_order` | `int` | — | yes: no ms-order filter was applied | The ms-order filter, echoed. |
| `record_count` | `int` | scans | never | Scans that passed the ms-order filter. |
| `returned_count` | `int` | scans | never | Scans in columns; 0 when written to out. |
| `offset` | `int` | scans | never | The offset applied. |
| `truncated` | `bool` | — | never | True whenever scans were left out by offset or limit. |
| `peaks_included` | `bool` | — | never | Whether mz and intensity columns are present. |
| `retention_time_unit` | `string` | — | never | Always "minutes" for this verb: every mzLib MsDataFile reader converts at the boundary. |
| `caveats` | `string[]` | — | never | Format-specific traps for THIS file; see caveats below. |
| `column_names` | `string[]` | — | never | Column order. |
| `columns` | `table` | — | yes: the table was written to out instead | Column name to per-scan values. |
| `output` | `object` | — | yes: out was not given | {path, format: "tsv", row_count} when out was given. |
| `source` | `object` | — | yes: the reader built no source-file description at all (inner fields are null individually when the file does not record them) | {instrument_model, instrument_model_accession, instrument_serial_number, acquisition_start_time, acquisition_start_time_is_utc} from mzLib SourceFile (#1349). instrument_model is the model NAME and instrument_model_accession its PSI-MS accession (mzML has both; Thermo .raw only the name, so the accession is null - match on the accession, never the name). acquisition_start_time is ISO-8601 text, ending in Z only when the source fixed the instant (mzML startTimeStamp with an offset); otherwise the acquisition PC's wall-clock time. acquisition_start_time_is_utc is never null: false means local instrument-PC time or absent, and is always false for Thermo .raw. Each inner field is null when the file does not record it (MGF and msalign record none). |
| `rows_not_read` | `int` | rows | yes: never counted for this verb | Always null. |
| `absent_fields` | `string[]` | — | never | Always empty for this verb: an unrecorded scan field is null per scan. |
| `failed_fields` | `string[]` | — | never | 'field: ExceptionType' for each column whose read threw on some scans; those cells are null. |
| `excluded_fields` | `object[]` | — | never | Always empty for this verb. |
| `error` | `object` | — | yes: always, for a single path: a file that cannot be read fails the call instead | {kind, type, message}; non-null only in a files[] entry under on-error skip. |
Per-row fields (one value per record, in `columns` or each list entry):
| Field | Type | Unit | Null? | Meaning |
|---|---|---|---|---|
| `one_based_scan_number` | `int` | — | never | Scan number. For MGF it may be assigned in file order; see caveats. |
| `ms_order` | `int` | — | never | MS level (1, 2, ...). |
| `retention_time` | `float` | min | never | Retention time. |
| `polarity` | `string` | — | never | mzLib Polarity enum name. |
| `mz_analyzer` | `string` | — | never | mzLib MZAnalyzerType enum name. |
| `is_centroid` | `bool` | — | never | Whether the spectrum is centroided. |
| `total_ion_current` | `float` | intensity (instrument units) | never | Total ion current. |
| `injection_time` | `float` | ms | yes: the file did not record it | Ion injection time. |
| `peak_count` | `int` | peaks | never | Peaks in the scan; 0 when it has no spectrum. |
| `scan_window_lower_mz` | `float` | m/z | yes: no scan window recorded | Lower scan-window bound. DERIVED for MGF (first observed peak). |
| `scan_window_upper_mz` | `float` | m/z | yes: no scan window recorded | Upper scan-window bound. DERIVED for MGF (last observed peak). |
| `scan_filter` | `string` | — | yes: the file did not record one | Vendor scan filter string. |
| `native_id` | `string` | — | yes: the file did not record one | Vendor native ID. |
| `scan_description` | `string` | — | yes: the file did not record one | Free-text scan description. |
| `one_based_precursor_scan_number` | `int` | — | yes: MS1 scan, or the file does not record the survey scan (standard MGF) | Survey scan this scan was selected from. |
| `isolation_mz` | `float` | m/z | yes: MS1 scan, or not recorded | Isolation window centre. May be calibration-adjusted. |
| `isolation_width` | `float` | m/z | yes: MS1 scan, or not recorded | Isolation window width. |
| `selected_ion_mz` | `float` | m/z | yes: MS1 scan, or not recorded | Selected precursor m/z. May be calibration-adjusted. |
| `selected_ion_intensity` | `float` | intensity (instrument units) | yes: MS1 scan, or not recorded | Selected precursor intensity. |
| `selected_ion_charge_state_guess` | `int` | charge | yes: MS1 scan, or no charge assigned | Instrument or file charge-state guess. |
| `selected_ion_monoisotopic_guess_mz` | `float` | m/z | yes: MS1 scan, or no monoisotopic guess | Monoisotopic precursor m/z guess. |
| `dissociation_type` | `string` | — | yes: MS1 scan, or not recorded | mzLib DissociationType enum name. |
| `hcd_energy` | `string` | — | yes: not HCD, or not recorded | Collision energy AS WRITTEN by the file (mzLib keeps it a string, e.g. "35" or "25,30,35"); not parsed. |
| `compensation_voltage` | `float` | V | yes: no FAIMS on this instrument | FAIMS compensation voltage. |
| `mz` | `float[]` | m/z | yes: scan has no spectrum | Peak m/z values. *Present only with `peaks`.* |
| `intensity` | `float[]` | intensity (instrument units) | yes: scan has no spectrum | Peak intensities, parallel to mz. *Present only with `peaks`.* |
### Errors
| Kind | pyMzLib raises | When |
|---|---|---|
| `usage` | [`UsageError`][pymzlib.UsageError] | path missing or blank; file not found; the file is not one of the seven spectra types (the message names the views it does have) |
| `usage` | [`UsageError`][pymzlib.UsageError] | ms-order, offset, limit or out given with no value; ms-order < 1; offset or limit < 0; non-integer value |
| `usage` | [`UsageError`][pymzlib.UsageError] | out resolves to the same file as path |
| `correctness` | [`BridgeError`][pymzlib.BridgeError] | a vendor format that cannot load on this platform surfaces as its real failure (Bruker/timsTOF native DLLs are Windows x64 only) |
| `usage` | [`UsageError`][pymzlib.UsageError] | paths-stdin with path, offset or limit; paths-stdin with a value; no paths on stdin; a repeated path; threads 0 or below -1; on-error not fail or skip; on-error skip with a single path; out naming an input |
| `usage` | [`UsageError`][pymzlib.UsageError] | with on-error fail, an input that is missing or not this view: the usage error of that input, prefixed 'Input <i> (<path>)' |
| `correctness` | [`BridgeError`][pymzlib.BridgeError] | with on-error fail, mzLib failing to parse an input: that input's own error type, message prefixed 'Input <i> (<path>)' |
| `usage` | [`UsageError`][pymzlib.UsageError] | out (with path or paths-stdin) whose extension is not .tsv, any case, including none (PYB-1); checked before any input is opened |
| `usage` | [`UsageError`][pymzlib.UsageError] | the answer is too large to return as one JSON document (PYB-3 part 1; measured: past the ~1.07 G-character .NET string ceiling, not RAM). Message: 'this read (N records) is too large to return as one document; use --limit/--offset or --out', N the parsed record_count |
| `service_unavailable` | [`ServiceUnavailableError`][pymzlib.ServiceUnavailableError] | never raised by this verb |
### Caveats
- Headers only unless peaks is set; peak_count tells you how many peaks, not what they are.
- Bruker .d and timsTOF .d need vendor native DLLs and read only on Windows x64. Thermo .raw is managed and reads everywhere.
- MGF: scan numbers may be file order; precursor scan is null unless the mzLib PRECURSORSCAN header is present; ms_order comes from MSLEVEL or is inferred from PEPMASS; the scan window is derived from observed peaks.
- msalign holds DECONVOLVED neutral monoisotopic masses, not raw m/z: the mz column is not comparable with an mzML or raw file's mz and must not be re-deconvolved.
- msalign: scan_window_lower_mz/_upper_mz are in m/z while the mz column is neutral MASS, so they are not on the same axis. mzLib synthesises the window by converting each mass back to m/z at its reported charge (MsAlign.cs:526); msalign records no window of its own.
- timsTOF .d is ion-mobility-resolved and this view flattens it: the mobility dimension is collapsed into scans and no 1/K0 value is reported.
- source.acquisition_start_time from a Thermo .raw is the acquisition PC's local clock (is_utc false); ProteoWizard's mzML of the same run writes it as UTC assuming the CONVERTING machine's time zone, so the two can differ by the site's UTC offset (SourceFile.AcquisitionStartTime).
- Many files: --paths-stdin reads the list in ONE process, --threads at a time, into one long table with source_index and source_path first. The output does not depend on --threads (a tested property); the default of 1 is a memory choice, since every reader materialises the whole file.
### Example
```python
result = pymzlib.readers.read_spectra(path, limit=3)
Recorded from the live bridge as readers_spectra_mzml.json. The doctests in the API reference replay these recordings in CI.
batch = pymzlib.readers.read_spectra_many(['sliced_ethcd.mzML', 'no-such-run.mzML', 'withZeros.mgf'], threads=2, on_error='skip')
Recorded from the live bridge as readers_many_spectra.json. The doctests in the API reference replay these recordings in CI.
Same verb in other bindings¶
| Python (pyMzLib) | Rust (mzLibRust) | R (mzLibR) |
|---|---|---|
pymzlib.readers.read_spectra |
mzlib::readers::read_spectra_with with SpectraOptions |
readers_read_spectra |
References¶
None: no publication is attached to this verb.
Since¶
| Wire protocol | pyMzLib | mzLibRust | mzLibR |
|---|---|---|---|
| 1 | 0.1.0 | 0.1.0 | 0.1.0 |
Not yet verified
- injection_time unit 'ms' is from the mzML CV term (MS:1000927); not yet confirmed that every mzLib vendor reader (Thermo, Bruker, timsTOF, MGF) emits ms.
- source is read from MsDataFile.SourceFile after LoadAllStaticData; Bruker .d and timsTOF .d have not been checked for which of its fields they fill.
- PYB-3 part 2: refusing a too-large answer BEFORE serialising, when the parsed record count already predicts it, is not built. It needs a characters-per-record threshold measured per format (dataRepo's ~805 per record is one file type); part 1 catches the failure after serialising.
```
The fragments are committed. The MkDocs hook scripts/mkdocs_hooks.py re-renders them on every
build, so mkdocs serve always shows the current specs. CI fails if a committed fragment differs
from what the specs render.
The docstring lint (pkg/python/tests/test_spec_docs.py). For every spec that names a
bindings.py function, it:
- imports that function;
- parses its Google docstring with griffe (the parser mkdocstrings uses);
- fails if a spec param is missing from the signature or from
Args:; - fails if a result field is missing from
Attributes:on the returned class. When the function returns a list of row objects, the per-row fields are checked on the row class instead; - fails if a param or field whose spec
unitis not null does not mention that unit. "Skip this many" fails; "Scans to skip" passes.
A deliberate difference from the spec, such as a renamed field or a field that the Python shape
absorbs, must be declared with a reason in PYTHON_DEVIATIONS in scripts/spec_facts.py. The
lint fails on a declaration that names nothing in the spec, so stale entries cannot pile up. The
lint also checks that every verb the spec says pyMzLib ships has its fragment included on a
reference page.
The executed examples. pytest --doctest-modules pkg/python/src/pymzlib runs every docstring
example. pkg/python/src/conftest.py points PYMZLIB_BRIDGE at pkg/python/tests/replay_bridge.py,
a stand-in bridge that answers each call from a fixture recorded from the real one. The same
fixtures are the ones mzLibRust and mzLibR replay. A verb may answer from the fixtures its spec
lists under examples, plus any listed in REPLAY_EXTRA in that conftest (for verbs that have no
spec yet). The replay bridge answers only when a recording fits the call: the same file, the same
echoed options, and a limit/offset that reproduces the recorded window. A mismatch fails the
example and names each recording and why it did not fit. An example therefore cannot print output
recorded for other arguments.
An example that needs the network, a download or pandas keeps # doctest: +SKIP. The line above it
says why, as a >>> # comment, so a reader can tell a deliberate skip from a forgotten one.
In CI¶
docs.yml runs, on every pull request:
| check | fails when |
|---|---|
render_spec_docs.py --check |
a fragment is stale or missing |
pytest tests/test_spec_docs.py |
a docstring lacks a spec param or field, or its unit |
pytest --doctest-modules pkg/python/src/pymzlib |
an example raises or prints something else |
mkdocs build --strict |
a fragment is not found, or a cross-reference breaks |
links.yml runs lychee over every external link weekly, including the mzLib source links and DOIs
that the fragments render. It does not run on pull requests: another site's outage is not a reason
to block a change.
When you add or change a verb¶
- The spec lands in the bridge first, or with your change (SPEC-1). Sync it here.
- Render. Add the
--8<--include todocs/reference/<module>.md. If the module has no page yet, create one and add it to the nav. - Write the docstrings so that the lint passes: every param in
Args:, and every field in the result class'sAttributes:, each with its unit. - Give the docstring an example that runs against the spec's recorded fixture. Remove any
+SKIPthat no longer applies.
See Adding a capability for the whole recipe.