add base signal class

This commit is contained in:
collerek
2020-12-06 10:28:48 +01:00
parent 9838547c4f
commit 2bbfd05017
8 changed files with 112 additions and 11 deletions

View File

@ -1,3 +1,10 @@
# 0.7.0
* **Breaking:** QuerySet `bulk_update` method now raises `ModelPersistenceError` for unsaved models passed instead of `QueryDefinitionError`
* **Breaking:** Model initialization with unknown field name now raises `ModelError` instead of `KeyError`
*
* Performance optimization
# 0.6.2 # 0.6.2
* Performance optimization * Performance optimization
@ -12,7 +19,7 @@
# 0.6.0 # 0.6.0
* **Breaking:** calling instance.load() when the instance row was deleted from db now raises ormar.NoMatch instead of ValueError * **Breaking:** calling instance.load() when the instance row was deleted from db now raises `NoMatch` instead of `ValueError`
* **Breaking:** calling add and remove on ReverseForeignKey relation now updates the child model in db setting/removing fk column * **Breaking:** calling add and remove on ReverseForeignKey relation now updates the child model in db setting/removing fk column
* **Breaking:** ReverseForeignKey relation now exposes QuerySetProxy API like ManyToMany relation * **Breaking:** ReverseForeignKey relation now exposes QuerySetProxy API like ManyToMany relation
* **Breaking:** querying related models from ManyToMany cleans list of related models loaded on parent model: * **Breaking:** querying related models from ManyToMany cleans list of related models loaded on parent model:

View File

@ -1,5 +1,5 @@
from ormar.decorators import property_field from ormar.decorators import property_field
from ormar.exceptions import ModelDefinitionError, ModelNotSet, MultipleMatches, NoMatch from ormar.exceptions import ModelDefinitionError, MultipleMatches, NoMatch
from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100 from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100
from ormar.fields import ( # noqa: I100 from ormar.fields import ( # noqa: I100
BigInteger, BigInteger,
@ -47,7 +47,6 @@ __all__ = [
"ManyToMany", "ManyToMany",
"Model", "Model",
"ModelDefinitionError", "ModelDefinitionError",
"ModelNotSet",
"MultipleMatches", "MultipleMatches",
"NoMatch", "NoMatch",
"ForeignKey", "ForeignKey",

View File

@ -13,7 +13,7 @@ def property_field(func: Callable) -> Union[property, Callable]:
if len(arguments) > 1 or arguments[0] != "self": if len(arguments) > 1 or arguments[0] != "self":
raise ModelDefinitionError( raise ModelDefinitionError(
"property_field decorator can be used " "property_field decorator can be used "
"only on class methods with no arguments" "only on methods with no arguments"
) )
func.__dict__["__property_field__"] = True func.__dict__["__property_field__"] = True
return func return func

View File

@ -1,28 +1,57 @@
class AsyncOrmException(Exception): class AsyncOrmException(Exception):
"""
Base ormar Exception
"""
pass pass
class ModelDefinitionError(AsyncOrmException): class ModelDefinitionError(AsyncOrmException):
"""
Raised for errors related to the model definition itself.
* setting @property_field on method with arguments other than func(self)
* defining a Field without required parameters
* defining a model with more than one primary_key
* defining a model without primary_key
* setting primary_key column as pydantic_only
"""
pass pass
class ModelError(AsyncOrmException): class ModelError(AsyncOrmException):
pass """
Raised for initialization of model with non-existing field keyword.
"""
class ModelNotSet(AsyncOrmException):
pass pass
class NoMatch(AsyncOrmException): class NoMatch(AsyncOrmException):
"""
Raised for database queries that has no matching result (empty result).
"""
pass pass
class MultipleMatches(AsyncOrmException): class MultipleMatches(AsyncOrmException):
"""
Raised for database queries that should return one row (i.e. get, first etc.)
but has multiple matching results in response.
"""
pass pass
class QueryDefinitionError(AsyncOrmException): class QueryDefinitionError(AsyncOrmException):
"""
Raised for errors in query definition.
* using contains or icontains filter with instance of the Model
* using Queryset.update() without filter and setting each flag to True
* using Queryset.delete() without filter and setting each flag to True
"""
pass pass
@ -31,4 +60,17 @@ class RelationshipInstanceError(AsyncOrmException):
class ModelPersistenceError(AsyncOrmException): class ModelPersistenceError(AsyncOrmException):
"""
Raised for update of models without primary_key set (cannot retrieve from db)
or for saving a model with relation to unsaved model (cannot extract fk value).
"""
pass
class SignalDefinitionError(AsyncOrmException):
"""
Raised when non callable receiver is passed as signal callback.
"""
pass pass

View File

@ -6,7 +6,7 @@ from sqlalchemy import bindparam
import ormar # noqa I100 import ormar # noqa I100
from ormar import MultipleMatches, NoMatch from ormar import MultipleMatches, NoMatch
from ormar.exceptions import QueryDefinitionError from ormar.exceptions import ModelPersistenceError, QueryDefinitionError
from ormar.queryset import FilterQuery from ormar.queryset import FilterQuery
from ormar.queryset.clause import QueryClause from ormar.queryset.clause import QueryClause
from ormar.queryset.prefetch_query import PrefetchQuery from ormar.queryset.prefetch_query import PrefetchQuery
@ -446,7 +446,7 @@ class QuerySet:
for objt in objects: for objt in objects:
new_kwargs = objt.dict() new_kwargs = objt.dict()
if pk_name not in new_kwargs or new_kwargs.get(pk_name) is None: if pk_name not in new_kwargs or new_kwargs.get(pk_name) is None:
raise QueryDefinitionError( raise ModelPersistenceError(
"You cannot update unsaved objects. " "You cannot update unsaved objects. "
f"{self.model.__name__} has to have {pk_name} filled." f"{self.model.__name__} has to have {pk_name} filled."
) )

View File

53
ormar/signals/signal.py Normal file
View File

@ -0,0 +1,53 @@
import asyncio
import inspect
from typing import Any, Callable, List, Tuple, Union
from ormar.exceptions import SignalDefinitionError
def callable_accepts_kwargs(func: Callable) -> bool:
return any(
p
for p in inspect.signature(func).parameters.values()
if p.kind == p.VAR_KEYWORD
)
def make_id(target: Any) -> Union[int, Tuple[int, int]]:
if hasattr(target, "__func__"):
return id(target.__self__), id(target.__func__)
return id(target)
class Signal:
def __init__(self) -> None:
self._receivers: List[Tuple[Union[int, Tuple[int, int]], Callable]] = []
def connect(self, receiver: Callable) -> None:
if not callable(receiver):
raise SignalDefinitionError("Signal receivers must be callable.")
if not callable_accepts_kwargs(receiver):
raise SignalDefinitionError(
"Signal receivers must accept **kwargs argument."
)
new_receiver_key = make_id(receiver)
if not any(rec_id == new_receiver_key for rec_id, _ in self._receivers):
self._receivers.append((new_receiver_key, receiver))
def disconnect(self, receiver: Callable) -> bool:
removed = False
new_receiver_key = make_id(receiver)
for ind, rec in enumerate(self._receivers):
rec_id, _ = rec
if rec_id == new_receiver_key:
removed = True
del self._receivers[ind]
break
return removed
async def send(self, sender: Any, **kwargs: Any) -> None:
receivers = []
for receiver in self._receivers:
_, receiver_func = receiver
receivers.append(receiver_func(sender, **kwargs))
await asyncio.gather(*receivers)

View File

@ -5,7 +5,7 @@ import pytest
import sqlalchemy import sqlalchemy
import ormar import ormar
from ormar.exceptions import QueryDefinitionError from ormar.exceptions import ModelPersistenceError, QueryDefinitionError
from tests.settings import DATABASE_URL from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True) database = databases.Database(DATABASE_URL, force_rollback=True)
@ -302,7 +302,7 @@ async def test_bulk_update_with_relation():
async def test_bulk_update_not_saved_objts(): async def test_bulk_update_not_saved_objts():
async with database: async with database:
category = await Category.objects.create(name="Sample Category") category = await Category.objects.create(name="Sample Category")
with pytest.raises(QueryDefinitionError): with pytest.raises(ModelPersistenceError):
await Note.objects.bulk_update( await Note.objects.bulk_update(
[ [
Note(text="Buy the groceries.", category=category), Note(text="Buy the groceries.", category=category),