From 2485a6bea0a78d823d28691d76fa4cf126046540 Mon Sep 17 00:00:00 2001 From: Mauro Andre Date: Thu, 23 May 2024 04:59:19 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20MainBaseModel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyodmongo/__init__.py | 2 +- pyodmongo/models/db_model.py | 63 ++-------------------- pyodmongo/models/metaclasses.py | 93 +++++++++++++++++++++++++++++++++ tests/test_class.py | 16 +++++- 4 files changed, 112 insertions(+), 62 deletions(-) create mode 100644 pyodmongo/models/metaclasses.py diff --git a/pyodmongo/__init__.py b/pyodmongo/__init__.py index 9178a3b..236e257 100644 --- a/pyodmongo/__init__.py +++ b/pyodmongo/__init__.py @@ -1,6 +1,6 @@ from .async_engine.engine import AsyncDbEngine from .engine.engine import DbEngine -from .models.db_model import DbModel +from .models.db_model import DbModel, MainBaseModel from .models.id_model import Id from .models.paginate import ResponsePaginate from .models.responses import SaveResponse, DeleteResponse diff --git a/pyodmongo/models/db_model.py b/pyodmongo/models/db_model.py index 41a8d47..6174a1f 100644 --- a/pyodmongo/models/db_model.py +++ b/pyodmongo/models/db_model.py @@ -1,70 +1,13 @@ from pydantic import ConfigDict from .id_model import Id from datetime import datetime -from typing import Any, ClassVar -from ..services.model_init import ( - resolve_indexes, - resolve_class_fields_db_info, - resolve_reference_pipeline, - # resolve_db_fields, -) +from typing import ClassVar from pydantic import BaseModel -from pydantic._internal._model_construction import ModelMetaclass -from typing_extensions import dataclass_transform from typing import ClassVar -import copy - - -@dataclass_transform(kw_only_default=True) -class DbMeta(ModelMetaclass): - """ - Metaclass for database model entities in a PyODMongo environment. It extends - the functionality of the ModelMetaclass by applying specific behaviors and - transformations related to database operations such as indexing, reference - resolution, and initialization of database fields. - - Attributes: - __pyodmongo_complete__ (bool): Attribute used to track the completion of - the meta-level configuration. - - Methods: - __new__(cls, name, bases, namespace, **kwargs): Constructs a new class - instance, ensuring database-specific adjustments and initializations - are applied. - __getattr__(cls, name): Custom attribute access handling that supports - dynamic attributes based on database field definitions. - """ - - def __new__( - cls, name: str, bases: tuple[Any], namespace: dict, **kwargs: Any - ) -> type: - setattr(cls, "__pyodmongo_complete__", False) - for base in bases: - setattr(base, "__pyodmongo_complete__", False) - - # TODO finish db_fields after ModelMetaclass - # db_fields = copy.deepcopy(namespace.get("__annotations__")) - # db_fields = resolve_db_fields(bases=bases, db_fields=db_fields) - - cls: BaseModel = ModelMetaclass.__new__(cls, name, bases, namespace, **kwargs) - - setattr(cls, "__pyodmongo_complete__", True) - for base in bases: - setattr(base, "__pyodmongo_complete__", True) +from .metaclasses import PyOdmongoMeta, DbMeta - resolve_class_fields_db_info(cls=cls) - pipeline = resolve_reference_pipeline(cls=cls, pipeline=[]) - setattr(cls, "_reference_pipeline", pipeline) - indexes = resolve_indexes(cls=cls) - setattr(cls, "_init_indexes", indexes) - return cls - def __getattr__(cls, name: str): - if cls.__dict__.get("__pyodmongo_complete__"): - is_attr = name in cls.__dict__.get("model_fields").keys() - if is_attr: - return cls.__dict__.get(name + "__pyodmongo") - ModelMetaclass.__getattr__(cls, name) +class MainBaseModel(BaseModel, metaclass=PyOdmongoMeta): ... class DbModel(BaseModel, metaclass=DbMeta): diff --git a/pyodmongo/models/metaclasses.py b/pyodmongo/models/metaclasses.py new file mode 100644 index 0000000..1f4a160 --- /dev/null +++ b/pyodmongo/models/metaclasses.py @@ -0,0 +1,93 @@ +from typing import Any +from ..services.model_init import ( + resolve_indexes, + resolve_class_fields_db_info, + resolve_reference_pipeline, +) +from pydantic import BaseModel +from pydantic._internal._model_construction import ModelMetaclass +from typing_extensions import dataclass_transform + + +@dataclass_transform(kw_only_default=True) +class PyOdmongoMeta(ModelMetaclass): + """ + Metaclass for creating and configuring PyODMongo models. This metaclass extends + the functionality of Pydantic's ModelMetaclass to include database-specific + configurations and transformations. + + The primary responsibilities of this metaclass are: + - Setting and tracking the `__pyodmongo_complete__` attribute to ensure proper + initialization of database-related features. + - Applying necessary transformations to class fields for database compatibility. + - Providing custom attribute access behavior for dynamically generated attributes. + + Attributes: + __pyodmongo_complete__ (bool): Indicator of whether the meta-level + configuration is complete. + + Methods: + __new__(cls, name, bases, namespace, **kwargs): Constructs a new class instance, + applying database-specific initializations. + __getattr__(cls, name): Custom attribute access method supporting dynamic + attribute retrieval based on model fields. + """ + + def __new__( + cls, name: str, bases: tuple[Any], namespace: dict, **kwargs: Any + ) -> type: + setattr(cls, "__pyodmongo_complete__", False) + for base in bases: + setattr(base, "__pyodmongo_complete__", False) + + cls: BaseModel = ModelMetaclass.__new__(cls, name, bases, namespace, **kwargs) + + setattr(cls, "__pyodmongo_complete__", True) + for base in bases: + setattr(base, "__pyodmongo_complete__", True) + + resolve_class_fields_db_info(cls=cls) + return cls + + def __getattr__(cls, name: str): + if cls.__dict__.get("__pyodmongo_complete__"): + is_attr = name in cls.__dict__.get("model_fields").keys() + if is_attr: + return cls.__dict__.get(name + "__pyodmongo") + ModelMetaclass.__getattr__(cls, name) + + +@dataclass_transform(kw_only_default=True) +class DbMeta(PyOdmongoMeta): + """ + Metaclass for database model entities in a PyODMongo environment. It extends + the functionality of the PyOdmongoMeta by applying specific behaviors and + transformations related to database operations such as indexing, reference + resolution, and initialization of database fields. + + The primary responsibilities of this metaclass are: + - Constructing new class instances with additional database-specific adjustments + and initializations. + - Setting up pipelines for resolving references within the database context. + - Configuring indexes for efficient database operations. + + Attributes: + __pyodmongo_complete__ (bool): Attribute used to track the completion of + the meta-level configuration. + + Methods: + __new__(cls, name, bases, namespace, **kwargs): Constructs a new class instance, + ensuring database-specific adjustments and initializations are applied. + """ + + def __new__( + cls, name: str, bases: tuple[Any], namespace: dict, **kwargs: Any + ) -> type: + + cls: BaseModel = PyOdmongoMeta.__new__(cls, name, bases, namespace, **kwargs) + + pipeline = resolve_reference_pipeline(cls=cls, pipeline=[]) + setattr(cls, "_reference_pipeline", pipeline) + indexes = resolve_indexes(cls=cls) + setattr(cls, "_init_indexes", indexes) + return cls diff --git a/tests/test_class.py b/tests/test_class.py index 260f922..8dbf429 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -1,10 +1,24 @@ -from pyodmongo import DbModel, Id +from pyodmongo import MainBaseModel, DbModel, Id +from pyodmongo.models.db_field_info import DbField from pydantic import EmailStr, ValidationError from datetime import datetime from typing import ClassVar, Union import pytest +def test_main_base_model(): + class MainModel(MainBaseModel): + attr_1: int + attr_2: str + + assert issubclass(MainModel, MainBaseModel) + assert isinstance(MainModel.attr_1, DbField) + assert isinstance(MainModel.attr_2, DbField) + obj = MainModel(attr_1=1, attr_2="one") + assert obj.attr_1 == 1 + assert obj.attr_2 == "one" + + def test_create_class(): class MyClass(DbModel): _collection: ClassVar = "myclass"