Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement CloneBehavior + CloneStrategy for a bone individual clone behavior #1401

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion src/viur/core/bones/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .base import (
BaseBone,
CloneBehavior,
CloneStrategy,
Compute,
ComputeInterval,
ComputeMethod,
Expand Down Expand Up @@ -48,6 +50,8 @@
"BaseBone",
"BooleanBone",
"CaptchaBone",
"CloneBehavior",
"CloneStrategy",
"ColorBone",
"Compute",
"ComputeInterval",
Expand Down Expand Up @@ -82,8 +86,8 @@
"UidBone",
"UniqueLockMethod",
"UniqueValue",
"UserBone",
"UriBone",
"UserBone",
"translation_key_prefix_bonename",
"translation_key_prefix_skeleton_bonename",
]
Expand Down
94 changes: 93 additions & 1 deletion src/viur/core/bones/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"""

import copy
import dataclasses
import enum
import hashlib
import inspect
import logging
Expand All @@ -15,7 +17,7 @@
from datetime import timedelta
from enum import Enum

from viur.core import db, utils, current, i18n
from viur.core import current, db, i18n, utils
from viur.core.config import conf

if t.TYPE_CHECKING:
Expand Down Expand Up @@ -204,6 +206,55 @@ 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):
"""Strategy for selecting the value of a cloned skeleton"""

SET_NULL = enum.auto()
"""Sets the cloned bone value to None."""

SET_DEFAULT = enum.auto()
"""Sets the cloned bone value to its defaultValue."""

SET_EMPTY = enum.auto()
"""Sets the cloned bone value to its emptyValue."""

COPY_VALUE = enum.auto()
"""Copies the bone value from the source skeleton."""

CUSTOM = enum.auto()
"""Uses a custom-defined logic for setting the cloned value.
Requires :attr:`CloneBehavior.custom_func` to be set.
"""


class CloneCustomFunc(t.Protocol):
"""Type for a custom clone function assigned to :attr:`CloneBehavior.custom_func`"""

def __call__(self, skel: "SkeletonInstance", src_skel: "SkeletonInstance", bone_name: str) -> t.Any:
"""Return the value for the cloned bone"""
...


@dataclass
class CloneBehavior:
"""Strategy configuration for selecting the value of a cloned skeleton"""

strategy: CloneStrategy
"""The strategy used to select a value from a cloned skeleton"""

custom_func: CloneCustomFunc = None
"""custom-defined logic for setting the cloned value
Only required when :attr:`strategy` is set to :attr:`CloneStrategy.CUSTOM`.
"""

def __post_init__(self):
"""Validate this configuration."""
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")


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 +311,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 +428,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
Expand Down Expand Up @@ -1252,6 +1317,30 @@ def postDeletedHandler(self, skel: 'viur.core.skeleton.SkeletonInstance', boneNa
"""
pass

def clone_value(self, skel: "SkeletonInstance", src_skel: "SkeletonInstance", bone_name: str) -> None:
"""Clone / Set the value for this bone depending on :attr:`clone_behavior`"""
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, cannot clone it
try:
skel.renderAccessedValues[bone_name] = copy.deepcopy(src_skel.renderAccessedValues[bone_name])
except KeyError:
pass # bone_name is not in renderAccessedValues, cannot clone it
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 +1549,9 @@ def structure(self) -> dict:
"languages": self.languages,
"emptyvalue": self.getEmptyValue(),
"indexed": self.indexed,
"clone_behavior": {
"strategy": self.clone_behavior.strategy,
},
}

# 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),
**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
2 changes: 1 addition & 1 deletion src/viur/core/prototypes/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ def clone(self, skelType: SkelType, 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

# make parententry required and writeable when provided
Expand Down
17 changes: 13 additions & 4 deletions src/viur/core/skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import time
import typing as t
import warnings
from deprecated.sphinx import deprecated
from functools import partial
from itertools import chain

from deprecated.sphinx import deprecated

from viur.core import conf, current, db, email, errors, translate, utils
from viur.core.bones import (
BaseBone,
Expand Down Expand Up @@ -399,16 +401,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