From c9813bf5027d1541cfe8a10b9149645d2480555e Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Sat, 8 Jun 2024 19:29:01 -0700 Subject: [PATCH] Revamp BaseModel for parity with other domain elements (#434) Also, remove `ModelMeta` and use the general `Options` class for meta options. --- .../adapters/repository/elasticsearch.py | 21 ++++++-- src/protean/adapters/repository/memory.py | 9 ++-- src/protean/adapters/repository/sqlalchemy.py | 9 ++-- src/protean/container.py | 11 ++-- src/protean/core/model.py | 50 +++++-------------- 5 files changed, 41 insertions(+), 59 deletions(-) diff --git a/src/protean/adapters/repository/elasticsearch.py b/src/protean/adapters/repository/elasticsearch.py index 54d0fff8..6ea1795c 100644 --- a/src/protean/adapters/repository/elasticsearch.py +++ b/src/protean/adapters/repository/elasticsearch.py @@ -12,6 +12,7 @@ from elasticsearch.exceptions import NotFoundError from elasticsearch_dsl import Document, Index, Keyword, Mapping, Search, query +from protean.container import Options from protean.exceptions import ObjectNotFoundError from protean.fields import Reference from protean.globals import current_domain @@ -399,10 +400,24 @@ def decorate_model_class(self, entity_cls, model_cls): # Construct Inner Index class with options options = {} - options["name"] = model_cls.meta_.schema_name or schema_name + + # Set schema name intelligently + # model_cls.meta_.schema_name - would come from custom model's options + # model_cls._index._name - would come from custom model's `Index` inner class + # schema_name - is derived + index_name = ( + model_cls._index._name if hasattr(model_cls, "_index") else None + ) + options["name"] = model_cls.meta_.schema_name or index_name or schema_name + + # Gather adapter settings if "SETTINGS" in self.conn_info and self.conn_info["SETTINGS"]: options["settings"] = self.conn_info["SETTINGS"] + + # Set options into `Index` inner class for ElasticsearchModel index_cls = type("Index", (object,), options) + + # Add the Index class to the custom attributes custom_attrs.update({"Index": index_cls}) # FIXME Ensure the custom model attributes are constructed properly @@ -423,9 +438,7 @@ def construct_model_class(self, entity_cls): if entity_cls.meta_.schema_name in self._model_classes: model_cls = self._model_classes[entity_cls.meta_.schema_name] else: - from protean.core.model import ModelMeta - - meta_ = ModelMeta() + meta_ = Options() meta_.entity_cls = entity_cls # Construct Inner Index class with options diff --git a/src/protean/adapters/repository/memory.py b/src/protean/adapters/repository/memory.py index f40b9b7c..fe97217a 100644 --- a/src/protean/adapters/repository/memory.py +++ b/src/protean/adapters/repository/memory.py @@ -11,6 +11,7 @@ from typing import Any from uuid import UUID +from protean.container import Options from protean.core.model import BaseModel from protean.exceptions import ObjectNotFoundError, ValidationError from protean.fields.basic import Auto @@ -148,9 +149,7 @@ def decorate_model_class(self, entity_cls, model_cls): if key not in ["Meta", "__module__", "__doc__", "__weakref__"] } - from protean.core.model import ModelMeta - - meta_ = ModelMeta() + meta_ = Options() meta_.entity_cls = entity_cls custom_attrs.update({"meta_": meta_}) @@ -172,9 +171,7 @@ def construct_model_class(self, entity_cls): if entity_cls.meta_.schema_name in self._model_classes: model_cls = self._model_classes[entity_cls.meta_.schema_name] else: - from protean.core.model import ModelMeta - - meta_ = ModelMeta() + meta_ = Options() meta_.entity_cls = entity_cls attrs = { diff --git a/src/protean/adapters/repository/sqlalchemy.py b/src/protean/adapters/repository/sqlalchemy.py index 0a8b56a5..ae44b020 100644 --- a/src/protean/adapters/repository/sqlalchemy.py +++ b/src/protean/adapters/repository/sqlalchemy.py @@ -16,6 +16,7 @@ from sqlalchemy.exc import DatabaseError from sqlalchemy.types import CHAR, TypeDecorator +from protean.container import Options from protean.core.value_object import BaseValueObject from protean.core.model import BaseModel from protean.exceptions import ( @@ -695,9 +696,7 @@ def decorate_model_class(self, entity_cls, model_cls): # Add the earlier copied columns to the custom attributes custom_attrs = {**custom_attrs, **columns} - from protean.core.model import ModelMeta - - meta_ = ModelMeta(model_cls.meta_) + meta_ = Options(model_cls.meta_) meta_.entity_cls = entity_cls meta_.schema_name = ( schema_name if meta_.schema_name is None else meta_.schema_name @@ -724,10 +723,8 @@ def construct_model_class(self, entity_cls): if entity_cls.meta_.schema_name in self._model_classes: model_cls = self._model_classes[entity_cls.meta_.schema_name] else: - from protean.core.model import ModelMeta - # Construct a new Meta object with existing values - meta_ = ModelMeta() + meta_ = Options() meta_.entity_cls = entity_cls # If schema_name is not provided, sqlalchemy can throw # sqlalchemy.exc.InvalidRequestError: Class does not diff --git a/src/protean/container.py b/src/protean/container.py index f6242c57..051250d2 100644 --- a/src/protean/container.py +++ b/src/protean/container.py @@ -35,12 +35,16 @@ def __init__(self, opts: Union[dict, Type] = None) -> None: self._opts = set() if opts: - if inspect.isclass(opts): + # FIXME Remove support passing a class as opts after revamping BaseSerializer + # The `inspect.isclass` check will not be necessary + if isinstance(opts, (self.__class__)) or inspect.isclass(opts): attributes = inspect.getmembers( opts, lambda a: not (inspect.isroutine(a)) ) for attr in attributes: - if not (attr[0].startswith("__") and attr[0].endswith("__")): + if not ( + attr[0].startswith("__") and attr[0].endswith("__") + ) and attr[0] not in ["_opts"]: setattr(self, attr[0], attr[1]) self.abstract = getattr(opts, "abstract", None) or False @@ -109,9 +113,6 @@ def _set_defaults(cls): if not (hasattr(cls.meta_, key) and getattr(cls.meta_, key) is not None): setattr(cls.meta_, key, default) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - class ContainerMeta(type): """ diff --git a/src/protean/core/model.py b/src/protean/core/model.py index 2c3c8200..49c61794 100644 --- a/src/protean/core/model.py +++ b/src/protean/core/model.py @@ -1,29 +1,11 @@ from abc import abstractmethod -from protean.container import Element +from protean.container import Element, OptionsMixin from protean.exceptions import IncorrectUsageError, NotSupportedError -from protean.utils import DomainObjects +from protean.utils import DomainObjects, derive_element_class -class ModelMeta: - """Metadata info for the Model. - - Options: - - ``entity_cls``: The Entity that this model is associated with - """ - - def __init__(self, meta=None): - if meta: - self.entity_cls = getattr(meta, "entity_cls", None) - self.schema_name = getattr(meta, "schema_name", None) - self.database = getattr(meta, "database", None) - else: - self.entity_cls = None - self.schema_name = None - self.database = None - - -class BaseModel(Element): +class BaseModel(Element, OptionsMixin): """This is a Model representing a data schema in the persistence store. A concrete implementation of this model has to be provided by each persistence store plugin. """ @@ -35,6 +17,14 @@ def __new__(cls, *args, **kwargs): raise NotSupportedError("BaseModel cannot be instantiated") return super().__new__(cls) + @classmethod + def _default_options(cls): + return [ + ("entity_cls", None), + ("schema_name", None), + ("database", None), + ] + @classmethod @abstractmethod def from_entity(cls, entity): @@ -47,23 +37,7 @@ def to_entity(cls, *args, **kwargs): def model_factory(element_cls, **kwargs): - element_cls.element_type = DomainObjects.MODEL - - if hasattr(element_cls, "Meta"): - element_cls.meta_ = ModelMeta(element_cls.Meta) - else: - element_cls.meta_ = ModelMeta() - - if not (hasattr(element_cls.meta_, "entity_cls") and element_cls.meta_.entity_cls): - element_cls.meta_.entity_cls = kwargs.pop("entity_cls", None) - - if not ( - hasattr(element_cls.meta_, "schema_name") and element_cls.meta_.schema_name - ): - element_cls.meta_.schema_name = kwargs.pop("schema_name", None) - - if not (hasattr(element_cls.meta_, "database") and element_cls.meta_.database): - element_cls.meta_.database = kwargs.pop("database", None) + element_cls = derive_element_class(element_cls, BaseModel, **kwargs) if not element_cls.meta_.entity_cls: raise IncorrectUsageError(