From cfc614cd72d977cfd1758fa62fec1fec5224be60 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Sun, 5 Jul 2026 19:04:51 +0200 Subject: [PATCH] Small wins: confirm_overwrite preset (#13), remove dead codec stubs (#68), drop redundant windows_ci.yml (#67) #13: add `confirm_overwrite`, a wrap_kvs preset that asks for typed confirmation before overwriting an existing key that holds a DIFFERENT value, plus a `mk_confirm_overwrite_preset(get_input=..., prompt=...)` factory for customizing / testing. Both exported. Declined overwrites keep the existing value. #68: remove KeyValueCodecs.key_based / .extension_based -- empty stub methods that silently returned None (No Silent Failures). They were unused; the building-block module functions remain for a future real implementation. #67: remove .github/workflows/windows_ci.yml. Windows CI already runs automatically on push/PR via ci.yml's `windows-validation` ("Windows Tests") job, so the workflow_dispatch-only file was a redundant, confusing duplicate. Closes #13 Closes #68 Closes #67 Claude-Session: https://claude.ai/code/session_01H8PmV6xmMhzV64zi1Twraw --- .github/workflows/windows_ci.yml | 39 ----------------- dol/__init__.py | 2 + dol/kv_codecs.py | 21 ++-------- dol/tests/test_confirm_overwrite.py | 45 ++++++++++++++++++++ dol/trans.py | 65 +++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 56 deletions(-) delete mode 100644 .github/workflows/windows_ci.yml create mode 100644 dol/tests/test_confirm_overwrite.py diff --git a/.github/workflows/windows_ci.yml b/.github/workflows/windows_ci.yml deleted file mode 100644 index 8c3d1ee1..00000000 --- a/.github/workflows/windows_ci.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Windows CI -on: - workflow_dispatch: -env: - PROJECT_NAME: dol -jobs: - windows-validation: - name: Windows Validation - if: "!contains(github.event.head_commit.message, '[skip ci]')" - runs-on: windows-latest - strategy: - matrix: - python-version: ["3.10"] - - steps: - - uses: actions/checkout@v6 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Package - run: | - python -m pip install --upgrade pip - python -m pip install . - # If pytest is not already installed through setup.cfg or setup.py requirements, - # include the line below: - python -m pip install pytest - - - name: Run Tests - run: python -m pytest - # run: python -m pytest --maxfail=1 --disable-warnings - - # - name: Pytest Validation - # uses: i2mint/isee/actions/pytest-validation@master - # with: - # root-dir: ${{ env.PROJECT_NAME }} - # paths-to-ignore: scrap diff --git a/dol/__init__.py b/dol/__init__.py index 486df78d..9514c02d 100644 --- a/dol/__init__.py +++ b/dol/__init__.py @@ -123,6 +123,8 @@ def ihead(store, n=1): from dol.trans import ( wrap_kvs, # transform store key and/or value FirstArgIsMapping, # mark a transform whose first arg is the store (self), not data + confirm_overwrite, # wrap_kvs preset: confirm before overwriting a differing value + mk_confirm_overwrite_preset, # factory for a customizable confirm_overwrite preset filt_iter, # filter store keys (and contains ready to use filters as attributes) cached_keys, # cache store keys add_decoder, # add a decoder (i.e. outcomming value transformer) to a store diff --git a/dol/kv_codecs.py b/dol/kv_codecs.py index 70503830..8444fbda 100644 --- a/dol/kv_codecs.py +++ b/dol/kv_codecs.py @@ -577,20 +577,7 @@ class KeyValueCodecs(CodecCollection): A collection of key-value codecs that can be used with postget and preset kv_wraps. """ - def key_based( - key_mapping: dict, - key_func: Callable = identity_func, - *, - default: Callable | None = None, - ): - """A factory that creates a key-value codec that uses the key to determine the - value codec to use.""" - - def extension_based( - ext_mapping: dict = dflt_ext_mapping, - *, - default: Callable | None = None, - ): - """A factory that creates a key-value codec that uses the file extension to - determine the value codec to use.""" - # TODO: Implement this function - ext_mapping parameter is currently unused + # Note: ``key_based`` / ``extension_based`` factories were removed here (Issue #68): + # they were empty stubs that silently returned ``None``. The building blocks live as + # module-level functions (:func:`key_based_codec_factory`, :func:`key_based_value_trans`) + # if/when a key/extension-dispatching value codec is implemented for real. diff --git a/dol/tests/test_confirm_overwrite.py b/dol/tests/test_confirm_overwrite.py new file mode 100644 index 00000000..2c8f814c --- /dev/null +++ b/dol/tests/test_confirm_overwrite.py @@ -0,0 +1,45 @@ +"""Tests for the confirm_overwrite wrap_kvs preset (Issue #13).""" + +import pytest + +from dol import wrap_kvs, confirm_overwrite, mk_confirm_overwrite_preset + + +def test_confirm_overwrite_no_prompt_for_same_value_or_new_key(monkeypatch): + # confirm_overwrite uses the builtin input; it must NOT be called in these cases + monkeypatch.setattr("builtins.input", lambda prompt="": pytest.fail("prompted!")) + d = wrap_kvs(dict(a="apple", b="banana"), preset=confirm_overwrite) + d["a"] = "apple" # same value under existing key -> no prompt + d["c"] = "coconut" # brand new key -> no prompt + assert dict(d) == {"a": "apple", "b": "banana", "c": "coconut"} + + +def test_confirm_overwrite_confirmed(monkeypatch): + monkeypatch.setattr("builtins.input", lambda prompt="": "alligator") + store = dict(a="apple") + d = wrap_kvs(store, preset=confirm_overwrite) + d["a"] = "alligator" # differing value; user types the new value -> overwrite + assert store["a"] == "alligator" + + +def test_confirm_overwrite_declined(monkeypatch): + monkeypatch.setattr("builtins.input", lambda prompt="": "") # anything != new value + store = dict(a="apple") + d = wrap_kvs(store, preset=confirm_overwrite) + d["a"] = "alligator" # differing value; declined -> keep existing + assert store["a"] == "apple" + + +def test_mk_confirm_overwrite_preset_injectable_input(): + typed = [] + + def fake_input(prompt): + typed.append(prompt) + return "yes-please" + + preset = mk_confirm_overwrite_preset(get_input=fake_input) + store = dict(k="old") + d = wrap_kvs(store, preset=preset) + d["k"] = "yes-please" # matches what fake_input returns -> overwrite + assert store["k"] == "yes-please" + assert typed and "old" in typed[0] # the prompt mentioned the existing value diff --git a/dol/trans.py b/dol/trans.py index 0759b129..d7283625 100644 --- a/dol/trans.py +++ b/dol/trans.py @@ -2444,6 +2444,71 @@ def __call__(self, *args, **kwargs): raise TypeError(f"{self._obj=} is not callable") +def _dflt_overwrite_prompt(k, existing, new): + return ( + f"The key {k} already exists and has value {existing}. " + f"If you want to overwrite it with {new}, confirm by typing {new} here: " + ) + + +def mk_confirm_overwrite_preset( + *, + get_input: Callable[[str], str] | None = None, + prompt: Callable = _dflt_overwrite_prompt, +): + """Make a ``wrap_kvs`` ``preset`` that asks for confirmation before overwriting an + existing key that holds a *different* value. + + The returned preset writes ``v`` unchanged unless ``k`` already maps to a different + value; in that case it asks ``get_input`` to confirm (by typing the new value), and + keeps the existing value if the confirmation doesn't match. Use ``get_input`` to + customize how confirmation is obtained (``None`` -> the builtin ``input``, resolved at + call time), e.g. for testing or for a non-interactive policy. See + :func:`confirm_overwrite`. + + >>> store = dict(a='apple', b='banana') + >>> # simulate a user who always types the exact new value (confirms every overwrite) + >>> always_yes = wrap_kvs( + ... store, preset=mk_confirm_overwrite_preset(get_input=lambda prompt: 'alligator') + ... ) + >>> always_yes['a'] = 'alligator' # confirmation matches -> overwrites + >>> store['a'] + 'alligator' + >>> # simulate a user who declines (types something that doesn't match) + >>> always_no = wrap_kvs( + ... store, preset=mk_confirm_overwrite_preset(get_input=lambda prompt: 'nope') + ... ) + >>> always_no['a'] = 'anteater' # declined -> existing value kept + >>> store['a'] + 'alligator' + """ + + def confirm_overwrite(self, k, v): + _sentinel = object() + existing = self.get(k, _sentinel) + if existing is not _sentinel and existing != v: + # resolve ``input`` at call time (not capture) so monkeypatching works + ask = get_input if get_input is not None else input + if ask(prompt(k, existing, v)) != str(v): + return existing # not confirmed: keep the existing value (no change) + return v + + return confirm_overwrite + + +#: A ready-to-use ``wrap_kvs`` ``preset`` that asks (via the builtin ``input``) to confirm +#: before overwriting an existing key with a different value (Issue #13). For customization +#: (e.g. a non-interactive confirmation policy), use :func:`mk_confirm_overwrite_preset`. +#: +#: >>> from dol import wrap_kvs, confirm_overwrite +#: >>> d = wrap_kvs(dict(a='apple', b='banana'), preset=confirm_overwrite) +#: >>> d['a'] = 'apple' # same value -> no prompt, no change +#: >>> d['c'] = 'coconut' # new key -> no prompt, written +#: >>> dict(d) == {'a': 'apple', 'b': 'banana', 'c': 'coconut'} +#: True +confirm_overwrite = mk_confirm_overwrite_preset() + + def add_aliases(obj, **aliases): """A function that wraps the object instance and adds aliases.