# Changelog
# 1.0.3 - 2026-09-29
# Changed
- Documentation only: the README's Documentation list links each page on the documentation site, so readers on GitHub and PyPI land there. The code is the same as 1.0.2.
# 1.0.2 - 2026-09-29
# Changed
- Documentation only: the README becomes a short entrance to the documentation
site at https://offerrall.github.io/pytypehintstore/, and
docs/overview.mdholds the introduction and the full walk-through, linked toexample.py. - The fingerprint shown in the overview and on the lock page is the real one,
Task.ef8e8a47.json; the file page shows the class its sample row belongs to. - "Known limits" becomes "Limits". The lock's POSIX and Windows behaviour moves to the lock page, and release history moves out of the pages into this file.
- The package metadata gains a
DocumentationURL. - Every heading in this changelog carries its release date.
The code is the same as 1.0.1.
# 1.0.1 - 2026-09-29
Documentation only; the code is the same as 1.0.0.
- The README keeps the overview, install, quick start and ecosystem; the rest
moves to
docs/, one page per topic. - The Changelog link in the package metadata points to the
mainbranch, which is where this file lives.
# 1.0.0 - 2026-08-10
The store keeps what is genuinely its own — the disk, the lock, the identity of
a schema, the life of a process — and stops having opinions about the contract.
Every reading of a row was a second dialect maintained beside the core's, and a
dialect that agrees today is a dialect that diverges later. pytypehint 1.0.0
publishes the operations that make the second one unnecessary, so this release
deletes it.
- The dependency is
pytypehint==1.0.0, pinned exact rather than floored. The codec importspytypehint.validation.value_branch, which is the core's own router and carries no public promise; one author owns both packages, and a pin plustest_codec_router_contractis what turns a move upstream into a failing test rather than a file naming the wrong option. codec.decodeis gone, and with it_decode_options,_decode_dict,_decode_list,_decode_str,_convert,_core_typeand_field. A row is read back withschema.decode, which the core published for exactly this and does better: the spellings it accepts for a date and a time are fixed and disjoint rather than delegated tofromisoformat, a malformed wrapper is left whole instead of half-read, and a non-string key is refused before anything probes it. Before the deletion the two were compared over the vocabulary, the wrappers, the malformed wrappers and every enum shape including aStrEnummixin and an alias; they agreed in every case.- Writing still belongs here — the core describes and validates, and turning a
live instance into the tree that goes on disk is the file's business — but the
router underneath it does not.
codec._branch_ofandcodec._acceptsare replaced byvalue_branch, so the option the file names is the option validation would have chosen, by construction rather than by coincidence. store._ambiguousandstore._shared_nameare gone. They existed because the old core compiled a union whose options share a transport name — an enum class calleddatebeside adate— and the store refused it at the door to keep a row from coming back as the wrong thing. The core now applies the identity rule at compilation, inside lists included, so the schema never exists. The refusal a caller sees is the core's own, with the core's coordinates, and the store adds nothing to it.
Three limits declared in the README are gone with them:
- A union of lists differing only in a constraint.
Annotated[list[str], Min(1)] | list[int]with[]used to be written under the branch that rejects it, soaddrefused a row the core had just called valid. The core's router reads the constraints, because it calls_check. - A union whose options share a transport name. Refused by the core at compilation now, rather than by the store at open.
- A date in another ISO spelling.
"20260101"and"2026-W01-1"used to load, becausedate.fromisoformataccepts the whole of ISO 8601, and the next dump rewrote them in the one form the store emits — turning a week date into the Monday it names, in another year, without a word. The core pinned the canonical spellings, so these are refused out loud instead.
Breaking, and the reason it is declared rather than absorbed:
-
Fingerprints move, so every store gets a new file. The core renamed the
Strfieldis_path_filetofile_hint, and a field name is part of the text a schema is rendered as. This moves the fingerprint of every schema holding astrat any depth — a field, a list item, a union option, a nested dataclass — which in practice is nearly every schema. A schema with nostranywhere keeps its fingerprint and its file.What moved is the rendering of the contract, not the contract: the same dataclass describes the same rows, and the old file is valid against the new schema in every byte. So this is the one case where the usual answer — a changed contract is a separate database, never a migration — is answering a question nobody asked. Rename the file from
Class.<old>.jsontoClass.<new>.jsonand it loads: ids,next_idand every row survive, because the transport form is unchanged from 0.0.4 (verified field by field across the vocabulary and all three wrappers). Get the new name by opening a store on an empty directory and readingstore.path.Two things to know before renaming. A file holding a date or time in a non-canonical ISO spelling now fails the load rather than being read and rewritten — loudly, naming the row. And a row whose
FileHintfile has since moved or grown now loads where it used to be refused. If you would rather revalidate than rename, re-import throughadd()from the old JSON; the old file is never read and never deleted either way. -
FileHintreplacesIsPathFile, and the core stopped checking the file. A row whose file was moved, deleted or grown past its maximum no longer stops a load: the extension is validated because the text settles it, and existence and size are questions for whoever opens the file. The one declared exception to "a row that went in comes back out" is therefore gone.
What 1.0.0 means here: the documented surface is the real one, and the store interprets the contract in zero places. What it adds is a file, a lock, an identity and a lifecycle — and it adds nothing else.
# 0.0.4 - 2026-07-30
0.0.3 was tagged but not published to PyPI; its changes are part of this entry.
One library on both platforms: the lock now holds the same contract on POSIX that it held on Windows, and the checker agrees on both.
- The lock is settled by the operating system on POSIX too. It was an open
handle on Windows — a file Windows will not unlink while it is open — and a
bare
O_EXCLeverywhere else, which POSIX does not defend: two processes reaching for one orphaned lockfile could both unlink it and both create their own, ending as two live owners of one file. POSIX now takes an advisoryflock, which the kernel drops when a process dies, and checks the inode afterwards in case the file was replaced between the open and the lock. - An orphan needs no stealing on POSIX: what makes it an orphan is that its
flockdied with its owner. The behaviour a caller sees is unchanged — one owner, orphans reclaimed, the same message naming the live PID. releaseunlinks before closing on POSIX and after closing on Windows, each being the order that cannot hand a second owner the lock.mypypasses on both platforms. The Windows probe is declared undersys.platformrather thanos.name, which is the form a checker reads as a platform guard, soctypes.WinDLLis not looked for on Linux.- CI runs the suite on
ubuntu-latestandwindows-latest, over 3.11, 3.12 and 3.13. Half of the lock only exists on one of them. - New test: two processes reaching for one orphan at the same instant leave a single owner. It is the property both halves exist to hold, and it runs on both.
- One difference is left, and declared: with a lockfile nobody is holding, POSIX
trusts the
flockand takes it over — so a recycled pid cannot lock a path out there — while Windows trusts the pid written in it and refuses. Two live stores behave identically on both.
# 0.0.2 - 2026-07-30
Out of a stress campaign: 1150 generated schemas, four adversarial fronts, and the suite from 200 tests to 1583.
- Fixed, data loss:
fingerprintwalked a schema as a tree when it is a graph — the core compiles a class once and shares it between every field naming it — so a class the core compiled in a millisecond could take minutes to fingerprint, andstore_ofhung with no error and no output. Each dataclass is now written once and referred back to. Schemas with the same class in two fields change their fingerprint, and so their file name. - Fixed, data loss: a lone surrogate was accepted by
add, could never be written to a UTF-8 file, and froze every later dump — the failure surfacing only atclose(), as an error from another library. Such a row is refused ataddwithSchemaValueError: cannot be written as UTF-8, after the core has had its say. - Fixed, data loss: a union whose options share a transport name — an enum class
called
datebesidedateitself — routed values to the wrong branch, and a stored member came back as a plain string. The store now refuses the schema at open, naming the field and the shared name, before the lockfile is taken. Real collisions only: the same enum with nothing to collide with still works. - A
$type/$valuewrapper is those two keys and nothing else. A key beside them, or a$valuethat is itself a wrapper, travels intact for the core to refuse instead of being read anyway and erased by the next dump. - The format version is checked by type as well as by value, so a
1.0or atrueinvis no longer read as version 1. - Packaging: classifiers, keywords and project URLs for PyPI.
# 0.0.1 - 2026-07-30
First release. Rows of one pytypehint-validated dataclass, kept in memory and
mirrored to a JSON file a person can open and edit.
store_of(cls, directory, *, debounce=2.0, keep=5)opens the store of one dataclass on a file named for the class and the fingerprint of its schema. Reads answer from memory; writes mutate the dict at once and a writer thread dumps the whole statedebounceseconds after the burst of writes stops.addandputaccept a row by making the round trip it will make anyway: the instance is encoded to its transport form and rebuilt through the schema, so what the store holds always came out of the constructor and always survives the file. Validation failures are the core's ownSchemaTypeErrorandSchemaValueError, untouched; the store adds no second validation system.- The file carries the transport form — ISO text for a date or a time, the member name for an enum, an object for a nested dataclass — so it stays readable and editable. A row that cannot be read back names the file, the row and the core's error.
- Dumps are atomic:
.part,fsync,os.replace. A dump that fails leaves nothing of itself behind, keeps the state unwritten and is retried; the writer thread never dies of it. The previous file is rotated aside with a nanosecond stamp and copies beyondkeepare pruned. close()dumps what is pending, stops the writer and releases the lock. It is idempotent, and a failure on the way down reaches every caller that was closing the same store. Anatexithook closes a store the caller forgot, on a normal interpreter exit.- One process owns a store. The lockfile holds the owner's PID and stays open for as long as the lock is held, so a second process is refused at startup rather than corrupting the file later, and two processes racing for the same orphaned lock cannot both end up owning it.
- Three exceptions, all subclasses of
StoreError:StoreLockedError,StoreLoadError, andStoreErroritself for an operation on a closed store.