Skip to content

Commit

Permalink
add TransparentCage
Browse files Browse the repository at this point in the history
Changes:

- add TransparentCage
- fix bug in with_overwrite
  • Loading branch information
devkral committed Jan 6, 2025
1 parent 2c49e4c commit 56f1563
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 5 deletions.
13 changes: 13 additions & 0 deletions docs/cages.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ By default when no contextvariable was initialized the original is copied via `c
By providing `deep_copy=True` `copy.deepcopy()` is used.


## TransparentCage

This is a subclass of Cage also exposing a ContextVar like interface. You can use them as container as well as a
ContextVar.

A simpler variant is just prefixing the ContextVar public methods and attributes:

`name`, `set`, `reset` and `get` become

`monkay_name`, `monkay_set`, `monkay_reset` and `monkay_get`.

What TransparentCage is doing is just redirecting.

## Advanced

### New context variable name
Expand Down
3 changes: 3 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
### Added

- Add `evaluate_settings_once`.
- Add TransparentCage, which also exposes the ContextVar interface.
- Add `monkay_` prefixed ContextVar-like attributes and methods.

### Changed

Expand All @@ -15,6 +17,7 @@
### Fixed

- Assigning an empty dictionary to settings deletes the settings. This should only work for some falsy values.
- Cage `with_overwrite` didn't escape the last update compontent properly.

## Version 0.1.1

Expand Down
3 changes: 2 additions & 1 deletion monkay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
load,
load_any,
)
from .cages import Cage
from .cages import Cage, TransparentCage
from .core import (
Monkay,
)
Expand All @@ -31,4 +31,5 @@
"InGlobalsDict",
"get_value_from_settings",
"Cage",
"TransparentCage",
]
44 changes: 41 additions & 3 deletions monkay/cages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import copy
from collections.abc import Callable, Generator, Iterable
from contextlib import AbstractContextManager, contextmanager, nullcontext
from contextvars import ContextVar
from contextvars import ContextVar, Token
from functools import wraps
from importlib import import_module
from inspect import ismethoddescriptor
Expand All @@ -15,15 +15,24 @@ class Undefined: ...


T = TypeVar("T")
DEFAULT = TypeVar("DEFAULT")

forbidden_names = {"__getattribute__", "__setattr__", "__delattr__", "__new__", "__init__"}
forbidden_names: set[str] = {
"__getattribute__",
"__setattr__",
"__delattr__",
"__new__",
"__init__",
}
context_var_attributes: set[str] = {"name", "get", "set", "reset"}


class Cage(Generic[T]):
monkay_context_var: ContextVar[tuple[int, T] | type[Undefined]]
monkay_deep_copy: bool
monkay_use_wrapper_for_reads: bool
monkay_update_fn: Callable[[T, T], T] | None
monkay_name: str
monkay_original: T
monkay_original_last_update: int
monkay_original_last_update_lock: None | Lock
Expand Down Expand Up @@ -74,6 +83,7 @@ def __new__(
attrs["__new__"] = object.__new__
monkay_cage_cls = type(cls.__name__, (cls,), attrs)
monkay_cage_instance = monkay_cage_cls()
monkay_cage_instance.monkay_name = name
monkay_cage_instance.monkay_context_var = globals_dict[context_var_name] = ContextVar(
context_var_name, default=Undefined
)
Expand Down Expand Up @@ -178,7 +188,7 @@ def monkay_proxied(
@contextmanager
def monkay_with_override(self, value: T) -> Generator[T]:
monkay_dict = super().__getattribute__("__dict__")
token = monkay_dict["monkay_context_var"].set(value)
token = monkay_dict["monkay_context_var"].set((None, value))
try:
yield value
finally:
Expand All @@ -195,3 +205,31 @@ def monkay_with_original(
if update_after and monkay_dict["monkay_original_last_update_lock"] is not None:
with monkay_dict["monkay_original_last_update_lock"]:
monkay_dict["monkay_original_last_update"] += 1

def monkay_set(self, value: T) -> Token:
monkay_dict = super().__getattribute__("__dict__")
return monkay_dict["monkay_context_var"].set(
(monkay_dict["monkay_original_last_update"], value)
)

def monkay_get(self, default: T | DEFAULT = None) -> T | DEFAULT:
monkay_dict = super().__getattribute__("__dict__")
tup = monkay_dict["monkay_context_var"].get()
if tup is Undefined:
original: T | Undefined = monkay_dict["monkay_original"]
if original is Undefined:
return default
else:
return original
else:
return tup[1]

def monkay_reset(self, token: Token):
self.monkay_context_var.reset(token)


class TransparentCage(Cage):
def __getattribute__(self, name: str) -> Any:
if name in context_var_attributes:
name = f"monkay_{name}"
return super().__getattribute__(name)
16 changes: 15 additions & 1 deletion tests/test_cages.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest

from monkay import Cage
from monkay import Cage, TransparentCage

target_ro = {"foo": "bar"}

Expand All @@ -16,6 +16,10 @@
target2_caged: set = {500000, 600000}
Cage(globals(), {1, 2, 4}, name="target2_caged", update_fn=lambda private, new: private.union(new))

transparent_cage = TransparentCage(
globals(), {1}, name="transparent_cage", skip_self_register=True
)


@pytest.fixture(autouse=True, scope="function")
def cleanup():
Expand Down Expand Up @@ -71,6 +75,16 @@ def test_cages_retrieve_with_name():
assert isinstance(globals()["foo_cages_retrieve_with_name_ctx"], ContextVar)


def test_transparent_cage():
assert transparent_cage.name == "transparent_cage"
assert transparent_cage.get() == {1}
assert transparent_cage == {1}
token = transparent_cage.set({2})
assert transparent_cage == {2}
transparent_cage.reset(token)
assert transparent_cage == {1}


@pytest.mark.parametrize("read_lock", [True, False])
def test_cages_wrapper_for_non_existing(read_lock):
lock = Lock()
Expand Down

0 comments on commit 56f1563

Please sign in to comment.