memgres 0.12.2__tar.gz → 0.12.3__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.
- {memgres-0.12.2 → memgres-0.12.3}/PKG-INFO +1 -1
- {memgres-0.12.2 → memgres-0.12.3}/memgres/_version.py +1 -1
- {memgres-0.12.2 → memgres-0.12.3}/memgres/store.py +85 -5
- {memgres-0.12.2 → memgres-0.12.3}/memgres.egg-info/PKG-INFO +1 -1
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_require_title.py +34 -1
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_store_integration.py +32 -0
- {memgres-0.12.2 → memgres-0.12.3}/LICENSE +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/README.md +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/__init__.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/admin.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/admin_cli.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/blame.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/bootstrap.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/config.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/delimiters.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/diffing.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/embed_worker.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/embeddings.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/healthcheck.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/identity.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/indexing.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/info.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/lines.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/links.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/mcp_server.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0001_core.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0002_identity.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0003_history_author.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0004_title.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0005_chunk_index.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0006_reader_floor.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0007_embed_retry.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0008_service_roles.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0009_create_namespace_right.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0010_namespace_alias.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0011_drop_default_namespace.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0012_user_profile.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0013_hash_version.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0014_access_request_no_fk.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0015_normalize_tags.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0016_valid_at.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0017_memory_link.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0018_links_built.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0019_memory_usage.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0020_memory_usage_no_fk.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0021_enrollment_key.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0022_user_disabled.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/migrations/0023_relink_after_parser_fix.sql +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/paths.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/periodic.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/reembed.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/relink.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/schema.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/search.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/segments.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/server.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/tags.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/token_cli.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/vector/__init__.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/vector/base.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/vector/pgvector.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/vector/qdrant.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres/worker.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres.egg-info/SOURCES.txt +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres.egg-info/dependency_links.txt +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres.egg-info/entry_points.txt +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres.egg-info/requires.txt +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/memgres.egg-info/top_level.txt +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/pyproject.toml +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/setup.cfg +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_admin_two_way.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_blame_integration.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_chunk_index.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_claim_and_reembed.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_config.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_diffing.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_embed_worker.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_embeddings.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_enrollment.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_healthcheck.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_identity_integration.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_lexical_match.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_limits.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_links.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_list.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_mcp_admin_tools.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_mcp_error_messages.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_mcp_http_transport.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_mcp_instructions.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_mcp_recall_schema.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_mcp_tool_visibility.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_mcp_tool_visibility_http.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_migration_upgrade.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_multi_space_search.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_path_addressing.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_qdrant_ca.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_qdrant_integration.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_replace_build.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_required_fields.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_retention.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_roles_bootstrap.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_search_integration.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_security_followups.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_security_integration.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_segments.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_segments_store.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_server_info.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_server_integration.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_snippets.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_tags.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_token_sink.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_usage.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_valid_at.py +0 -0
- {memgres-0.12.2 → memgres-0.12.3}/tests/test_write_ergonomics.py +0 -0
|
@@ -451,6 +451,57 @@ def _slice_lines(m: "Memory", spec: str) -> "Memory":
|
|
|
451
451
|
return m
|
|
452
452
|
|
|
453
453
|
|
|
454
|
+
|
|
455
|
+
# How much body text a "why did my replace miss" hint may quote, per side.
|
|
456
|
+
_HINT_CONTEXT = 48
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _why_replace_missed(body: str, old: str) -> str:
|
|
460
|
+
"""Say WHY a substring edit found nothing — the refusal alone sends the author
|
|
461
|
+
to re-read the whole record, when the useful answer is almost always "your
|
|
462
|
+
quote is not what the body says, here is what it says".
|
|
463
|
+
|
|
464
|
+
Three causes, in the order they actually happen:
|
|
465
|
+
|
|
466
|
+
1. **Whitespace.** Bodies are hard-wrapped, so a phrase that reads as one line
|
|
467
|
+
on screen contains a newline. Retyped as a space, it can never match.
|
|
468
|
+
2. **Case.**
|
|
469
|
+
3. **Everything else** — then the most useful thing is the point where the
|
|
470
|
+
quote stops agreeing with the body, and what the body has instead.
|
|
471
|
+
|
|
472
|
+
The quote itself is usually reconstructed by eye rather than copied, which is
|
|
473
|
+
why the hint ends by naming the one reliable source: `memory_get`. A recall
|
|
474
|
+
snippet is a slice of the body (`lines` says which) EXCEPT on the ts_headline
|
|
475
|
+
path, where Postgres rebuilds the text from tokens and `lines` is null — that
|
|
476
|
+
one is not quotable at all.
|
|
477
|
+
"""
|
|
478
|
+
squashed_old = " ".join(old.split())
|
|
479
|
+
if squashed_old and squashed_old in " ".join(body.split()):
|
|
480
|
+
return (" — the text IS in the body, but the whitespace differs: bodies "
|
|
481
|
+
"wrap, so a line break in the record reads as a space on screen. "
|
|
482
|
+
"Copy the line from `memory_get`, not from a recall snippet")
|
|
483
|
+
if old.lower() in body.lower():
|
|
484
|
+
return (" — it is there in a different case. Copy it from `memory_get`, "
|
|
485
|
+
"not from a recall snippet")
|
|
486
|
+
# Longest prefix of `old` the body still agrees with: the point of divergence
|
|
487
|
+
# is what the author needs to see. Binary search — bodies are capped, but a
|
|
488
|
+
# linear scan would re-scan the whole body once per character.
|
|
489
|
+
lo, hi = 0, len(old)
|
|
490
|
+
while lo < hi:
|
|
491
|
+
mid = (lo + hi + 1) // 2
|
|
492
|
+
if old[:mid] in body:
|
|
493
|
+
lo = mid
|
|
494
|
+
else:
|
|
495
|
+
hi = mid - 1
|
|
496
|
+
if lo == 0:
|
|
497
|
+
return (" — not one character of it is in the body; wrong memory, or the "
|
|
498
|
+
"quote was written from memory rather than copied from `memory_get`")
|
|
499
|
+
at = body.find(old[:lo])
|
|
500
|
+
reads = body[at:at + lo + _HINT_CONTEXT]
|
|
501
|
+
return (f" — the first {lo} chars match, then they part: the body reads "
|
|
502
|
+
f"{reads!r}. Copy from `memory_get`, not from a recall snippet")
|
|
503
|
+
|
|
504
|
+
|
|
454
505
|
class Store:
|
|
455
506
|
def __init__(self, cfg: Config, embedder: Optional[Embedder] = None,
|
|
456
507
|
conn: Optional["psycopg.Connection"] = None,
|
|
@@ -773,11 +824,38 @@ class Store:
|
|
|
773
824
|
f"this deployment requires `{field}` on every write that stores "
|
|
774
825
|
f"content — {help_text}")
|
|
775
826
|
|
|
827
|
+
# How much of the title to quote either side of the cut. Enough to recognise
|
|
828
|
+
# the words, short enough that a pathological title cannot turn the refusal
|
|
829
|
+
# into a wall of text.
|
|
830
|
+
_TITLE_CUT_CONTEXT = 40
|
|
831
|
+
|
|
776
832
|
def _check_title_size(self, title: Optional[str]):
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
833
|
+
"""Refuse an oversized title, and SHOW WHERE IT STOPS FITTING.
|
|
834
|
+
|
|
835
|
+
The ceiling is in bytes because that is what storage and the wire count,
|
|
836
|
+
but the author is writing characters — and in UTF-8 the exchange rate
|
|
837
|
+
depends on the script: Latin runs ~1 B/char, Cyrillic ~2, so the same 256
|
|
838
|
+
B is ~256 characters in one language and ~148 in another. A refusal that
|
|
839
|
+
names only bytes leaves the author trimming blind, one attempt at a time
|
|
840
|
+
(observed: four rejected writes in a row on one record). So the message
|
|
841
|
+
carries the character counts and quotes the cut itself, marked with ✂ —
|
|
842
|
+
what survives on the left, what has to go on the right."""
|
|
843
|
+
if title is None:
|
|
844
|
+
return
|
|
845
|
+
size, cap = byte_len(title), self.cfg.max_title_bytes
|
|
846
|
+
if size <= cap:
|
|
847
|
+
return
|
|
848
|
+
# Truncating BYTES can land mid-character; decoding with "ignore" drops
|
|
849
|
+
# that partial character, which is exactly the last one that does NOT fit.
|
|
850
|
+
fits = title.encode("utf-8")[:cap].decode("utf-8", "ignore")
|
|
851
|
+
over = title[len(fits):]
|
|
852
|
+
ctx = self._TITLE_CUT_CONTEXT
|
|
853
|
+
left = ("…" if len(fits) > ctx else "") + fits[-ctx:]
|
|
854
|
+
right = over[:ctx] + ("…" if len(over) > ctx else "")
|
|
855
|
+
raise TooLarge(
|
|
856
|
+
f"title is {size}B > MEMGRES_MAX_TITLE_BYTES {cap}: "
|
|
857
|
+
f"{len(title)} chars, {len(fits)} fit, drop {len(over)} — "
|
|
858
|
+
f"{left}[✂]{right}")
|
|
781
859
|
|
|
782
860
|
def _create(self, ns, author, body, path, tags, source, reason,
|
|
783
861
|
title=None, valid_at=None) -> Memory:
|
|
@@ -868,7 +946,9 @@ class Store:
|
|
|
868
946
|
raise Conflict(f"stale replace: base {base_hash[:12]} != current {cur_hash[:12]}")
|
|
869
947
|
count = cur_body.count(old)
|
|
870
948
|
if count == 0:
|
|
871
|
-
raise ReplaceNotFound(
|
|
949
|
+
raise ReplaceNotFound(
|
|
950
|
+
f"replace text not found in body: {old[:60]!r}"
|
|
951
|
+
f"{_why_replace_missed(cur_body, old)}")
|
|
872
952
|
if count > 1 and not replace_all:
|
|
873
953
|
raise AmbiguousReplace(
|
|
874
954
|
f"replace text occurs {count}× — pass replace_all, or add "
|
|
@@ -26,7 +26,7 @@ psycopg = pytest.importorskip("psycopg")
|
|
|
26
26
|
|
|
27
27
|
from memgres.config import load # noqa: E402
|
|
28
28
|
from memgres.schema import migrate # noqa: E402
|
|
29
|
-
from memgres.store import MissingTitle, Store # noqa: E402
|
|
29
|
+
from memgres.store import MissingTitle, Store, TooLarge # noqa: E402
|
|
30
30
|
|
|
31
31
|
DSN = os.environ.get("MEMGRES_TEST_DSN",
|
|
32
32
|
"postgresql://memgres:memgres@localhost:55432/memgres")
|
|
@@ -181,3 +181,36 @@ def test_a_create_with_no_body_blames_the_body(conn, monkeypatch):
|
|
|
181
181
|
s = _store(conn, monkeypatch, True)
|
|
182
182
|
with pytest.raises(ValueError, match="needs a body"):
|
|
183
183
|
s.write(path="a.b")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ─── the size ceiling explains itself ────────────────────────────────────────
|
|
187
|
+
def test_an_oversized_title_shows_where_it_stops_fitting(conn, monkeypatch):
|
|
188
|
+
"""The ceiling counts bytes, the author counts characters, and in UTF-8 the
|
|
189
|
+
rate depends on the script — so a refusal naming only bytes leaves them
|
|
190
|
+
trimming blind. The message quotes the cut: what fits, what has to go."""
|
|
191
|
+
monkeypatch.setenv("MEMGRES_MAX_TITLE_BYTES", "40")
|
|
192
|
+
s = _store(conn, monkeypatch, True)
|
|
193
|
+
title = "Лимит заголовка меряется в байтах, а не в символах"
|
|
194
|
+
with pytest.raises(TooLarge) as e:
|
|
195
|
+
s.write(body="one", path="a.b", title=title)
|
|
196
|
+
msg = str(e.value)
|
|
197
|
+
# counts in BOTH units, and how much to drop
|
|
198
|
+
assert f"{len(title.encode())}B" in msg and "40" in msg
|
|
199
|
+
assert f"{len(title)} chars" in msg
|
|
200
|
+
# the cut is quoted, and the two sides really are the two sides
|
|
201
|
+
assert "[✂]" in msg
|
|
202
|
+
fits = title.encode()[:40].decode("utf-8", "ignore")
|
|
203
|
+
assert f"{len(fits)} fit" in msg
|
|
204
|
+
assert f"drop {len(title) - len(fits)}" in msg
|
|
205
|
+
assert fits[-10:] + "[✂]" in msg # left of the cut survives
|
|
206
|
+
assert "[✂]" + title[len(fits):][:10] in msg # right of it is what to drop
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_a_title_that_only_just_fits_is_accepted(conn, monkeypatch):
|
|
210
|
+
"""The boundary itself is not an error — byte_len == cap must pass, or the
|
|
211
|
+
limit silently means one byte less than it says."""
|
|
212
|
+
monkeypatch.setenv("MEMGRES_MAX_TITLE_BYTES", "40")
|
|
213
|
+
s = _store(conn, monkeypatch, True)
|
|
214
|
+
title = "я" * 20 # exactly 40 bytes
|
|
215
|
+
assert len(title.encode()) == 40
|
|
216
|
+
assert s.write(body="one", path="a.b", title=title).title == title
|
|
@@ -230,6 +230,38 @@ def test_replace_not_found_leaves_record_untouched(store):
|
|
|
230
230
|
assert again.body == "alpha\nbeta\n" and again.seq == 1
|
|
231
231
|
|
|
232
232
|
|
|
233
|
+
def test_a_missed_replace_says_the_quote_crossed_a_line_break(store):
|
|
234
|
+
"""The commonest miss: the body wraps, the phrase reads as one line on
|
|
235
|
+
screen, and the quote comes back with a space where the record has \n. The
|
|
236
|
+
refusal has to name that, or the author goes re-reading the whole record."""
|
|
237
|
+
store.write(body="хвост ниши по деньгам — перепродажа\nExa через x402\n",
|
|
238
|
+
path="a.b", title="A")
|
|
239
|
+
with pytest.raises(ReplaceNotFound) as e:
|
|
240
|
+
store.write(at="a.b", replace=("перепродажа Exa", "перепродажа Exa Search"))
|
|
241
|
+
msg = str(e.value)
|
|
242
|
+
assert "whitespace differs" in msg and "memory_get" in msg
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_a_missed_replace_shows_where_the_quote_parts_from_the_body(store):
|
|
246
|
+
"""When it is not whitespace, the useful answer is the point of divergence
|
|
247
|
+
and what the body actually says there."""
|
|
248
|
+
store.write(body="верх ниши — **«rail dead»** значит другое\n",
|
|
249
|
+
path="a.c", title="A")
|
|
250
|
+
with pytest.raises(ReplaceNotFound) as e:
|
|
251
|
+
store.write(at="a.c", replace=("верх ниши — «rail dead»", "x"))
|
|
252
|
+
msg = str(e.value)
|
|
253
|
+
assert "chars match" in msg and "«rail dead»**" in msg
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_a_replace_of_something_absent_says_so_plainly(store):
|
|
257
|
+
"""No prefix at all is a different mistake — wrong record, or a quote written
|
|
258
|
+
from memory — and must not be dressed up as a near miss."""
|
|
259
|
+
store.write(body="alpha\nbeta\n", path="a.d", title="A")
|
|
260
|
+
with pytest.raises(ReplaceNotFound) as e:
|
|
261
|
+
store.write(at="a.d", replace=("zzz", "x"))
|
|
262
|
+
assert "not one character" in str(e.value)
|
|
263
|
+
|
|
264
|
+
|
|
233
265
|
def test_replace_ambiguous_requires_all_or_context(store):
|
|
234
266
|
m = store.write(body="x x x\n")
|
|
235
267
|
with pytest.raises(AmbiguousReplace):
|
|
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
|
|
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
|
|
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
|
|
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
|