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

✨ MainBaseModel #129

Merged
merged 1 commit into from
May 23, 2024
Merged
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
2 changes: 1 addition & 1 deletion pyodmongo/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
63 changes: 3 additions & 60 deletions pyodmongo/models/db_model.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
93 changes: 93 additions & 0 deletions pyodmongo/models/metaclasses.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 15 additions & 1 deletion tests/test_class.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Loading