Skip to content

Commit

Permalink
feat: Implement CloneBehavior + CloneStrategy for a bone individu…
Browse files Browse the repository at this point in the history
…al clone behavior
  • Loading branch information
sveneberth committed Jan 31, 2025
1 parent 9caad83 commit 27a203e
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 6 deletions.
2 changes: 2 additions & 0 deletions src/viur/core/bones/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
ReadFromClientException,
UniqueLockMethod,
UniqueValue,
CloneBehavior,
CloneStrategy,
)
from .boolean import BooleanBone
from .captcha import CaptchaBone
Expand Down
69 changes: 68 additions & 1 deletion src/viur/core/bones/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
"""

import copy
import dataclasses
import hashlib
import inspect
import logging
import typing as t
from collections.abc import Iterable
from dataclasses import dataclass, field
from datetime import timedelta
from enum import Enum
from enum import Enum, auto
import enum

from viur.core import db, utils, current, i18n
from viur.core.config import conf
Expand Down Expand Up @@ -204,6 +206,33 @@ class Compute:
raw: bool = True # defines whether the value returned by fn is used as is, or is passed through bone.fromClient


class CloneStrategy(enum.StrEnum):
SET_NULL = enum.auto()
SET_DEFAULT = enum.auto()
SET_EMPTY = enum.auto()
COPY_VALUE = enum.auto()
CUSTOM = enum.auto()

class CloneCustomFunc(t.Protocol):

Check failure on line 216 in src/viur/core/bones/base.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E302: expected 2 blank lines, found 1
def __call__(self, skel: "SkeletonInstance", src_skel: "SkeletonInstance", bone_name:str) -> t.Any: ...

Check failure on line 217 in src/viur/core/bones/base.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E231: missing whitespace after ':'

@dataclass

Check failure on line 219 in src/viur/core/bones/base.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E302: expected 2 blank lines, found 1
class CloneBehavior:
strategy: CloneStrategy
custom_func: callable = None

def __post_init__(self):
if self.strategy == CloneStrategy.CUSTOM and self.custom_func is None:
raise ValueError("CloneStrategy is CUSTOM, but custom_func is not set")
elif self.strategy != CloneStrategy.CUSTOM and self.custom_func is not None:
raise ValueError("custom_func is set, but CloneStrategy is not CUSTOM")
# if not ((self.strategy == CloneStrategy.CUSTOM) ^ (self.custom_func is None)):
# raise ValueError("custom_func is not defined")





class BaseBone(object):
"""
The BaseBone class serves as the base class for all bone types in the ViUR framework.
Expand Down Expand Up @@ -260,6 +289,7 @@ def __init__(
unique: None | UniqueValue = None,
vfunc: callable = None, # fixme: Rename this, see below.
visible: bool = True,
clone_behavior: CloneBehavior | CloneStrategy | None = None,
):
"""
Initializes a new Bone.
Expand Down Expand Up @@ -376,6 +406,19 @@ def __init__(

self.compute = compute

if clone_behavior is None: # auto choose

Check failure on line 409 in src/viur/core/bones/base.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E261: at least two spaces before inline comment
if self.unique and self.readOnly:
self.clone_behavior = CloneBehavior(CloneStrategy.SET_DEFAULT)
else:
self.clone_behavior = CloneBehavior(CloneStrategy.COPY_VALUE)

Check failure on line 413 in src/viur/core/bones/base.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E111: indentation is not a multiple of 4
# TODO: Any different setting for computed bones?
elif isinstance(clone_behavior, CloneStrategy):
self.clone_behavior = CloneBehavior(strategy=clone_behavior)
elif isinstance(clone_behavior, CloneBehavior):
self.clone_behavior = clone_behavior
else:
raise TypeError(f"'clone_behavior' must be an instance of Clone, but {clone_behavior=} was specified")

def __set_name__(self, owner: "Skeleton", name: str) -> None:
self.skel_cls = owner
self.name = name
Expand Down Expand Up @@ -1252,6 +1295,29 @@ def postDeletedHandler(self, skel: 'viur.core.skeleton.SkeletonInstance', boneNa
"""
pass

def clone_value(self, skel: "SkeletonInstance", src_skel:"SkeletonInstance", bone_name: str) -> None:

Check failure on line 1298 in src/viur/core/bones/base.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E231: missing whitespace after ':'
logging.debug(f"{bone_name=} | {self.clone_behavior=}")
match self.clone_behavior.strategy:
case CloneStrategy.COPY_VALUE:
try:
skel.accessedValues[bone_name] = copy.deepcopy(src_skel.accessedValues[bone_name])
except KeyError:
pass # bone_name is not in accessedValues, no need to clone_behavior

Check failure on line 1305 in src/viur/core/bones/base.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E261: at least two spaces before inline comment
try:
skel.renderAccessedValues[bone_name] = copy.deepcopy(src_skel.renderAccessedValues[bone_name])
except KeyError:
pass # bone_name is not in renderAccessedValues, no need to clone_behavior

Check failure on line 1309 in src/viur/core/bones/base.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E261: at least two spaces before inline comment
case CloneStrategy.SET_NULL:
skel.accessedValues[bone_name] = None
case CloneStrategy.SET_DEFAULT:
skel.accessedValues[bone_name] = self.getDefaultValue(skel)
case CloneStrategy.SET_EMPTY:
skel.accessedValues[bone_name] = self.getEmptyValue()
case CloneStrategy.CUSTOM:
skel.accessedValues[bone_name] = self.clone_behavior.custom_func(skel, src_skel, bone_name)
case other:
raise NotImplementedError(other)

def refresh(self, skel: 'viur.core.skeleton.SkeletonInstance', boneName: str) -> None:
"""
Refresh all values we might have cached from other entities.
Expand Down Expand Up @@ -1460,6 +1526,7 @@ def structure(self) -> dict:
"languages": self.languages,
"emptyvalue": self.getEmptyValue(),
"indexed": self.indexed,
"clone_behavior": dataclasses.asdict(self.clone_behavior),
}

# Provide a defaultvalue, if it's not a function.
Expand Down
4 changes: 4 additions & 0 deletions src/viur/core/bones/sortindex.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import typing as t
import time

from viur.core.bones.base import CloneBehavior, CloneStrategy
from viur.core.bones.numeric import NumericBone


Expand All @@ -23,11 +25,13 @@ def __init__(
defaultValue: int | float = lambda *args, **kwargs: time.time(),
descr: str = "SortIndex",
precision: int = 8,
clone_behavior = CloneBehavior(CloneStrategy.SET_DEFAULT),

Check failure on line 28 in src/viur/core/bones/sortindex.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E251: unexpected spaces around keyword / parameter equals

Check failure on line 28 in src/viur/core/bones/sortindex.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E251: unexpected spaces around keyword / parameter equals
**kwargs
):
super().__init__(
defaultValue=defaultValue,
descr=descr,
precision=precision,
clone_behavior=clone_behavior,
**kwargs
)
4 changes: 2 additions & 2 deletions src/viur/core/prototypes/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ def getDefaultListParams(self):
@skey(allow_empty=True)
def clone(self, key: db.Key | str | int, **kwargs):
"""
Clone an existing entry, and render the entry, eventually with error notes on incorrect data.
CloneBehavior an existing entry, and render the entry, eventually with error notes on incorrect data.
Data is taken by any other arguments in *kwargs*.
The function performs several access control checks on the requested entity before it is added.
Expand All @@ -376,7 +376,7 @@ def clone(self, key: db.Key | str | int, **kwargs):

# Remember source skel and unset the key for clone operation!
src_skel = skel
skel = skel.clone()
skel = skel.clone(apply_clone_strategy=True)
skel["key"] = None

# Check all required preconditions for clone
Expand Down
13 changes: 10 additions & 3 deletions src/viur/core/skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,16 +399,23 @@ def __ior__(self, other: dict | SkeletonInstance | db.Entity) -> SkeletonInstanc
raise ValueError("Unsupported Type")
return self

def clone(self):
def clone(self, *, apply_clone_strategy:bool=False) -> t.Self:
"""
Clones a SkeletonInstance into a modificable, stand-alone instance.
This will also allow to modify the underlying data model.
"""
res = SkeletonInstance(self.skeletonCls, bone_map=self.boneMap, clone=True)
res.accessedValues = copy.deepcopy(self.accessedValues)
if apply_clone_strategy:
for bone_name, bone_instance in self.items():
bone_instance.clone_value(res, self, bone_name)
else:
res.accessedValues = copy.deepcopy(self.accessedValues)
res.dbEntity = copy.deepcopy(self.dbEntity)
res.is_cloned = True
res.renderAccessedValues = copy.deepcopy(self.renderAccessedValues)
if not apply_clone_strategy:
res.renderAccessedValues = copy.deepcopy(self.renderAccessedValues)
# else: Depending on the strategy the values are cloned in bone_instance.clone_value too

return res

def ensure_is_cloned(self):
Expand Down

0 comments on commit 27a203e

Please sign in to comment.