Skip to content
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
5 changes: 5 additions & 0 deletions docs/tutorial/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ User here is a model with one field; username. Field of username is str type, an
Also, It includes a one more field named `id`. This field is automatically generated by Pydbm
and these fields are equal.

> **Note:** The `id` field is read-only. It cannot be overwritten, redefined, or have its type changed.
> You can only change the id creation behavior by using `unique_together` in the `Config` class.
> Attempting to define `id` in your model annotations, pass it to the constructor, or modify it after creation
> will raise a `ReadOnlyFieldError`.

Now, let's create a user.

```python
Expand Down
7 changes: 7 additions & 0 deletions src/pydbm/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"ValidationError",
"EmptyModelError",
"UnnecessaryParamsError",
"ReadOnlyFieldError",
)


Expand Down Expand Up @@ -44,3 +45,9 @@ class UnnecessaryParamsError(PydbmBaseException, ValueError):
"""Exception for invalid params."""

pass


class ReadOnlyFieldError(PydbmBaseException, AttributeError):
"""Exception for attempting to overwrite or redefine a read-only field."""

pass
7 changes: 6 additions & 1 deletion src/pydbm/models/fields/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import uuid

from pydbm import contstant as C
from pydbm.exceptions import ReadOnlyFieldError
from pydbm.models.fields.base import BaseField

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -35,7 +36,11 @@ def __init__(self, field_name: str, field_type: SupportedClassT, *, unique_toget
super().__init__(default_factory=self.generate_id, **kwargs)

def __set__(self, instance: DbmModel, value: typing.Any) -> None:
if (fields := getattr(instance, "fields", None)) is not None and C.PRIMARY_KEY not in fields:
if getattr(instance, "fields", None) is not None:
if hasattr(instance, self.private_name):
raise ReadOnlyFieldError(
f"'{C.PRIMARY_KEY}' field is read-only and cannot be modified after creation."
)
return super().__set__(instance, value)

def __call__(self: Self, fields: dict[str, typing.Any] | None = None, *args, **kwargs) -> Self: # type: ignore[valid-type, override] # noqa: E501
Expand Down
12 changes: 11 additions & 1 deletion src/pydbm/models/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydbm import contstant as C
from pydbm import typing_extra
from pydbm.database import DatabaseManager
from pydbm.exceptions import EmptyModelError, PydbmBaseException, UnnecessaryParamsError
from pydbm.exceptions import EmptyModelError, PydbmBaseException, ReadOnlyFieldError, UnnecessaryParamsError
from pydbm.inspect_extra import get_obj_annotations
from pydbm.models.fields import AutoField, Field, Undefined

Expand All @@ -31,6 +31,11 @@ class Meta(type):
@staticmethod
def __new__(mcs, cls_name: str, bases: tuple[Meta, ...], namespace: dict[str, typing.Any], **kwargs: typing.Any) -> type: # noqa: E501
annotations = namespace.pop("__annotations__", {})
if [b for b in bases if isinstance(b, mcs)] and C.PRIMARY_KEY in annotations:
raise ReadOnlyFieldError(
f"'{C.PRIMARY_KEY}' field is auto-generated and cannot be overwritten or change type."
" Use 'unique_together' in Config to change id creation behavior."
)
annotations[C.PRIMARY_KEY] = str
slots = mcs.generate_slots(annotations)
if not [b for b in bases if isinstance(b, mcs)]:
Expand Down Expand Up @@ -62,6 +67,11 @@ def __init__(cls, cls_name: str, bases: tuple[Meta, ...], namespace: dict[str, t
setattr(cls, key, value)

def __call__(cls, **kwargs):
if C.PRIMARY_KEY in kwargs:
raise ReadOnlyFieldError(
f"'{C.PRIMARY_KEY}' field is auto-generated and cannot be passed as an argument."
)

for extra_field_name in (set(kwargs.keys()) - set(cls.__annotations__.keys())):
raise UnnecessaryParamsError(f"{extra_field_name} is not defined in {cls.__name__}")

Expand Down
47 changes: 44 additions & 3 deletions tests/models/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from pydbm import DbmModel
from pydbm.exceptions import EmptyModelError
from pydbm.exceptions import EmptyModelError, ReadOnlyFieldError


class Model(DbmModel):
Expand Down Expand Up @@ -278,11 +278,11 @@ class EmptyModel(DbmModel):


def test_base_only_id_field_model():
with pytest.raises(EmptyModelError) as cm:
with pytest.raises(ReadOnlyFieldError) as cm:
class EmptyModel(DbmModel):
id: str

assert str(cm.value) == "Empty model is not allowed."
assert str(cm.value) == "'id' field is auto-generated and cannot be overwritten or change type. Use 'unique_together' in Config to change id creation behavior."


def test_base_update_obj_on_db_when_updating_the_field_on_the_instance(teardown_db):
Expand Down Expand Up @@ -448,3 +448,44 @@ class Config:
assert created2 is False
assert instance.id == instance2.id
assert Model.objects.count() == 1


def test_id_field_cannot_be_defined_in_model():
with pytest.raises(ReadOnlyFieldError) as cm:
class Model(DbmModel):
id: str
name: str

assert "'id' field is auto-generated" in str(cm.value)


def test_id_field_cannot_change_type():
with pytest.raises(ReadOnlyFieldError) as cm:
class Model(DbmModel):
id: int
name: str

assert "'id' field is auto-generated" in str(cm.value)


def test_id_field_cannot_be_passed_to_constructor():
class Model(DbmModel):
name: str

with pytest.raises(ReadOnlyFieldError) as cm:
Model(id="custom_id", name="test")

assert "'id' field is auto-generated and cannot be passed as an argument" in str(cm.value)


def test_id_field_cannot_be_overwritten_after_creation():
class Model(DbmModel):
name: str

model = Model(name="test")
assert model.id is not None

with pytest.raises(ReadOnlyFieldError) as cm:
model.id = "new_id"

assert "'id' field is read-only and cannot be modified after creation" in str(cm.value)