From 27a203e3e4a9ca055fa48f676f81e8ee5eaab803 Mon Sep 17 00:00:00 2001 From: Sven Eberth Date: Fri, 31 Jan 2025 18:12:56 +0100 Subject: [PATCH] feat: Implement `CloneBehavior` + `CloneStrategy` for a bone individual clone behavior --- src/viur/core/bones/__init__.py | 2 + src/viur/core/bones/base.py | 69 +++++++++++++++++++++++++++++++- src/viur/core/bones/sortindex.py | 4 ++ src/viur/core/prototypes/list.py | 4 +- src/viur/core/skeleton.py | 13 ++++-- 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/viur/core/bones/__init__.py b/src/viur/core/bones/__init__.py index 7856ae478..52ce1ce81 100644 --- a/src/viur/core/bones/__init__.py +++ b/src/viur/core/bones/__init__.py @@ -9,6 +9,8 @@ ReadFromClientException, UniqueLockMethod, UniqueValue, + CloneBehavior, + CloneStrategy, ) from .boolean import BooleanBone from .captcha import CaptchaBone diff --git a/src/viur/core/bones/base.py b/src/viur/core/bones/base.py index 71ecda57c..001acb681 100644 --- a/src/viur/core/bones/base.py +++ b/src/viur/core/bones/base.py @@ -6,6 +6,7 @@ """ import copy +import dataclasses import hashlib import inspect import logging @@ -13,7 +14,8 @@ 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 @@ -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): + def __call__(self, skel: "SkeletonInstance", src_skel: "SkeletonInstance", bone_name:str) -> t.Any: ... + +@dataclass +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. @@ -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. @@ -376,6 +406,19 @@ def __init__( self.compute = compute + if clone_behavior is None: # auto choose + if self.unique and self.readOnly: + self.clone_behavior = CloneBehavior(CloneStrategy.SET_DEFAULT) + else: + self.clone_behavior = CloneBehavior(CloneStrategy.COPY_VALUE) + # 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 @@ -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: + 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 + 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 + 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. @@ -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. diff --git a/src/viur/core/bones/sortindex.py b/src/viur/core/bones/sortindex.py index f85413d5e..efebb2737 100644 --- a/src/viur/core/bones/sortindex.py +++ b/src/viur/core/bones/sortindex.py @@ -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 @@ -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), **kwargs ): super().__init__( defaultValue=defaultValue, descr=descr, precision=precision, + clone_behavior=clone_behavior, **kwargs ) diff --git a/src/viur/core/prototypes/list.py b/src/viur/core/prototypes/list.py index e1957f239..800c2c09f 100644 --- a/src/viur/core/prototypes/list.py +++ b/src/viur/core/prototypes/list.py @@ -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. @@ -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 diff --git a/src/viur/core/skeleton.py b/src/viur/core/skeleton.py index 9e33f6cdc..31d5fa1af 100644 --- a/src/viur/core/skeleton.py +++ b/src/viur/core/skeleton.py @@ -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):