Repository navigation
Description
unbind() validates its arguments against _CONFIG_KEYS, which includes use_revision — but bind() has no use_revision parameter, because the revision_id field must exist in the class namespace before pydantic builds the class. So use_revision can be unbound and never re-bound: the field stays, the protocol stops.
What happens
Model.unbind() with no arguments removes every config key this class set (src/tinydantic/_model.py:1016-1017), use_revision among them. _uses_revision() then resolves False, so:
- writes stop rotating
revision_id(it staysNonein every new document), save(),replace(), anddelete()stop checking the held token,- the
revision_idfield itself is still there, still serialized, still inmodel_fields.
The model looks revisioned and behaves unrevisioned. bind() cannot put it back.
This is reachable through the documented teardown idiom: a fixture that binds a database in setup and calls Model.unbind() in teardown takes use_revision with it, so a later test on the same class runs without the concurrency guard.
Reproduction
from tinydb import TinyDB
from tinydb.storages import MemoryStorage
from tinydantic import StaleDocumentError, TinydanticModel
db = TinyDB(storage=MemoryStorage)
class Doc(TinydanticModel, database=db, use_revision=True):
text: str
first = Doc(text="a").insert()
print("token after insert:", first.revision_id is not None)
Doc.unbind()
Doc.bind(database=db)
second = Doc(text="b").insert()
print("token after unbind + rebind:", second.revision_id is not None)
print("revision_id still a field:", "revision_id" in Doc.model_fields)
stale = Doc.get_by_id(first.id)
Doc.update_by_ids({"text": "changed by someone else"}, [first.id])
try:
stale.save()
except StaleDocumentError:
print("stale save: rejected")
else:
print("stale save: accepted")Run with python repro.py.
Actual output:
token after insert: True
token after unbind + rebind: False
revision_id still a field: True
stale save: accepted
Expected: either unbind() refuses use_revision with the same explanation bind() carries (it cannot be late-bound, so it cannot be late-unbound either), or bare unbind() skips it. Nothing should be able to leave a model with a revision_id field and no revision protocol.
Environment: tinydantic 0.4.0 at 719ca78, Python 3.10.20, TinyDB 4.9.0, pydantic 2.13.4, macOS. Run 2026-08-13.
What to do
Make _CONFIG_KEYS membership the wrong test for unbind(). Either raise ValueError for an explicit unbind("use_revision") — naming the class-keyword-only rule — and skip it in the bare-unbind() sweep, or drop use_revision from what unbind() accepts entirely.
What was not done
Verified against the current tree; no fix was built or tested. Whether an explicit unbind("use_revision") should raise or be a documented no-op is a design call.
Found while fixing M1 of the 2026-08-13 docs and docstring audit, which flagged bind()'s docstring for claiming every config key can be bound late.
Assisted-by: Claude:claude-opus-5
Activity