Changelog¶
All notable changes to this project will be documented in this file.
[Unreleased]¶
Fixed¶
write_peff()no longer doubles the backslashes in a quotedCustomKeyDefvalue, so a header that declares aRegExpis written the way the spec writes it (ยง3.3.2 givesRegExp="([0-9]+)\|([0-9]+)\|...", one backslash before each pipe). Reading the spec's own example and writing it back producedRegExp="([0-9]+)\\|...", which another reader taking the quoted value literally compiles as an alternation around a literal backslash instead of a pipe. pefftacular's own reader undid its own doubling, so the models round-tripped and only the text was wrong. A file written by 1.3.0 or earlier still reads back to the same regex and is now rewritten in the spec's form. A value the literal form cannot hold, one with two consecutive backslashes or ending in one, still has every backslash escaped, since that is what reads back.
[1.3.0] (2026-11-14)¶
Added¶
from_records(), the inverse ofto_records(): it turns flat records (a data frame's rows) back intoSequenceEntryobjects forwrite_peff(). Each record must carry every key inRECORD_KEYS, and a missing key, an unknown key, a key set twice, a bad value or a value that would not read back as itself raisesPeffWriteErrorwith the record'sindex, because a dropped column is how a caller loses data without noticing.None, floatNaNand the pandas/polars missing markers mean the entry has no value for that key, while""is an empty value and is kept, since a PEFF file can hold one. The optional second argument is the file header, read only for itsCustomKeyDefdeclarations: without it a record'scustom_valuesraises instead of being read back as unknown keys, so those values cannot be silently dropped.
[1.2.2] (2026-11-06)¶
Fixed¶
- A FASTA header that repeats a key no longer reports two values for it.
SequenceEntry.from_fasta()kept the first value in the typed field and the last inextra, sosp|P1 GN=a GN=bgavegname="a"withextra["GN"]="b",to_fasta()wroteGNtwice, and reading that header back could keep the other value. A repeated key now keeps its first value and the later occurrences are dropped, matching the typed fields and fastatacular, andto_fasta()writes a key held by both a typed field andextraonce, from the typed field.
[1.2.1] (2026-10-26)¶
Fixed¶
- An entry line with leading whitespace (
>sp:B ...) starts a new entry instead of being appended to the previous entry's sequence. Leading whitespace is now ignored before checking for>,#and;on every line, as fastatacular does, inread_peff(),PeffReader, streams and compressed input. Indented#file header lines (including# PEFF 1.0) are read as header lines instead of raisingPeffParseError. Sequence lines still lose all whitespace.
[1.2.0] (2026-10-08)¶
Added¶
compressionargument onread_peff(),PeffReader,to_records()andwrite_peff(), typed by the new exported aliasCompression = Literal["infer", "gzip", "bz2", "xz"] | None(the same argument as fastatacular, after pandas)."infer"(the default) keeps today's behaviour: readers detect gzip, bzip2 or xz from the magic bytes, the writer compresses by the.gz/.bz2/.xzsuffix (any case), and open handles are plain. An explicit format overrides the suffix or content (a reader raisesPeffParseErrorif the bytes are not in that format) and works on binary handles, which are left open; with a text handle it raisesPeffErrorwith an "open in binary mode" hint.Noneis always plain text. Readers andwrite_peff()also accept binary handles for plain text. An unknown value raisesPeffErrorlisting the valid ones.
Fixed¶
SpecificKey/SpecificValuedatabase header lines (spec section 3.3.1) are no longer dropped: they are read intoDatabaseHeader.specific_keysasSpecificKeyDef(key_name, description, value)objects and written back, so they survive aread_peff()/write_peff()round trip.SequenceEntry.from_fasta()takes the accession of an NCBIgi|<number>|<db>|<accession>|header from the 4th field, with the database (ref,gb,emb,dbj,pdb,sp,tr...) as the prefix:gi|4557757|ref|NP_000240.1|givesref/NP_000240.1, notgi/4557757. UniProtsp|/tr|headers are unchanged.SequenceEntry.to_proforma()raisesPeffError(Nonewitherrors="skip") for a sequence holding a residue ProForma 2.0 cannot express, such as the PEFF interruption*or gap-, instead of returning an invalid ProForma string. A*removed by a stopVariantSimpletruncation is still fine. AVariantSimplesubstitution outsideA-Z(lowercase, non-ASCII) raises an error naming the variant.SequenceEntry.to_proforma()raisesPeffError(Nonewitherrors="skip") for a modification accession or name containing|or#, as it already did for a square bracket. ProForma 2.0 has no escape for them, so the string meant another modification:[M:a|b]reads as two alternative tags and[M:a#b]asain ambiguity groupb.SequenceEntry.to_proforma()writes a modification once per site when\ModResPsi/\ModResUnimodand\ModReslist it in different case or spacing (MOD:00046andmod:00046), instead of twice, which a ProForma reader counted as two modifications (double the mass shift). Accessions are compared ignoring case; the CV prefix is written in upper case.SequenceEntry.to_proforma()also writes a modification once per site when the same PSI-MOD or Unimod id is spelled with different zero-padding or a space after the colon (MOD:00046andMOD:46,MOD:00046andMOD: 00046,UNIMOD:21andUNIMOD:021), instead of twice (double the mass shift in a ProForma reader). Numeric accessions are compared as numbers and written in canonical form: PSI-MOD zero-padded to 5 digits (MOD:00046), Unimod unpadded (UNIMOD:21); a loneMOD:46is now writtenMOD:00046. Different CVs, and name tags ([M:name]) versus accessions, are never merged.write_peff()compresses a path ending in.gz,.bz2or.xzin that format, sowrite_peff(..., "x.peff.gz")round-trips throughread_peff()instead of writing plain text under a.gzname.
Changed¶
SequenceEntry.to_proforma()now writes PSI-MOD and Unimod accessions in canonical form, whatever spelling the PEFF file used:MOD:ids are zero-padded to 5 digits (MOD:46is written asMOD:00046) andUNIMOD:ids are unpadded (UNIMOD:021is written asUNIMOD:21), with the CV prefix in upper case. Code that comparesto_proforma()output against the original accession text may need updating.- 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.