Skip to content

Commit

Permalink
NullableObjectId (#62)
Browse files Browse the repository at this point in the history
* Add new NullableObjectId field
* Add docs for NullableObjectId
  • Loading branch information
tarsil authored Sep 12, 2024
1 parent 24be0eb commit edeb3ee
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 3 deletions.
26 changes: 26 additions & 0 deletions docs/en/docs/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,33 @@ import mongoz

class MyDocument(mongoz.Document):
an_id: ObjectId = mongoz.ObjectId()
```

#### NullableObjectId

This is another special field that extends the `bson.ObjectId` and on the contrary of the [ObjectId](#objectid),
this one allows to specify null fields and not null as it derives from the mongoz core FieldFactory.

If defaults to `null=True` and it can be specified to `null=False` if required.

**Default**

```python
import mongoz


class MyDocument(mongoz.Document):
an_id: ObjectId = mongoz.NullableObjectId()
```

**Null False**

```python
import mongoz


class MyDocument(mongoz.Document):
an_id: ObjectId = mongoz.NullableObjectId(null=False)
```

#### String
Expand Down
27 changes: 27 additions & 0 deletions docs/pt/docs/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,33 @@ class MyDocument(mongoz.Document):
an_id: ObjectId = mongoz.ObjectId()
```

#### NullableObjectId

Este é outro campo especial que estende o `bson.ObjectId` e, ao contrário do [ObjectId](#objectid),
este permite especificar campos nulos e não nulos visto que deriva do FieldFactory do Mongoz.

Por defeito, é definido como `null=True` e pode ser especificado como `null=False`, se necessário.

**Por defeito**

```python
import mongoz


class MyDocument(mongoz.Document):
an_id: ObjectId = mongoz.NullableObjectId()
```

**Null False**

```python
import mongoz


class MyDocument(mongoz.Document):
an_id: ObjectId = mongoz.NullableObjectId(null=False)
```

#### String

```python
Expand Down
2 changes: 2 additions & 0 deletions mongoz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Email,
Embed,
Integer,
NullableObjectId,
Object,
ObjectId,
String,
Expand Down Expand Up @@ -54,6 +55,7 @@
"Index",
"IndexType",
"Integer",
"NullableObjectId",
"Manager",
"MongozSettings",
"MultipleDocumentsReturned",
Expand Down
3 changes: 2 additions & 1 deletion mongoz/core/db/documents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

mongoz_setattr = object.__setattr__

DOCUMENT_KEYS = {"id", "_id", "pk"}


class BaseMongoz(BaseModel, metaclass=BaseModelMeta):
"""
Expand Down Expand Up @@ -70,7 +72,6 @@ def extract_default_values_from_field(
if key not in self.meta.fields:
if not hasattr(self, key):
raise ValueError(f"Invalid keyword {key} for class {self.__class__.__name__}")

# For non values. Example: bool
if value is not None:
# Checks if the default is a callable and executes it.
Expand Down
2 changes: 2 additions & 0 deletions mongoz/core/db/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Email,
Embed,
Integer,
NullableObjectId,
Object,
ObjectId,
String,
Expand All @@ -38,4 +39,5 @@
"Time",
"UUID",
"Embed",
"NullableObjectId",
]
15 changes: 15 additions & 0 deletions mongoz/core/db/fields/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ def __get_pydantic_core_schema__(
return general_plain_validator_function(cls._validate)


class NullableObjectId(FieldFactory, ObjectId):
_type = ObjectId

def __new__( # type: ignore
cls,
null: bool = True,
**kwargs: Any,
) -> BaseField:
kwargs = {
**kwargs,
**{key: value for key, value in locals().items() if key not in CLASS_DEFAULTS},
}
return super().__new__(cls, **kwargs)


class String(FieldFactory, str):
"""String field representation that constructs the Field class and populates the values"""

Expand Down
1 change: 1 addition & 0 deletions mongoz/core/db/querysets/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from mongoz.core.db.datastructures import Order
from mongoz.utils.enums import ExpressionOperator

if TYPE_CHECKING: # pragma: no cover
from mongoz.core.db.fields.base import MongozField

Expand Down
3 changes: 2 additions & 1 deletion mongoz/core/utils/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]:

potential_fields.update(get_model_fields(attrs))
for field_name, field in potential_fields.items():
model_fields[field_name] = field
field.name = field_name

default_type = field.field_type if not field.null else Union[field.field_type, None]
overwrite_type = (
field.__original_type__ if field.field_type != field.__original_type__ else None
)
field.annotation = overwrite_type or default_type
model_fields[field_name] = field
attrs["__annotations__"][field_name] = overwrite_type or default_type

return attrs, model_fields
23 changes: 22 additions & 1 deletion mongoz/utils/inspect.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import inspect
from typing import Callable
from inspect import isclass
from typing import Any, Callable

from typing_extensions import get_origin


def func_accepts_kwargs(func: Callable) -> bool:
Expand All @@ -11,3 +14,21 @@ def func_accepts_kwargs(func: Callable) -> bool:
for param in inspect.signature(func).parameters.values()
if param.kind == param.VAR_KEYWORD
)


def is_class_and_subclass(value: Any, _type: Any) -> bool:
"""
Checks if a `value` is of type class and subclass.
by checking the origin of the value against the type being
verified.
"""
original = get_origin(value)
if not original and not isclass(value):
return False

try:
if original:
return original and issubclass(original, _type)
return issubclass(value, _type)
except TypeError:
return False
78 changes: 78 additions & 0 deletions tests/models/manager/test_object_id_null.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from typing import AsyncGenerator

import pydantic
import pytest
from pydantic_core import ValidationError

import mongoz
from mongoz import Document, ObjectId
from tests.conftest import client

pytestmark = pytest.mark.anyio
pydantic_version = pydantic.__version__[:3]


class Movie(Document):
name: str = mongoz.String()
year: int = mongoz.Integer()
producer_id: mongoz.ObjectId = mongoz.NullableObjectId()

class Meta:
registry = client
database = "test_db"


@pytest.fixture(scope="function", autouse=True)
async def prepare_database() -> AsyncGenerator:
await Movie.objects.delete()
yield
await Movie.objects.delete()


async def test_nullable_objectid() -> None:
await Movie.objects.using("test_my_db").delete()
await Movie.objects.using("test_my_db").create(name="latest_movie", year=2024)

movie = await Movie.objects.using("test_my_db").get()
assert movie.name == "latest_movie"
assert movie.year == 2024
assert movie.producer_id is None


async def test_nullable_objectid_raises_error_on_type() -> None:
with pytest.raises(ValidationError):
await Movie.objects.create(name="latest_movie", year=2024, producer_id=123)

await Movie.objects.create(name="latest_movie", year=2024)

movie = await Movie.objects.last()

with pytest.raises(ValueError):
movie.producer_id = 1234


async def test_nullable_objectid_on_set() -> None:
await Movie.objects.create(name="latest_movie", year=2024)

movie = await Movie.objects.last()
producer_id = ObjectId()

movie.producer_id = producer_id
await movie.save()

movie = await Movie.objects.last()
assert movie.name == "latest_movie"
assert movie.year == 2024
assert str(movie.producer_id) == str(producer_id)


async def test_nullable_objectid_result() -> None:
producer_id = ObjectId()

await Movie.objects.create(name="latest_movie", year=2024, producer_id=producer_id)

movie = await Movie.objects.last()

assert movie.name == "latest_movie"
assert movie.year == 2024
assert str(movie.producer_id) == str(producer_id)

0 comments on commit edeb3ee

Please sign in to comment.