From 1a4be03131a82280210ec311c7af113a5307cf5e Mon Sep 17 00:00:00 2001 From: collerek Date: Thu, 17 Sep 2020 18:03:29 +0200 Subject: [PATCH 1/2] add exclude method on QuerySet and fix missing default values on creation --- .coverage | Bin 53248 -> 53248 bytes ormar/fields/base.py | 5 +++++ ormar/models/model.py | 1 + ormar/queryset/clause.py | 4 ++-- ormar/queryset/filter_query.py | 4 +++- ormar/queryset/join.py | 2 +- ormar/queryset/query.py | 11 ++++++++--- ormar/queryset/queryset.py | 27 +++++++++++++++++++++++++-- tests/test_models.py | 12 ++++++++++++ 9 files changed, 57 insertions(+), 9 deletions(-) diff --git a/.coverage b/.coverage index 49a54560217fb1081c660ae9a6013a303ccd779c..b11594792a134a95f3a3d01ad010ea098f58a551 100644 GIT binary patch delta 335 zcmV-V0kHmnpaX!Q1F!}l3KRejPY*s1A`cX^5fB6qlN~Q3A2>QNGdeIZ4*~-l7j|WG zb7^mGH7+tPF@Y`t1Ra7=lV2~O2rM8cEiG_lVzd4)2vAVv01x>O>JQux(GSHBv=5sP zjt^xIT@N`A4i5$n{0{RD>JH%!zYd-biw<)RY7SlwO%6T|D-IeC4Gr`S+YQqV#SM`S zaSc=rD-9J4{|x1`5fG0Ilk1E|DJLWZ0SQnfdN<#E^F5zGf1cm-`}UpR-TB+@ZQJJC z{QUWwzx?GlzYmjGjhqC(_x630_>G}V-@1F9&u^*U`yUeo0SQ18+U0+F+g*L{`)6+J zZ~wY;e82uZ-?vlOzY8D|1OW*`651F0|LyJj_PhI6-`k(vKkt40^UvMgyQ?=Jlj4pe h0(%dW2am214+aDQ2_ObqVf)+v-hKbi2eZYGJ3u!Ij9vf$ delta 275 zcmV+u0qp*OpaX!Q1F!}l3LO9sSPw@JD-RvB5fBXzlN2u_A2vEMH##vl4*~-l7j|WG zb7^mGH7+tPF@Yrk1Ra7ulU6UFv;Qv#P*Cdt5BU%358MyY55*6)51bE=4`vTu4>}JI z4+jqY4)hM|4&n~O4x$c^4tEZ24q^^b4nhtu4jv8>4fqY;4c86H4V4Xb4Ok5^4I2#w z4C}KI5S0v*<%~uKB_sp^2~3kQjTQ^O&Cj2|`O9B^^ZS!jjhqDEd;30<^o^kjx9(o& z^IPiQ2a}$TAs-zQ1OW*=651F0e|!7(yZfuJpZoLP*PlOkckixlK9k{&BLckxlLn8j Z5Do?e0SO)kT4DR!|L*&LKC{J-J3tKOW@!Kb diff --git a/ormar/fields/base.py b/ormar/fields/base.py index f3e83a4..18cdbfa 100644 --- a/ormar/fields/base.py +++ b/ormar/fields/base.py @@ -38,6 +38,11 @@ class BaseField: return Field(default=default) return None + @classmethod + def get_default(cls) -> Any: + if cls.has_default(): + return cls.default if cls.default is not None else cls.server_default + @classmethod def has_default(cls) -> bool: return cls.default is not None or cls.server_default is not None diff --git a/ormar/models/model.py b/ormar/models/model.py index fd61ba6..ae2a590 100644 --- a/ormar/models/model.py +++ b/ormar/models/model.py @@ -108,6 +108,7 @@ class Model(NewBaseModel): if not self.pk and self.Meta.model_fields.get(self.Meta.pkname).autoincrement: self_fields.pop(self.Meta.pkname, None) + self_fields = self.objects._populate_default_values(self_fields) expr = self.Meta.table.insert() expr = expr.values(**self_fields) item_id = await self.Meta.database.execute(expr) diff --git a/ormar/queryset/clause.py b/ormar/queryset/clause.py index 336db05..fd70258 100644 --- a/ormar/queryset/clause.py +++ b/ormar/queryset/clause.py @@ -29,8 +29,8 @@ class QueryClause: self, model_cls: Type["Model"], filter_clauses: List, select_related: List, ) -> None: - self._select_related = select_related - self.filter_clauses = filter_clauses + self._select_related = select_related[:] + self.filter_clauses = filter_clauses[:] self.model_cls = model_cls self.table = self.model_cls.Meta.table diff --git a/ormar/queryset/filter_query.py b/ormar/queryset/filter_query.py index 8db8185..f55d4e0 100644 --- a/ormar/queryset/filter_query.py +++ b/ormar/queryset/filter_query.py @@ -4,7 +4,8 @@ import sqlalchemy class FilterQuery: - def __init__(self, filter_clauses: List) -> None: + def __init__(self, filter_clauses: List, exclude: bool = False) -> None: + self.exclude = exclude self.filter_clauses = filter_clauses def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: @@ -13,5 +14,6 @@ class FilterQuery: clause = self.filter_clauses[0] else: clause = sqlalchemy.sql.and_(*self.filter_clauses) + clause = sqlalchemy.sql.not_(clause) if self.exclude else clause expr = expr.where(clause) return expr diff --git a/ormar/queryset/join.py b/ormar/queryset/join.py index 2d59f58..fa6ed74 100644 --- a/ormar/queryset/join.py +++ b/ormar/queryset/join.py @@ -3,7 +3,7 @@ from typing import List, NamedTuple, TYPE_CHECKING, Tuple, Type import sqlalchemy from sqlalchemy import text -from ormar.fields import ManyToManyField # noqa I100 +from ormar.fields import ManyToManyField # noqa I100 from ormar.relations import AliasManager if TYPE_CHECKING: # pragma no cover diff --git a/ormar/queryset/query.py b/ormar/queryset/query.py index 6fdbdd4..b07612a 100644 --- a/ormar/queryset/query.py +++ b/ormar/queryset/query.py @@ -12,18 +12,20 @@ if TYPE_CHECKING: # pragma no cover class Query: - def __init__( + def __init__( # noqa CFQ002 self, model_cls: Type["Model"], filter_clauses: List, + exclude_clauses: List, select_related: List, limit_count: int, offset: int, ) -> None: self.query_offset = offset self.limit_count = limit_count - self._select_related = select_related - self.filter_clauses = filter_clauses + self._select_related = select_related[:] + self.filter_clauses = filter_clauses[:] + self.exclude_clauses = exclude_clauses[:] self.model_cls = model_cls self.table = self.model_cls.Meta.table @@ -78,6 +80,9 @@ class Query: self, expr: sqlalchemy.sql.select ) -> sqlalchemy.sql.select: expr = FilterQuery(filter_clauses=self.filter_clauses).apply(expr) + expr = FilterQuery(filter_clauses=self.exclude_clauses, exclude=True).apply( + expr + ) expr = LimitQuery(limit_count=self.limit_count).apply(expr) expr = OffsetQuery(query_offset=self.query_offset).apply(expr) expr = OrderQuery(order_bys=self.order_bys).apply(expr) diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index ef3d636..46edf65 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -14,16 +14,18 @@ if TYPE_CHECKING: # pragma no cover class QuerySet: - def __init__( + def __init__( # noqa CFQ002 self, model_cls: Type["Model"] = None, filter_clauses: List = None, + exclude_clauses: List = None, select_related: List = None, limit_count: int = None, offset: int = None, ) -> None: self.model_cls = model_cls self.filter_clauses = [] if filter_clauses is None else filter_clauses + self.exclude_clauses = [] if exclude_clauses is None else exclude_clauses self._select_related = [] if select_related is None else select_related self.limit_count = limit_count self.query_offset = offset @@ -40,6 +42,12 @@ class QuerySet: rows = self.model_cls.merge_instances_list(result_rows) return rows + def _populate_default_values(self, new_kwargs: dict) -> dict: + for field_name, field in self.model_cls.Meta.model_fields.items(): + if field_name not in new_kwargs and field.has_default(): + new_kwargs[field_name] = field.get_default() + return new_kwargs + def _remove_pk_from_kwargs(self, new_kwargs: dict) -> dict: pkname = self.model_cls.Meta.pkname pk = self.model_cls.Meta.model_fields[pkname] @@ -69,6 +77,7 @@ class QuerySet: model_cls=self.model_cls, select_related=self._select_related, filter_clauses=self.filter_clauses, + exclude_clauses=self.exclude_clauses, offset=self.query_offset, limit_count=self.limit_count, ) @@ -76,22 +85,32 @@ class QuerySet: # print(exp.compile(compile_kwargs={"literal_binds": True})) return exp - def filter(self, **kwargs: Any) -> "QuerySet": # noqa: A003 + def filter(self, _exclude: bool = False, **kwargs: Any) -> "QuerySet": # noqa: A003 qryclause = QueryClause( model_cls=self.model_cls, select_related=self._select_related, filter_clauses=self.filter_clauses, ) filter_clauses, select_related = qryclause.filter(**kwargs) + if _exclude: + exclude_clauses = filter_clauses + filter_clauses = self.filter_clauses + else: + exclude_clauses = self.exclude_clauses + filter_clauses = filter_clauses return self.__class__( model_cls=self.model_cls, filter_clauses=filter_clauses, + exclude_clauses=exclude_clauses, select_related=select_related, limit_count=self.limit_count, offset=self.query_offset, ) + def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003 + return self.filter(_exclude=True, **kwargs) + def select_related(self, related: Union[List, Tuple, str]) -> "QuerySet": if not isinstance(related, (list, tuple)): related = [related] @@ -100,6 +119,7 @@ class QuerySet: return self.__class__( model_cls=self.model_cls, filter_clauses=self.filter_clauses, + exclude_clauses=self.exclude_clauses, select_related=related, limit_count=self.limit_count, offset=self.query_offset, @@ -127,6 +147,7 @@ class QuerySet: return self.__class__( model_cls=self.model_cls, filter_clauses=self.filter_clauses, + exclude_clauses=self.exclude_clauses, select_related=self._select_related, limit_count=limit_count, offset=self.query_offset, @@ -136,6 +157,7 @@ class QuerySet: return self.__class__( model_cls=self.model_cls, filter_clauses=self.filter_clauses, + exclude_clauses=self.exclude_clauses, select_related=self._select_related, limit_count=self.limit_count, offset=offset, @@ -177,6 +199,7 @@ class QuerySet: new_kwargs = dict(**kwargs) new_kwargs = self._remove_pk_from_kwargs(new_kwargs) new_kwargs = self.model_cls.substitute_models_with_pks(new_kwargs) + new_kwargs = self._populate_default_values(new_kwargs) expr = self.table.insert() expr = expr.values(**new_kwargs) diff --git a/tests/test_models.py b/tests/test_models.py index 79381fe..49f8ed3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -165,6 +165,18 @@ async def test_model_filter(): products = await Product.objects.all(name__icontains="T") assert len(products) == 2 + products = await Product.objects.exclude(rating__gte=4).all() + assert len(products) == 1 + + products = await Product.objects.exclude(rating__gte=4, in_stock=True).all() + assert len(products) == 2 + + products = await Product.objects.exclude(in_stock=True).all() + assert len(products) == 1 + + products = await Product.objects.exclude(name__icontains="T").all() + assert len(products) == 1 + # Test escaping % character from icontains, contains, and iexact await Product.objects.create(name="100%-Cotton", rating=3) await Product.objects.create(name="Cotton-100%-Egyptian", rating=3) From d0161a81af5c5e83fc1f964bfea36c180779d1c3 Mon Sep 17 00:00:00 2001 From: collerek Date: Thu, 17 Sep 2020 18:10:10 +0200 Subject: [PATCH 2/2] add callable excecution and test for default value, update readme with exclude, bump version --- .coverage | Bin 53248 -> 53248 bytes README.md | 3 +++ ormar/__init__.py | 2 +- ormar/fields/base.py | 5 ++++- tests/test_models.py | 3 +++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.coverage b/.coverage index b11594792a134a95f3a3d01ad010ea098f58a551..454571fa2dd84c558f39e6c558ca9ddf6d259588 100644 GIT binary patch delta 70 zcmV-M0J;BwpaX!Q1F$+T1UWh}II})4^j#wMZoc{E`+WZVd4A9D+jo9<=Wn}j+cw|k c=g;5#l53^K_Yyt(|egDq~v&D})K>K|r%m4rY diff --git a/README.md b/README.md index ef48fe5..a403d1a 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,9 @@ notes = await Note.objects.filter(completed=True).all() # exact, iexact, contains, icontains, lt, lte, gt, gte, in notes = await Note.objects.filter(text__icontains="mum").all() +# exclude - from ormar >= 0.3.1 +notes = await Note.objects.exclude(text__icontains="mum").all() + # .get() note = await Note.objects.get(id=1) diff --git a/ormar/__init__.py b/ormar/__init__.py index 351f6b1..0403f31 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -26,7 +26,7 @@ class UndefinedType: # pragma no cover Undefined = UndefinedType() -__version__ = "0.3.0" +__version__ = "0.3.1" __all__ = [ "Integer", "BigInteger", diff --git a/ormar/fields/base.py b/ormar/fields/base.py index 18cdbfa..da9a0d8 100644 --- a/ormar/fields/base.py +++ b/ormar/fields/base.py @@ -41,7 +41,10 @@ class BaseField: @classmethod def get_default(cls) -> Any: if cls.has_default(): - return cls.default if cls.default is not None else cls.server_default + default = cls.default if cls.default is not None else cls.server_default + if callable(default): + default = default() + return default @classmethod def has_default(cls) -> bool: diff --git a/tests/test_models.py b/tests/test_models.py index 49f8ed3..9eece9a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,5 @@ import asyncio +from datetime import datetime import databases import pydantic @@ -43,6 +44,7 @@ class Product(ormar.Model): name: ormar.String(max_length=100) rating: ormar.Integer(minimum=1, maximum=5) in_stock: ormar.Boolean(default=False) + last_delivery: ormar.Date(default=datetime.now) @pytest.fixture(scope="module") @@ -158,6 +160,7 @@ async def test_model_filter(): assert product.pk is not None assert product.name == "T-Shirt" assert product.rating == 5 + assert product.last_delivery == datetime.now().date() products = await Product.objects.all(rating__gte=2, in_stock=True) assert len(products) == 2