kvgit 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {kvgit-0.2.0 → kvgit-0.2.1}/PKG-INFO +1 -1
  2. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/staged.py +36 -11
  3. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit.egg-info/PKG-INFO +1 -1
  4. {kvgit-0.2.0 → kvgit-0.2.1}/pyproject.toml +1 -1
  5. {kvgit-0.2.0 → kvgit-0.2.1}/tests/test_staged.py +111 -0
  6. {kvgit-0.2.0 → kvgit-0.2.1}/LICENSE +0 -0
  7. {kvgit-0.2.0 → kvgit-0.2.1}/README.md +0 -0
  8. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/__init__.py +0 -0
  9. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/content_types.py +0 -0
  10. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/encoding.py +0 -0
  11. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/errors.py +0 -0
  12. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/hamt.py +0 -0
  13. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/kv/__init__.py +0 -0
  14. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/kv/base.py +0 -0
  15. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/kv/composite.py +0 -0
  16. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/kv/disk.py +0 -0
  17. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/kv/indexeddb.py +0 -0
  18. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/kv/memory.py +0 -0
  19. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/namespaced.py +0 -0
  20. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/py.typed +0 -0
  21. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/store.py +0 -0
  22. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/versioned/__init__.py +0 -0
  23. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/versioned/base.py +0 -0
  24. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/versioned/gp.py +0 -0
  25. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/versioned/helpers.py +0 -0
  26. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/versioned/keyset.py +0 -0
  27. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/versioned/kv.py +0 -0
  28. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/versioned/merge.py +0 -0
  29. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit/versioned/protocol.py +0 -0
  30. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit.egg-info/SOURCES.txt +0 -0
  31. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit.egg-info/dependency_links.txt +0 -0
  32. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit.egg-info/requires.txt +0 -0
  33. {kvgit-0.2.0 → kvgit-0.2.1}/kvgit.egg-info/top_level.txt +0 -0
  34. {kvgit-0.2.0 → kvgit-0.2.1}/setup.cfg +0 -0
  35. {kvgit-0.2.0 → kvgit-0.2.1}/tests/test_content_types.py +0 -0
  36. {kvgit-0.2.0 → kvgit-0.2.1}/tests/test_hamt.py +0 -0
  37. {kvgit-0.2.0 → kvgit-0.2.1}/tests/test_namespaced.py +0 -0
  38. {kvgit-0.2.0 → kvgit-0.2.1}/tests/test_store_factory.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kvgit
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Versioned key-value store with git-like commit, branch, and merge semantics.
5
5
  Author: ashenfad
6
6
  License: MIT
@@ -140,6 +140,7 @@ class Staged(MutableMapping[str, Any]):
140
140
  def commit(
141
141
  self,
142
142
  *,
143
+ keys: set[str] | None = None,
143
144
  on_conflict: str = "raise",
144
145
  merge_fns: dict[str, MergeFn] | None = None,
145
146
  default_merge: MergeFn | None = None,
@@ -148,20 +149,36 @@ class Staged(MutableMapping[str, Any]):
148
149
  """Flush staged changes to the underlying Versioned store.
149
150
 
150
151
  Encodes staged values to bytes, wraps merge functions, and
151
- calls ``Versioned.commit()``. On success, clears the staging
152
- buffer.
152
+ calls ``Versioned.commit()``. On success, clears the committed
153
+ entries from the staging buffer.
154
+
155
+ Args:
156
+ keys: If provided, only commit these specific keys.
157
+ Keys not in ``_updates`` or ``_removals`` are silently
158
+ ignored. Uncommitted keys remain staged for a future
159
+ commit. When ``None`` (default), all staged changes
160
+ are committed.
153
161
 
154
162
  Returns:
155
163
  A MergeResult (truthy when committed).
156
164
  """
157
- # Encode staged updates to bytes
165
+ # Encode staged updates to bytes — scoped to keys if provided
158
166
  encoded_updates: dict[str, bytes] | None = None
159
- if self._updates:
160
- encoded_updates = {
161
- key: self._encoder(value) for key, value in self._updates.items()
162
- }
163
-
164
- removals = self._removals if self._removals else None
167
+ if keys is not None:
168
+ # Iterate the (typically small) keys set, not the full _updates dict
169
+ matched_updates = {k: self._updates[k] for k in keys if k in self._updates}
170
+ if matched_updates:
171
+ encoded_updates = {
172
+ k: self._encoder(v) for k, v in matched_updates.items()
173
+ }
174
+ # .intersection() accepts any iterable, not just sets
175
+ removals = self._removals.intersection(keys) or None
176
+ else:
177
+ if self._updates:
178
+ encoded_updates = {
179
+ key: self._encoder(value) for key, value in self._updates.items()
180
+ }
181
+ removals = self._removals if self._removals else None
165
182
 
166
183
  # Build effective merge fns and wrap to bytes-level
167
184
  effective_fns = dict(self._merge_fns)
@@ -188,8 +205,16 @@ class Staged(MutableMapping[str, Any]):
188
205
  info=info,
189
206
  )
190
207
  if result.merged:
191
- self._updates.clear()
192
- self._removals.clear()
208
+ if keys is not None:
209
+ # Only clear the committed keys from staging
210
+ for k in keys:
211
+ self._updates.pop(k, None)
212
+ self._removals.discard(k)
213
+ else:
214
+ self._updates.clear()
215
+ self._removals.clear()
216
+ # Always clear the full read cache — HEAD moved, so cached
217
+ # values from other keys may be stale after a merge.
193
218
  self._cache.clear()
194
219
  return result
195
220
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kvgit
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Versioned key-value store with git-like commit, branch, and merge semantics.
5
5
  Author: ashenfad
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kvgit"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Versioned key-value store with git-like commit, branch, and merge semantics."
9
9
  authors = [{ name = "ashenfad" }]
10
10
  requires-python = ">=3.10"
@@ -173,6 +173,117 @@ class TestStagedCommit:
173
173
  assert s.versioned.commit_info() == {"author": "test"}
174
174
 
175
175
 
176
+ class TestStagedPartialCommit:
177
+ """Tests for commit(keys=...) partial commit support."""
178
+
179
+ def test_partial_commit_only_flushes_specified_keys(self):
180
+ store = Memory()
181
+ v = Versioned(store)
182
+ s = Staged(v)
183
+ s["a"] = 1
184
+ s["b"] = 2
185
+ result = s.commit(keys={"a"})
186
+ assert result.merged
187
+
188
+ # "a" is committed and no longer staged
189
+ assert not s.is_staged("a")
190
+ # "b" is still staged
191
+ assert s.is_staged("b")
192
+ assert s["b"] == 2
193
+
194
+ def test_partial_commit_persists_committed_keys(self):
195
+ store = Memory()
196
+ v = Versioned(store)
197
+ s = Staged(v)
198
+ s["a"] = 1
199
+ s["b"] = 2
200
+ s.commit(keys={"a"})
201
+
202
+ # Verify "a" is persisted (visible from a fresh Staged)
203
+ s2 = Staged(Versioned(store))
204
+ assert s2.get("a") == 1
205
+ # "b" is NOT persisted (still only in s's staging buffer)
206
+ assert s2.get("b") is None
207
+
208
+ def test_partial_commit_with_info(self):
209
+ s = Staged(Versioned())
210
+ s["a"] = 1
211
+ s["b"] = 2
212
+ result = s.commit(keys={"a"}, info={"message": "just a"})
213
+ assert result.merged
214
+ assert s.versioned.commit_info()["message"] == "just a"
215
+
216
+ def test_partial_commit_ignores_unknown_keys(self):
217
+ s = Staged(Versioned())
218
+ s["a"] = 1
219
+ # "ghost" is not staged — should be silently ignored
220
+ result = s.commit(keys={"a", "ghost"})
221
+ assert result.merged
222
+ assert not s.is_staged("a")
223
+
224
+ def test_partial_commit_with_removals(self):
225
+ s = Staged(Versioned())
226
+ s["a"] = 1
227
+ s["b"] = 2
228
+ s.commit()
229
+
230
+ del s["a"]
231
+ s["c"] = 3
232
+ # Partial commit: only the removal of "a"
233
+ s.commit(keys={"a"})
234
+ assert s.get("a") is None # deleted
235
+ assert s.is_staged("c") # still pending
236
+
237
+ def test_partial_commit_clears_full_cache(self):
238
+ """Regression test: after a partial commit, the entire read cache
239
+ must be cleared because HEAD moved. Cached values from non-committed
240
+ keys could be stale if a concurrent writer modified them.
241
+ """
242
+ store = Memory()
243
+ v = Versioned(store)
244
+ s = Staged(v)
245
+
246
+ s["x"] = "original_x"
247
+ s["y"] = "original_y"
248
+ s.commit()
249
+
250
+ # Read "y" to populate the cache
251
+ assert s["y"] == "original_y"
252
+
253
+ # Simulate a concurrent writer updating "y" on the same branch
254
+ v2 = Versioned(store)
255
+ v2.commit({"y": s._encoder("updated_by_other")})
256
+
257
+ # Partial commit of "x" — HEAD moves to include the concurrent write
258
+ s["x"] = "new_x"
259
+ s.commit(keys={"x"})
260
+
261
+ # After partial commit, "y" should reflect the concurrent update,
262
+ # NOT the stale cached value. Before the fix, only "x" was evicted
263
+ # from cache, leaving "y" stale as "original_y".
264
+ s.refresh()
265
+ assert s["y"] == "updated_by_other"
266
+
267
+ def test_keys_accepts_non_set_iterable(self):
268
+ """keys parameter should accept any iterable, not just sets."""
269
+ s = Staged(Versioned())
270
+ s["a"] = 1
271
+ s["b"] = 2
272
+ # Pass a list instead of a set
273
+ result = s.commit(keys=["a"])
274
+ assert result.merged
275
+ assert not s.is_staged("a")
276
+ assert s.is_staged("b")
277
+
278
+ def test_full_commit_unchanged_when_keys_is_none(self):
279
+ """Without keys, commit flushes everything (backwards compatible)."""
280
+ s = Staged(Versioned())
281
+ s["a"] = 1
282
+ s["b"] = 2
283
+ s.commit()
284
+ assert not s.has_changes
285
+
286
+
176
287
  class TestStagedReset:
177
288
  def test_reset_clears_staging(self):
178
289
  s = Staged(Versioned())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes