Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions .github/workflows/windows_ci.yml

This file was deleted.

2 changes: 2 additions & 0 deletions dol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 4 additions & 17 deletions dol/kv_codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
45 changes: 45 additions & 0 deletions dol/tests/test_confirm_overwrite.py
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions dol/trans.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading