Skip to content

Commit

Permalink
feat+refactor: Improved and extended Skeleton.subskel() (viur-frame…
Browse files Browse the repository at this point in the history
…work#1259)

This PR introduces a new way to define sub-skeletons, additionally to
the old one.

1. Renames `Skeleton.subSkel()` into `Skeleton.subskel()` with a
deprecation warning
2. Giving names as parameter merges the bones of all
`Skeleton.subSkels`-configurations together. This is the usual behavior.
3. Given the `bones`-parameter allows to freely specify a sub-skeleton;
One specialty here is, that the order of the bones can also be changed
in this mode!
4. 2 + 3 can be combined, but then the original order of the bones is
kept
5. Mostly backward-compatible to the original `Skeleton.subSkel()`
except that `fnmatch` is used to resolve wildcards, which is more
powerful (the old implementation only accepted "*" at the end of the
name).

Example Skeleton:
```py
from viur.core import skeleton, utils
from viur.core.bones import *


class TodoSkel(skeleton.Skeleton):
    subSkels = {
        "*": ["*name"],
        "add": ["subject"],
    }

    firstname = StringBone(
        descr="Vorname",
    )

    lastname = StringBone(
        descr="Nachname",
        required=True,
    )

    category = SelectBone(
        descr="Kategorie",
        defaultValue="question",
        required=True,
        values={
            "question": "Anfrage",
            "billing": "Abrechnung",
            "service": "Service",
        },
    )

    subject = StringBone(
        descr="Thema",
        required=True,
    )

    message = TextBone(
        descr="Nachricht",
        required=True,
        validHtml=None,
    )


    status = SelectBone(
        descr="Status",
        required=True,
        defaultValue="open",
        values={
            "open": "Offen",
            "pending": "In Bearbeitung",
            "closed": "Geschlossen",
        },
    )

    user = UserBone(
         descr="Zugewiesen an",
    )
```

Example usage:
```py
# legacy mode (see 2)
subskel = TodoSkel.subskel("add")
# creates subskel: key, firstname, lastname, subject

# free mode (see 3):
subskel = TodoSkel.subskel(bones=("subject", "message", "*stname"))
# creates subskel: key, subject, message, firstname, lastname

# mixed mode (see 4)
subskel = TodoSkel.subskel("add", bones=("message", ))
# creates subskel: key, firstname, lastname, subject, message
```

---------

Co-authored-by: Sven Eberth <[email protected]>
  • Loading branch information
phorward and sveneberth authored Oct 9, 2024
1 parent 8e04095 commit be93a65
Showing 1 changed file with 115 additions and 34 deletions.
149 changes: 115 additions & 34 deletions src/viur/core/skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,38 +141,58 @@ class SkeletonInstance:
"skeletonCls",
}

def __init__(self, skelCls, subSkelNames=None, fullClone=False, clonedBoneMap=None):
if clonedBoneMap:
self.boneMap = clonedBoneMap
for k, v in self.boneMap.items():
v.isClonedInstance = True
def __init__(
self,
skel_cls: t.Type[Skeleton],
*,
bones: t.Iterable[str] = (),
full_clone: bool = False,
):
"""
Creates a new SkeletonInstance based on `skel_cls`.
elif subSkelNames:
boneList = ["key"] + list(chain(*[skelCls.subSkels.get(x, []) for x in ["*"] + subSkelNames]))
doesMatch = lambda name: name in boneList or any(
[name.startswith(x[:-1]) for x in boneList if x[-1] == "*"])
if fullClone:
self.boneMap = {k: copy.deepcopy(v) for k, v in skelCls.__boneMap__.items() if doesMatch(k)}
for v in self.boneMap.values():
v.isClonedInstance = True
:param skel_cls: Is the base skeleton class to inherit from and reference to.
:param bones: If given, defines an iterable of bones that are take into the SkeletonInstance.
The order of the bones defines the order in the SkeletonInstance.
:param full_clone: If set True, performs a full clone of the used bone map, to be entirely stand-alone.
"""
bone_map = None

if bones:
names = ("key", ) + tuple(bones)

# generate full keys sequence based on definition; keeps order of patterns!
keys = []
for name in names:
if name in skel_cls.__boneMap__:
keys.append(name)
else:
keys.extend(fnmatch.filter(skel_cls.__boneMap__.keys(), name))

if full_clone:
bone_map = {k: copy.deepcopy(skel_cls.__boneMap__[k]) for k in keys if skel_cls.__boneMap__[k]}
else:
self.boneMap = {k: v for k, v in skelCls.__boneMap__.items() if doesMatch(k)}
bone_map = {k: skel_cls.__boneMap__[k] for k in keys if skel_cls.__boneMap__[k]}

elif full_clone:
bone_map = copy.deepcopy(skel_cls.__boneMap__)

elif fullClone:
self.boneMap = copy.deepcopy(skelCls.__boneMap__)
# generated or provided bone_map
if bone_map:
self.boneMap = bone_map
for v in self.boneMap.values():
v.isClonedInstance = True

else: # No Subskel, no Clone
self.boneMap = skelCls.__boneMap__.copy()
self.boneMap = skel_cls.__boneMap__.copy()

self.accessedValues = {}
self.dbEntity = None
self.errors = []
self.is_cloned = fullClone
self.is_cloned = full_clone
self.renderAccessedValues = {}
self.renderPreparation = None
self.skeletonCls = skelCls
self.skeletonCls = skel_cls

def items(self, yieldBoneValues: bool = False) -> t.Iterable[tuple[str, BaseBone]]:
if yieldBoneValues:
Expand Down Expand Up @@ -358,7 +378,7 @@ def clone(self):
Clones a SkeletonInstance into a modificable, stand-alone instance.
This will also allow to modify the underlying data model.
"""
res = SkeletonInstance(self.skeletonCls, clonedBoneMap=copy.deepcopy(self.boneMap))
res = SkeletonInstance(self.skeletonCls, full_clone=True)
res.accessedValues = copy.deepcopy(self.accessedValues)
res.dbEntity = copy.deepcopy(self.dbEntity)
res.is_cloned = True
Expand Down Expand Up @@ -416,27 +436,88 @@ class BaseSkeleton(object, metaclass=MetaBaseSkel):
boneMap = None

@classmethod
def subSkel(cls, *name, fullClone: bool = False, **kwargs) -> SkeletonInstance:
@deprecated(
version="3.7.0",
reason="Function renamed. Use subskel function as alternative implementation.",
action="always"
)
def subSkel(cls, *subskel_names, full_clone: bool = False, **kwargs) -> SkeletonInstance:
return cls.subskel(*subskel_names, full_clone=full_clone) # FIXME: REMOVE WITH VIUR4

@classmethod
def subskel(
cls,
*names: str,
bones: t.Iterable[str] = (),
full_clone: bool = False,
) -> SkeletonInstance:
"""
Creates a new sub-skeleton as part of the current skeleton.
Creates a new sub-skeleton from the current skeleton.
A sub-skeleton is a copy of the original skeleton, containing only a subset of its bones.
To define sub-skeletons, use the subSkels property of the Skeleton object.
By passing multiple sub-skeleton names to this function, a sub-skeleton with the union of
all bones of the specified sub-skeletons is returned.
If an entry called "*" exists in the subSkels-dictionary, the bones listed in this entry
will always be part of the generated sub-skeleton.
:param name: Name of the sub-skeleton (that's the key of the subSkels dictionary); \
Multiple names can be specified.
Sub-skeletons can either be defined using the the subSkels property of the Skeleton object,
or freely by giving patterns for bone names which shall be part of the sub-skeleton.
1. Giving names as parameter merges the bones of all Skeleton.subSkels-configurations together.
This is the usual behavior. By passing multiple sub-skeleton names to this function, a sub-skeleton
with the union of all bones of the specified sub-skeletons is returned. If an entry called "*"
exists in the subSkels-dictionary, the bones listed in this entry will always be part of the
generated sub-skeleton.
2. Given the *bones* parameter allows to freely specify a sub-skeleton; One specialty here is,
that the order of the bones can also be changed in this mode. This mode is the new way of defining
sub-skeletons, and might become the primary way to define sub-skeletons in future.
3. Both modes (1 + 2) can be combined, but then the original order of the bones is kept.
4. The "key" bone is automatically available in each sub-skeleton.
5. An fnmatch-compatible wildcard pattern is allowed both in the subSkels-bone-list and the
free bone list.
Example (TodoSkel is the example skeleton from viur-base):
```py
# legacy mode (see 1)
subskel = TodoSkel.subskel("add")
# creates subskel: key, firstname, lastname, subject
# free mode (see 2) allows to specify a different order!
subskel = TodoSkel.subskel(bones=("subject", "message", "*stname"))
# creates subskel: key, subject, message, firstname, lastname
# mixed mode (see 3)
subskel = TodoSkel.subskel("add", bones=("message", ))
# creates subskel: key, firstname, lastname, subject, message
```
:param bones: Allows to specify an iterator of bone names (more precisely, fnmatch-wildards) which allow
to freely define a subskel. If *only* this parameter is given, the order of the specification also
defines, the order of the list. Otherwise, the original order as defined in the skeleton is kept.
:param full_clone: If set True, performs a full clone of the used bone map, to be entirely stand-alone.
:return: The sub-skeleton of the specified type.
"""
if not name:
raise ValueError("Which subSkel?")
return cls(subSkelNames=list(name), fullClone=fullClone)
from_subskel = False
bones = list(bones)

for name in names:
# a str refers to a subskel name from the cls.subSkel dict
if isinstance(name, str):
# add bones from "*" subskel once
if not from_subskel:
bones.extend(cls.subSkels.get("*") or ())
from_subskel = True

bones.extend(cls.subSkels.get(name) or ())

else:
raise ValueError(f"Invalid subskel definition: {name!r}")

if from_subskel:
# when from_subskel is True, create bone names based on the order of the bones in the original skeleton
bones = tuple(k for k in cls.__boneMap__.keys() if any(fnmatch.fnmatch(k, n) for n in bones))

if not bones:
raise ValueError("The given subskel definition doesn't contain any bones!")

return cls(bones=bones, full_clone=full_clone)

@classmethod
def setSystemInitialized(cls):
Expand Down

0 comments on commit be93a65

Please sign in to comment.