Changelog¶
All notable changes to this project will be documented in this file.
[Unreleased]¶
Changed¶
- The source distribution now contains only the source, tests and the README, changelog, citation and license files: no paper, docs, lockfile or repository tooling.
[1.1.0] (2026-09-24)¶
Added¶
to_records(source),PeffReader.to_records()andSequenceEntry.to_record(): one plaindictper entry with stable snake_case keys (RECORD_KEYS), annotations as their PEFF value text, ready forpandas.DataFrame(records)orpolars.DataFrame(records). No new dependency.SequenceEntry.from_fasta(header, sequence, *, prefix=None)andentry.to_fasta()convert to and from plain FASTA with UniProt-style headers (sp|P12345|NAME_HUMAN ... OS= OX= GN= PE= SV=). They take plain strings, so there is still no dependency on fastatacular.entry.to_proforma(*, mods="psimod"|"unimod", variants=())renders the sequence and its\ModResPsi/\ModResUnimod(and matching\ModRes) sites as a ProForma 2.0 string. Unknown (?) positions are handled, andVariantSimplesubstitutions can be applied.errors="skip"returnsNonefor an entry that cannot be written (for example, a site past the end of the sequence) instead of raisingPeffError.read_peff()andPeffReaderread gzip, bzip2 and xz compressed files directly (proteins.peff.gz). The format is detected from the magic bytes. Paths are opened once, so pipes and FIFOs work, even when the first chunk written is shorter than the magic number;bz2andlzmaare imported only when needed. Standard library only; no new dependency.write_peff(..., verify=False)skips the per-entry read-back check (about three quarters of the write time) for entries that came unchanged fromread_peff()/PeffReaderor were already written once. The default,verify=True, is unchanged; the basic checks (empty or malformed prefix, id or sequence, line breaks, duplicate keys) always run.
Performance¶
- Description-line lexer fast paths:
split_description_keys,split_itemsand the escapedsplit_fieldsskip the character-by-character scan when the text holds no backslash escape or nested parens. Output is identical (checked against the 1.0 implementation with Hypothesis and on the 20k-entry human UniProt PEFF). On that file (one core, indicative)read_pefftakes about 2.2 s instead of 4.6 s, andwrite_peff3.2 s instead of 5.6 s (0.9 s withverify=False). write_peff()consumesentriesas a stream instead of building a list, and spools the formatted text to a temporary file past 32 MiB, so a large write no longer holds every entry's text in memory. The validate-before-write guarantee is unchanged: on aPeffWriteErrora path is not created and nothing is written to a handle.
Fixed¶
- A file that is not valid UTF-8, or a corrupt or truncated compressed file, raises
PeffParseError("Cannot read the input after line N", chained to the original error) instead of a bareUnicodeDecodeError.
[1.0.0] (2026-09-23)¶
The public API (everything in pefftacular.__all__) is now stable and follows semantic versioning.
Changed¶
- Breaking:
PeffReadernow follows the same contract asfastatacular.FastaReader: a path is opened in__enter__, not in__init__(constructing a reader no longer leaks an open file), and accessing.headeror iterating outside awithblock raisesRuntimeError. Usewith PeffReader(path) as reader:orread_peff(). SequenceEntryis now explicitly unhashable (__hash__ = None). It holdscustom_values/extradicts, sohash()already failed; it now fails with a clearTypeErrorat the class level. Equality is unchanged.parse_positionin the internal_parsermodule is now_parse_position; the old name remains as an alias.PeffWriteErrorhas an.indexattribute (0-based position of the bad entry,Nonefor header problems) and its message starts withEntry N:for entry problems, matchingfastatacular.FastaWriteError.write_peff()now serializes every entry before writing anything, so a custom-key value that no longer matches its RegExp also leaves the destination untouched (it used to fail after the header and earlier entries were written).- Classifier is now
Development Status :: 5 - Production/Stable. - The wheel ships
py.typed, so type checkers use the package's inline type hints.
Fixed¶
- Entering
witha second time on a pathPeffReader(e.g.with r: r.headerthenwith r: list(r)) no longer fails with "I/O operation on closed file": the reopened file is read from the top, likefastatacular.FastaReader. A stream reader still continues where it stopped. write_peff()now re-parses each description line and the header it writes and raisesPeffWriteError(with.indexfor entries) if they would read back differently or not at all, instead of writing a corrupt file. Caught cases include a string position holding|or an unbalanced paren (VariantSimple("1|2", "A"),VariantSimple("1)", "A")), aModRes*with no positions, anid/db_unique_id_key/extravalue containing\Key=, anextrakey that is a known PEFF key or contains=, a non-integerProteoform.annot_id_refs, apeff_versionthat is notN.N, and aDatabaseHeader.extrakey that repeats a known header key or contains=. It also rejects a key present in bothcustom_valuesandextra, and a sequence that would start a wrapped line with;or#.""vsNonefor optional text and"5"vs5for positions are still accepted. The check costs about one extra parse per entry (the 20k-entry human UniProt PEFF writes in about 4.5 s). The whitespace and line-break pre-checks use a single regex scan and a line count, walking the entry only to name the offending field.- Lines starting with
;(FASTA comments) are skipped, as infastatacular, instead of being glued into the sequence. A#line after an entry's>line is skipped with aPeffWarninginstead of becoming part of the sequence. DatabaseHeader,FileHeaderandCustomKeyValueare now explicitly unhashable (__hash__ = None), likeSequenceEntry: they hold dicts, sohash()already failed, now with a clearTypeErrorat the class level.- The sdist no longer ships the spec PDF, docs, or repo tooling (hatch
only-include); the unusedMANIFEST.inis removed. scripts/release_version.py sync --set X.Y.Zalso setsdate-releasedinCITATION.cff.- Whitespace inside sequence lines (spaces, tabs) is now removed, not only at line ends. It used to be kept, so
write_peff()rejected the parsed entry and a read -> write round trip failed. - A
>entry with no sequence now raisesPeffParseError(the writer already rejected empty sequences, so such a file could be read but not written back). - Non-blank text between the header and the first
>entry line now raisesPeffParseErrorinstead of being dropped silently (spec section 3.3.1: header lines start with#). - Files starting with a UTF-8 BOM are now read (paths are opened as
utf-8-sig; a BOM at the start of a stream is skipped). They used to fail with "First line must be '# PEFF'". write_peff()no longer ignores edits to a parsed custom-key value:dataclasses.replace(value, fields=...)used to write the stale original text fromCustomKeyValue.raw.rawis now used verbatim only if re-parsing it with the key'sCustomKeyDefgives the value's currentfields, otherwise the item is rebuilt fromfields. Unedited values still round-trip byte-exact.- The last database block in the file header is no longer dropped when the header runs straight into the first
>entry without a closing# //(now aPeffWarning), or when a# //separator has trailing whitespace. Previously itsPrefixandCustomKeyDefs were lost and its custom keys ended up inextra. - A blank line inside the file header no longer ends the header and discards the database blocks after it. It is skipped with a
PeffWarning(spec section 3.3.1: every header line starts with#). Blank lines between the header and the first entry are still ignored silently. write_peff()now escapes aProteoformid, so an id containing|or an unbalanced paren no longer writes an unparseable line.write_peff()no longer writes a trailing|for aDisulfideBondwithdescription=""(it read back asNone, so the text changed on a second write).- A quoted
DescriptionorRegExpin aCustomKeyDefmay now contain an unbalanced paren; it used to raisePeffParseErroron read. write_peff()raisesPeffWriteErrorinstead of writing a corrupt file when an edited field of aRegExp-controlled custom key can no longer be read back through the RegExp, when any header or entry value contains a line break, when a prefix contains:or whitespace, when adb_unique_idcontains whitespace, or when a sequence contains whitespace or>. The check runs before anything is written.- The reader now warns (
PeffWarning) on the spec's illegal examples it used to accept silently: aVariantSimplenew residue that is not one letter or*(section 3.3.8), aVariantComplexnew sequence with non-residue characters, or a single-residue substitution that should be aVariantSimple(3.3.9), and aProcesseditem without an accession or name (3.3.13).
Tests¶
tests/test_spec_examples.py: every example in the PEFF 1.0 specification (header, custom keys, entry rules, TYRO3, insulin, legal and illegal annotation examples) parses to the expected structure and writes back; fixtures intests/fixtures/spec/.tests/test_properties.py: Hypothesis properties for write -> read -> write stability, edited custom-key fields, and malformed input raising onlyPeffError.HYPOTHESIS_PROFILE=thoroughruns 5000 examples per property.
[0.4.4] (2026-09-23)¶
Fixed¶
- The description-line key scan now honours spec escapes (section 3.3.3): an escaped unbalanced paren (e.g. a
ModResnamedodd ( name, written asodd \( name) no longer swallows the following\Key=, so such entries round-trip. write_peff()now escapes the free-text fieldsPName,GName,TaxNameandComment(\,|, unbalanced parens) and the reader unescapes them, so values containing those characters or\Key=round-trip.- The deprecated
\Variant=key now warns withPeffWarninginstead ofDeprecationWarning, like every other spec issue. write_peff()now escapes custom-key fields built fromfields(keys without aRegExp), so a|or unbalanced paren in a field no longer reads back as an extra field or a parse error.PeffParseErrorraised while parsing an annotation value now carries the entry's line number (.line) instead ofNone.- The
Proteoformparse-error hint now showsannotIdRefsas optional, matching the parser (2 fields accepted). - README:
ModResUnimod/ModResPsiexamples usepositions, notposition. CITATION.cff:date-releasedfor 0.4.3 is 2026-09-23.
[0.4.3] (2026-09-23)¶
Changed¶
- First release archived on Zenodo; no code changes.
[0.4.2] (2026-09-23)¶
Fixed¶
- Zenodo archiving: removed the grant ids and hard-coded version from
.zenodo.json, which made Zenodo reject the previous release. Funding is now credited in the README.
[0.4.1] (2026-09-23)¶
- Publish from GitHub Actions with PyPI trusted publishing (
publish.yml); release metadata is checked against the tag. - Keep
__version__andCITATION.cffin sync withscripts/release_version.py(just set-version X.Y.Z). - CI tests Python 3.12-3.14 on Linux plus macOS and Windows, the lowest direct dependency versions, and the built wheel.
- Standardize citation and package metadata.
[0.4.0] (2026-07-10)¶
Breaking¶
DisulfideBond.positionsrenamed toDisulfideBond.annot_id_refsto reflect that a\DisulfideBondreferences priorModResPsientries by annotation ID, not by residue position (spec section 3.4.2).
Spec compliance¶
- Entry-item components now honor the spec's backslash escaping (section 3.3.3):
\|,\(,\), and\\parse as literals, andwrite_peff()emits those escapes so values containing pipes or unpaired parens round-trip. Balanced parens (e.g.N-linked (GlcNAc...)) are left unescaped. - Database-level
# GeneralComment=lines are now preserved onDatabaseHeader.general_commentsand re-emitted, instead of being silently dropped. DbNameis now included in the missing-mandatory-key warning for database headers.ProteoformDbandHasAnnotationIdentifiersheader flags are now recognized case-insensitively (tolerating the spec's ownProteoformDB/ singular-form variants).
Errors, warnings, and logging¶
- Added a
PeffErrorbase class (subclass ofValueError);PeffParseErrorandPeffWriteErrornow derive from it, so any library failure can be caught with a singleexcept PeffError. - Parse/write errors now carry an actionable
hint, andPeffParseErrorattaches itscontextandhintas exception notes (PEP 678) so they appear in tracebacks. - Non-fatal spec violations are now emitted through a dedicated
PeffWarningcategory (subclass ofUserWarning), so they can be filtered or escalated on their own: annotationMUST-rule checks (positions outside1..len(sequence), emptyVariantSimplenewAminoAcid, missing required accession/name onModRes*) plus the existing version/header/count/custom-value warnings. - Added
loggingthroughout the reader and writer under thepefftacular.parser/pefftacular.writerloggers (with a package-levelNullHandler), for a behavioral trace atDEBUG/INFO.
Tooling & docs¶
- Added
AGENTS.mdandCLAUDE.mdguidance for AI coding agents. - Reworked the
justfile:just checkis a single read-only pre-commit gate (format-check + lint + types + tests oversrcandtests), andjust fixauto-applies fixes. - Documented error handling, spec-violation warnings, and logging in the README.
[0.3.0] (2026-05-14)¶
- Header-declared custom keys (
# CustomKeyDef=) now drive typed parsing of entry values: registered keys are parsed via theirRegExp(with pipe-split as fallback) and coerced perFieldTypes(integer,decimal,boolean,date,time,string,enumeration). - Parsed custom-key values are exposed on
SequenceEntry.custom_values, with the raw string preserved alongside for lossless round-trip writing. - Undeclared custom keys continue to land in
SequenceEntry.extraunchanged. CustomKeyDefnow captures theConceptCURIEfield.- Multiple
# CustomKeyDef=lines per database are preserved instead of silently overwriting each other. - Field tokenization is now quote-aware so a
RegExpvalue containing escaped pipes survives splitting.
[0.2.0] (2026-03-18)¶
- Added
DisulfideBond,Proteoform,SequenceRange, andOptionalTagDefmodels. \DisulfideBond=and\Proteoform=entry keys now parse into typed fields instead ofextra.\OX=(UniProt NcbiTaxId alias) now maps toncbi_tax_id.\ID=,\DbUniqueId=, and\Comment=entry keys now parse into named fields.\Variant=(deprecated since 2015) now emits aDeprecationWarning.DatabaseHeadergains named fields:db_description,db_date,db_sources,decoy,conversion,has_annotation_identifiers,proteoform_db,optional_tag_defs.db_source: str | Nonereplaced bydb_sources: tuple[str, ...]to support multipleDbSourcelines.- Multi-value header keys (
DbSource,OptionalTagDef) no longer silently overwrite on repeated lines. annot_id: int | Nonefield added to all annotation models (VariantSimple,VariantComplex,ModResUnimod,ModResPsi,ModRes,Processed) forHasAnnotationIdentifiers=truedatabases.- Integer parsing errors (
NcbiTaxId,Length,SV,EV,PE,NumberOfEntries) now raisePeffParseError. Lengthmismatch between tag and actual sequence now emits aUserWarning.write_peff()now raisesPeffWriteErrorforNoneheader or entries with emptyprefix,db_unique_id, orsequence.- Added GitHub Actions CI workflow (Python 3.12 and 3.13).
- Added official PEFF example files as test fixtures.
[0.1.0] (2026-03-18)¶
- First release on PyPI.