From 12d0069d3a3c85051e86e9f1a91d1a7a75563d1e Mon Sep 17 00:00:00 2001 From: Sven Eberth Date: Thu, 13 Feb 2025 01:46:49 +0100 Subject: [PATCH 1/3] feat: Implement TypeVar for Skeleton and make SkeletonInstance Generic This adds the possibility to shows to which `Skeleton` class a `SkeletonInstance` belongs. I've used this in the viur-shop a lot, otherwise it gets very complicated to know which type of Skeleton in meaned e.g. here: https://github.com/viur-framework/viur-shop/blob/caa2476c41e9b886f7961cdaa84a441dc3eb2c76/src/viur/shop/types/dc_scope.py#L25-L29 --- src/viur/core/modules/user.py | 2 +- src/viur/core/skeleton.py | 31 +++++++++++++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/viur/core/modules/user.py b/src/viur/core/modules/user.py index 1830f2bd4..5e2bc62a9 100644 --- a/src/viur/core/modules/user.py +++ b/src/viur/core/modules/user.py @@ -194,7 +194,7 @@ def __init__(self, moduleName, modulePath, userModule): super().__init__(moduleName, modulePath) self._user_module = userModule - def can_handle(self, skel: skeleton.SkeletonInstance) -> bool: + def can_handle(self, skel: skeleton.SkeletonInstance[UserSkel]) -> bool: return True @classmethod diff --git a/src/viur/core/skeleton.py b/src/viur/core/skeleton.py index e477e0000..6ed32ca31 100644 --- a/src/viur/core/skeleton.py +++ b/src/viur/core/skeleton.py @@ -10,9 +10,10 @@ 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, @@ -39,6 +40,8 @@ ABSTRACT_SKEL_CLS_SUFFIX = "AbstractSkel" KeyType: t.TypeAlias = db.Key | str | int +Skeleton_Cls = t.TypeVar("Skeleton_Cls", bound=t.Type["BaseSkeleton"], covariant=True) + class MetaBaseSkel(type): """ @@ -126,7 +129,7 @@ def __setattr__(self, key, value): value.__set_name__(self, key) -class SkeletonInstance: +class SkeletonInstance(t.Generic[Skeleton_Cls]): """ The actual wrapper around a Skeleton-Class. An object of this class is what's actually returned when you call a Skeleton-Class. With ViUR3, you don't get an instance of a Skeleton-Class any more - it's always this @@ -145,7 +148,7 @@ class SkeletonInstance: def __init__( self, - skel_cls: t.Type[Skeleton], + skel_cls: Skeleton_Cls, *, bones: t.Iterable[str] = (), bone_map: t.Optional[t.Dict[str, BaseBone]] = None, @@ -177,7 +180,7 @@ def __init__( bone_map = bone_map or {} if bones: - names = ("key", ) + tuple(bones) + names = ("key",) + tuple(bones) # generate full keys sequence based on definition; keeps order of patterns! keys = [] @@ -210,7 +213,7 @@ def __init__( self.is_cloned = full_clone self.renderAccessedValues = {} self.renderPreparation = None - self.skeletonCls = skel_cls + self.skeletonCls: Skeleton_Cls = skel_cls def items(self, yieldBoneValues: bool = False) -> t.Iterable[tuple[str, BaseBone]]: if yieldBoneValues: @@ -431,7 +434,7 @@ def __deepcopy__(self, memodict): return res -class BaseSkeleton(object, metaclass=MetaBaseSkel): +class BaseSkeleton(metaclass=MetaBaseSkel): """ This is a container-object holding information about one database entity. @@ -593,7 +596,7 @@ def setBoneValue( return bone.setBoneValue(skel, boneName, value, append, language) @classmethod - def fromClient(cls, skel: SkeletonInstance, data: dict[str, list[str] | str], amend: bool = False) -> bool: + def fromClient(cls, skel: SkeletonInstance[t.Self], data: dict[str, list[str] | str], amend: bool = False) -> bool: """ Load supplied *data* into Skeleton. @@ -667,7 +670,7 @@ def fromClient(cls, skel: SkeletonInstance, data: dict[str, list[str] | str], am return complete @classmethod - def refresh(cls, skel: SkeletonInstance): + def refresh(cls, skel: SkeletonInstance[t.Self]): """ Refresh the bones current content. @@ -683,7 +686,7 @@ def refresh(cls, skel: SkeletonInstance): _ = skel[key] # Ensure value gets loaded bone.refresh(skel, key) - def __new__(cls, *args, **kwargs) -> SkeletonInstance: + def __new__(cls, *args, **kwargs) -> SkeletonInstance[t.Self]: return SkeletonInstance(cls, *args, **kwargs) @@ -1334,7 +1337,7 @@ def __txn_write(write_skel): skel.dbEntity["viur"].setdefault("viurActiveSeoKeys", []) for language, seo_key in last_set_seo_keys.items(): if skel.dbEntity["viur"]["viurCurrentSeoKeys"][language] not in \ - skel.dbEntity["viur"]["viurActiveSeoKeys"]: + skel.dbEntity["viur"]["viurActiveSeoKeys"]: # Ensure the current, active seo key is in the list of all seo keys skel.dbEntity["viur"]["viurActiveSeoKeys"].insert(0, seo_key) if str(skel.dbEntity.key.id_or_name) not in skel.dbEntity["viur"]["viurActiveSeoKeys"]: @@ -1755,7 +1758,7 @@ def read(self, key: t.Optional[db.Key | str | int] = None) -> SkeletonInstance: return skel -class SkelList(list): +class SkelList(list, t.Generic[Skeleton_Cls]): """ This class is used to hold multiple skeletons together with other, commonly used information. @@ -1774,12 +1777,12 @@ class SkelList(list): "renderPreparation", ) - def __init__(self, baseSkel=None): + def __init__(self, baseSkel: Skeleton_Cls = None): """ :param baseSkel: The baseclass for all entries in this list """ - super(SkelList, self).__init__() - self.baseSkel = baseSkel or {} + super().__init__() + self.baseSkel: Skeleton_Cls = baseSkel or {} self.getCursor = lambda: None self.get_orders = lambda: None self.renderPreparation = None From 0e20b4d8ff23974e9d8f1ab2ee2a72234cc9bf4f Mon Sep 17 00:00:00 2001 From: Sven Eberth Date: Thu, 13 Feb 2025 02:19:24 +0100 Subject: [PATCH 2/3] change it again --- src/viur/core/modules/user.py | 3 ++- src/viur/core/skeleton.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/viur/core/modules/user.py b/src/viur/core/modules/user.py index 7580bc2e0..56881a81b 100644 --- a/src/viur/core/modules/user.py +++ b/src/viur/core/modules/user.py @@ -27,6 +27,7 @@ from viur.core.prototypes.list import List from viur.core.ratelimit import RateLimit from viur.core.securityheaders import extendCsp +from viur.core.skeleton import SkeletonInstance @functools.total_ordering @@ -1309,7 +1310,7 @@ def editSkel(self, *args, **kwargs): def secondFactorProviderByClass(self, cls) -> UserSecondFactorAuthentication: return getattr(self, f"f2_{cls.__name__.lower()}") - def getCurrentUser(self): + def getCurrentUser(self) -> SkeletonInstance[UserSkel] | None: session = current.session.get() req = current.request.get() diff --git a/src/viur/core/skeleton.py b/src/viur/core/skeleton.py index 6eff4a3e4..d1999bff3 100644 --- a/src/viur/core/skeleton.py +++ b/src/viur/core/skeleton.py @@ -40,7 +40,7 @@ ABSTRACT_SKEL_CLS_SUFFIX = "AbstractSkel" KeyType: t.TypeAlias = db.Key | str | int -Skeleton_Cls = t.TypeVar("Skeleton_Cls", bound=t.Type["BaseSkeleton"], covariant=True) +Skeleton_Cls = t.TypeVar("Skeleton_Cls", bound="BaseSkeleton") class MetaBaseSkel(type): @@ -149,7 +149,7 @@ class SkeletonInstance(t.Generic[Skeleton_Cls]): def __init__( self, - skel_cls: Skeleton_Cls, + skel_cls: t.Type[Skeleton_Cls], *, bones: t.Iterable[str] = (), bone_map: t.Optional[t.Dict[str, BaseBone]] = None, @@ -219,7 +219,7 @@ def __init__( self.is_cloned = clone self.renderAccessedValues = {} self.renderPreparation = None - self.skeletonCls: Skeleton_Cls = skel_cls + self.skeletonCls: t.Type[Skeleton_Cls] = skel_cls def items(self, yieldBoneValues: bool = False) -> t.Iterable[tuple[str, BaseBone]]: if yieldBoneValues: @@ -1823,12 +1823,12 @@ class SkelList(list, t.Generic[Skeleton_Cls]): "renderPreparation", ) - def __init__(self, baseSkel: Skeleton_Cls = None): + def __init__(self, baseSkel: SkeletonInstance[Skeleton_Cls] = None): """ :param baseSkel: The baseclass for all entries in this list """ super().__init__() - self.baseSkel: Skeleton_Cls = baseSkel or {} + self.baseSkel: SkeletonInstance[Skeleton_Cls] | dict = baseSkel or {} self.getCursor = lambda: None self.get_orders = lambda: None self.renderPreparation = None From f656c0a3f21dd2d0b817e23f65ddd0e4a9b76df2 Mon Sep 17 00:00:00 2001 From: Sven Eberth Date: Thu, 13 Feb 2025 02:20:32 +0100 Subject: [PATCH 3/3] syntax --- src/viur/core/skeleton.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/viur/core/skeleton.py b/src/viur/core/skeleton.py index d1999bff3..1722f4ba6 100644 --- a/src/viur/core/skeleton.py +++ b/src/viur/core/skeleton.py @@ -1379,8 +1379,10 @@ def __txn_write(write_skel): skel.dbEntity["viur"].setdefault("viurActiveSeoKeys", []) for language, seo_key in last_set_seo_keys.items(): - if skel.dbEntity["viur"]["viurCurrentSeoKeys"][language] not in \ - skel.dbEntity["viur"]["viurActiveSeoKeys"]: + if ( + skel.dbEntity["viur"]["viurCurrentSeoKeys"][language] + not in skel.dbEntity["viur"]["viurActiveSeoKeys"] + ): # Ensure the current, active seo key is in the list of all seo keys skel.dbEntity["viur"]["viurActiveSeoKeys"].insert(0, seo_key) if str(skel.dbEntity.key.id_or_name) not in skel.dbEntity["viur"]["viurActiveSeoKeys"]: