Skip to content

Commit

Permalink
fix copying registry & models. Fix relationships among tenancy object…
Browse files Browse the repository at this point in the history
…s. (#259)

Changes:

- fix copying registry+tests
- M2M-Field `create_through_model` allows now the keyword only argument `replace_related_field`.
- `add_to_registry` has now an additional keyword `replace_related_field_m2m` for seperate controlling the `create_through_model` registration logic.
- `add_to_registry` has at most one positional argument. It was intended this way but not enforced.
- `create_edgy_model` passes through additional keyword arguments to the edgy model class.
- add on_conflict for handling model conflicts
- fix invalidation causing _db_schemas removed
- instead of passing down keyword arguments from create_edgy_model to type add an
  argument matching the other behaviour
- fix foreign keys with tenancy
  • Loading branch information
devkral authored Jan 13, 2025
1 parent 3a9ecc6 commit 6b80a02
Show file tree
Hide file tree
Showing 36 changed files with 1,311 additions and 190 deletions.
4 changes: 3 additions & 1 deletion docs/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ The reverse end of a `ForeignKey` is a [Many to one relation](./queries/many-to-
##### Parameters

* `to` - A string [model](./models.md) name or a class object of that same model.
* `target_registry` - Registry where the model callback is installed if `to` is a string.
* `target_registry` - Registry where the model callback is installed if `to` is a string. Defaults to the field owner registry.
* `related_name` - The name to use for the relation from the related object back to this one. Can be set to `False` to disable a reverse connection.
Note: Setting to `False` will also prevent prefetching and reversing via `__`.
See also [related_name](./queries/related-name.md) for defaults
Expand Down Expand Up @@ -586,11 +586,13 @@ class MyModel(edgy.Model):
##### Parameters

* `to` - A string [model](./models.md) name or a class object of that same model.
* `target_registry` - Registry where the model callback is installed if `to` is a string. Defaults to the field owner registry.
* `from_fields` - Provide the `related_fields` for the implicitly generated ForeignKey to the owner model.
* `to_fields` - Provide the `related_fields` for the implicitly generated ForeignKey to the child model.
* `related_name` - The name to use for the relation from the related object back to this one.
* `through` - The model to be used for the relationship. Edgy generates the model by default
if None is provided or `through` is an abstract model.
* `through_registry` - Registry where the model callback is installed if `through` is a string or empty. Defaults to the field owner registry.
* `through_tablename` - Custom tablename for `through`. E.g. when special characters are used in model names.
* `embed_through` - When traversing, embed the through object in this attribute. Otherwise it is not accessable from the result.
if an empty string was provided, the old behaviour is used to query from the through model as base (default).
Expand Down
14 changes: 13 additions & 1 deletion docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ For this the `StrictModel` model can be used. Otherwise it behaves like a normal

There is no strict version of a `ReflectModel` because the laxness is required.


### Loading models

You may have the models distributed among multiple files and packages.
Expand Down Expand Up @@ -97,6 +98,17 @@ If no `id` is declared in the model, **Edgy** will automatically generate an `id

Earlier there were many restrictions. Now they were lifted

### Controlling collision behaviour

Earlier models were simply replaced when defining a model with the same name or adding such.

Now the default is to error when a collision was detected, or in case the `on_conflict` parameter was set, either
a `replace` or `keep` executed.

``` python
{!> ../docs_src/models/on_conflict.py !}
```

#### What you should not do

##### Declaring an IntegerField as primary key without autoincrement set
Expand Down Expand Up @@ -171,7 +183,7 @@ to copy a model class and optionally add it to an other registry.

You can add it to a registry later by using:

`model_class.add_to_registry(registry, name="", database=None, replace_related_field=False)`
`model_class.add_to_registry(registry, name="", database=None, replace_related_field=...)`

In fact the last method is called when the registry parameter of `copy_edgy_model` is not `None`.

Expand Down
35 changes: 35 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ hide:

# Release Notes

## 0.24.3

### Added

- ManyToManyField `create_through_model` method allows now the keyword only argument `replace_related_field`.
- `add_to_registry` and models have now an additional keyword-only argument `on_conflict` for controlling what happens when a same named model already exists.
For models this can be passed : `class Foo(edgy.Model, on_conflict="keep"): ...`.
- Passing a tuple or list of types to `replace_related_field` is now allowed.
- Add `through_registry` to ManyToMany.
- Add `no_copy` to models MetaInfo.
- Add `ModelCollisionError` exception.
- Add keyword only hook function `real_add_to_registry`. It can be used to customize the `add_to_registry` behaviour.

### Changed

- `create_edgy_model` has now `__type_kwargs__` which contains a dict of keyword arguments provided to `__new__` of type.
- RelatedField uses now `no_copy`.
- `add_to_registry` returns the type which was actually added to registry instead of None.
- Through models use now `no_copy` when autogenerated. This way they don't land in copied registries but are autogenerated again.
- Instead of silent replacing models with the same `__name__` now an error is raised.
- `skip_registry` has now also an allowed literal value: `"allow_search"`. It enables the search of the registry but doesn't register the model.

### Fixed

- Copying registries and models is working now.
- Fix deleting (clearing cache) of BaseForeignKey target.
- Creating two models with the same name did lead to silent replacements.
- Invalidating caused schema errors.
- ManyToMany and ForeignKey fields didn't worked when referencing tenant models.
- ManyToMany fields didn't worked when specified on tenant models.

### BREAKING

- Instead of silent replacing models with the same `__name__` now an error is raised.
- The return value of `add_to_registry` changed. If you customize the function you need to return now the actual model added to the registry.

## 0.24.2

Expand Down
19 changes: 19 additions & 0 deletions docs_src/models/on_conflict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import edgy

models = ...


class Foo(edgy.Model, on_conflict="keep"):
class Meta:
registry = models


# or


class Foo2(edgy.Model):
class Meta:
registry = False


Foo2.add_to_registry(models, name="Foo", on_conflict="replace")
2 changes: 1 addition & 1 deletion edgy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

__version__ = "0.24.2"
__version__ = "0.24.3"
from typing import TYPE_CHECKING

from ._monkay import Instance, create_monkay
Expand Down
12 changes: 1 addition & 11 deletions edgy/contrib/autoreflection/metaclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,24 +69,14 @@ def __new__(
name: str,
bases: tuple[type, ...],
attrs: dict[str, Any],
skip_registry: bool = False,
meta_info_class: type[AutoReflectionMetaInfo] = AutoReflectionMetaInfo,
**kwargs: Any,
) -> Any:
new_model = super().__new__(
return super().__new__(
cls,
name,
bases,
attrs,
meta_info_class=meta_info_class,
skip_registry=True,
**kwargs,
)
if (
not skip_registry
and isinstance(new_model.meta, AutoReflectionMetaInfo)
and not new_model.meta.abstract
and new_model.meta.registry
):
new_model.meta.registry.pattern_models[new_model.__name__] = new_model
return new_model
11 changes: 10 additions & 1 deletion edgy/contrib/autoreflection/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
from typing import ClassVar
from typing import TYPE_CHECKING, Any, ClassVar

import edgy

from .metaclasses import AutoReflectionMeta, AutoReflectionMetaInfo

if TYPE_CHECKING:
from edgy.core.db.models.types import BaseModelType


class AutoReflectModel(edgy.ReflectModel, metaclass=AutoReflectionMeta):
meta: ClassVar[AutoReflectionMetaInfo]

@classmethod
def real_add_to_registry(cls, **kwargs: Any) -> type["BaseModelType"]:
if isinstance(cls.meta, AutoReflectionMetaInfo):
kwargs.setdefault("registry_type_name", "pattern_models")
return super().real_add_to_registry(**kwargs)
26 changes: 25 additions & 1 deletion edgy/contrib/multi_tenancy/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from typing import ClassVar
from typing import TYPE_CHECKING, Any, ClassVar

from edgy.contrib.multi_tenancy.metaclasses import BaseTenantMeta, TenantMeta
from edgy.core.db.models.model import Model

if TYPE_CHECKING:
from edgy.core.db.models.types import BaseModelType


class TenantModel(Model, metaclass=BaseTenantMeta):
"""
Expand All @@ -16,3 +19,24 @@ class TenantModel(Model, metaclass=BaseTenantMeta):
"""

meta: ClassVar[TenantMeta] = TenantMeta(None, abstract=True)

@classmethod
def real_add_to_registry(cls, **kwargs: Any) -> type["BaseModelType"]:
result = super().real_add_to_registry(**kwargs)

if (
cls.meta.registry
and cls.meta.is_tenant
and not cls.meta.abstract
and not cls.__is_proxy_model__
):
assert cls.__reflected__ is False, (
"Reflected models are not compatible with multi_tenancy"
)

if not cls.meta.register_default:
# remove from models
cls.meta.registry.models.pop(cls.__name__, None)
cls.meta.registry.tenant_models[cls.__name__] = cls

return result
42 changes: 29 additions & 13 deletions edgy/contrib/multi_tenancy/metaclasses.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from typing import Any, Optional, cast
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast

from edgy.core.db.models.metaclasses import (
BaseModelMeta,
MetaInfo,
)

if TYPE_CHECKING:
from edgy.core.connection.database import Database


def _check_model_inherited_tenancy(bases: tuple[type, ...]) -> bool:
for base in bases:
Expand Down Expand Up @@ -42,23 +45,36 @@ class BaseTenantMeta(BaseModelMeta):
your own tenant model using the `is_tenant` inside the `Meta` object.
"""

def __new__(cls, name: str, bases: tuple[type, ...], attrs: Any, **kwargs: Any) -> Any:
new_model = super().__new__(cls, name, bases, attrs, meta_info_class=TenantMeta, **kwargs)
def __new__(
cls,
name: str,
bases: tuple[type, ...],
attrs: Any,
on_conflict: Literal["error", "replace", "keep"] = "error",
skip_registry: Union[bool, Literal["allow_search"]] = False,
meta_info_class: type[TenantMeta] = TenantMeta,
**kwargs: Any,
) -> Any:
database: Union[Literal["keep"], None, Database, bool] = attrs.get("database", "keep")
new_model = super().__new__(
cls,
name,
bases,
attrs,
skip_registry="allow_search",
meta_info_class=meta_info_class,
**kwargs,
)
if new_model.meta.is_tenant is None:
new_model.meta.is_tenant = _check_model_inherited_tenancy(bases)

if (
new_model.meta.registry
and new_model.meta.is_tenant
not skip_registry
and new_model.meta.registry
and not new_model.meta.abstract
and not new_model.__is_proxy_model__
):
assert (
new_model.__reflected__ is False
), "Reflected models are not compatible with multi_tenancy"

if not new_model.meta.register_default:
# remove from models
new_model.meta.registry.models.pop(new_model.__name__, None)
new_model.meta.registry.tenant_models[new_model.__name__] = new_model
new_model.add_to_registry(
new_model.meta.registry, on_conflict=on_conflict, database=database
)
return new_model
Loading

0 comments on commit 6b80a02

Please sign in to comment.