From 5e38426694faf17ec3065f6292c2b7fde99a771c Mon Sep 17 00:00:00 2001 From: collerek Date: Tue, 20 Apr 2021 11:52:41 +0200 Subject: [PATCH] finish release notes, add more test --- docs/releases.md | 9 +++ ormar/models/metaclass.py | 10 ++- ormar/queryset/actions/filter_action.py | 1 + ormar/queryset/clause.py | 37 ++++++----- ormar/queryset/field_accessor.py | 45 +++++++------ ormar/queryset/queryset.py | 15 +++-- ormar/relations/querysetproxy.py | 10 +-- .../test_inheritance_concrete.py | 55 ++++++++-------- .../test_fields_access.py | 43 ++++++------ .../test_pydantic_fields.py | 66 +++++++++++++++++++ tests/test_queries/test_or_filters.py | 28 ++++++++ tests/test_queries/test_order_by.py | 18 +++++ 12 files changed, 242 insertions(+), 95 deletions(-) create mode 100644 tests/test_model_definition/test_pydantic_fields.py diff --git a/docs/releases.md b/docs/releases.md index 50f2315..2fc282d 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -102,6 +102,15 @@ (Product.categories.name << ['Toys', 'Books']) ).get() ``` +* Now you can alos use field access to provide OrderActions to `order_by()` + * Order ascending: + * OLD: `Product.objects.order_by("name").all()` + * NEW: `Product.objects.order_by(Product.name.asc()).all()` + * Order descending: + * OLD: `Product.objects.order_by("-name").all()` + * NEW: `Product.objects.order_by(Product.name.desc()).all()` + * You can of course also combine different models and many order_bys: + `Product.objects.order_by([Product.category.name.asc(), Product.name.desc()]).all()` # 0.10.3 diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 1ed1cfa..43c8822 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -567,8 +567,14 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): field = self.Meta.model_fields.get(item) if field.is_relation: return FieldAccessor( - source_model=self, model=field.to, access_chain=item + source_model=cast(Type["Model"], self), + model=field.to, + access_chain=item, ) else: - return FieldAccessor(source_model=self, field=field, access_chain=item) + return FieldAccessor( + source_model=cast(Type["Model"], self), + field=field, + access_chain=item, + ) return object.__getattribute__(self, item) diff --git a/ormar/queryset/actions/filter_action.py b/ormar/queryset/actions/filter_action.py index 2917063..8b2c7b1 100644 --- a/ormar/queryset/actions/filter_action.py +++ b/ormar/queryset/actions/filter_action.py @@ -176,5 +176,6 @@ class FilterAction(QueryAction): clause_text = clause_text.replace( f"{self.table.name}.{self.column.name}", aliased_name ) + clause_text.replace("%%", "%") # remove doubles in some dialects clause = text(clause_text) return clause diff --git a/ormar/queryset/clause.py b/ormar/queryset/clause.py index 18f2d1f..9f5b35b 100644 --- a/ormar/queryset/clause.py +++ b/ormar/queryset/clause.py @@ -25,10 +25,11 @@ class FilterGroup: """ def __init__( - self, *args: Any, - _filter_type: FilterType = FilterType.AND, - _exclude: bool = False, - **kwargs: Any, + self, + *args: Any, + _filter_type: FilterType = FilterType.AND, + _exclude: bool = False, + **kwargs: Any, ) -> None: self.filter_type = _filter_type self.exclude = _exclude @@ -41,7 +42,7 @@ class FilterGroup: def __and__(self, other: "FilterGroup") -> "FilterGroup": return FilterGroup(self, other) - def __or__(self, other) -> "FilterGroup": + def __or__(self, other: "FilterGroup") -> "FilterGroup": return FilterGroup(self, other, _filter_type=FilterType.OR) def __invert__(self) -> "FilterGroup": @@ -49,10 +50,10 @@ class FilterGroup: return self def resolve( - self, - model_cls: Type["Model"], - select_related: List = None, - filter_clauses: List = None, + self, + model_cls: Type["Model"], + select_related: List = None, + filter_clauses: List = None, ) -> Tuple[List[FilterAction], List[str]]: """ Resolves the FilterGroups actions to use proper target model, replace @@ -123,13 +124,15 @@ class FilterGroup: prefix = " NOT " if self.exclude else "" if self.filter_type == FilterType.AND: clause = sqlalchemy.text( - f"{prefix}( " + str( - sqlalchemy.sql.and_(*self._get_text_clauses())) + " )" + f"{prefix}( " + + str(sqlalchemy.sql.and_(*self._get_text_clauses())) + + " )" ) else: clause = sqlalchemy.text( - f"{prefix}( " + str( - sqlalchemy.sql.or_(*self._get_text_clauses())) + " )" + f"{prefix}( " + + str(sqlalchemy.sql.or_(*self._get_text_clauses())) + + " )" ) return clause @@ -182,7 +185,7 @@ class QueryClause: """ def __init__( - self, model_cls: Type["Model"], filter_clauses: List, select_related: List, + self, model_cls: Type["Model"], filter_clauses: List, select_related: List, ) -> None: self._select_related = select_related[:] @@ -192,7 +195,7 @@ class QueryClause: self.table = self.model_cls.Meta.table def prepare_filter( # noqa: A003 - self, _own_only: bool = False, **kwargs: Any + self, _own_only: bool = False, **kwargs: Any ) -> Tuple[List[FilterAction], List[str]]: """ Main external access point that processes the clauses into sqlalchemy text @@ -217,7 +220,7 @@ class QueryClause: return filter_clauses, select_related def _populate_filter_clauses( - self, _own_only: bool, **kwargs: Any + self, _own_only: bool, **kwargs: Any ) -> Tuple[List[FilterAction], List[str]]: """ Iterates all clauses and extracts used operator and field from related @@ -298,7 +301,7 @@ class QueryClause: return prefixes def _switch_filter_action_prefixes( - self, filter_clauses: List[FilterAction] + self, filter_clauses: List[FilterAction] ) -> List[FilterAction]: """ Substitutes aliases for filter action if the complex key (whole relation str) is diff --git a/ormar/queryset/field_accessor.py b/ormar/queryset/field_accessor.py index f595947..52cc755 100644 --- a/ormar/queryset/field_accessor.py +++ b/ormar/queryset/field_accessor.py @@ -1,20 +1,27 @@ -from typing import Any +from typing import Any, TYPE_CHECKING, Type from ormar.queryset.actions import OrderAction from ormar.queryset.actions.filter_action import METHODS_TO_OPERATORS from ormar.queryset.clause import FilterGroup +if TYPE_CHECKING: # pragma: no cover + from ormar import BaseField, Model + class FieldAccessor: def __init__( - self, source_model=None, field=None, model=None, access_chain: str = "" - ): + self, + source_model: Type["Model"], + field: "BaseField" = None, + model: Type["Model"] = None, + access_chain: str = "", + ) -> None: self._source_model = source_model self._field = field self._model = model self._access_chain = access_chain - def __bool__(self): + def __bool__(self) -> bool: # hack to avoid pydantic name check from parent model return False @@ -22,8 +29,8 @@ class FieldAccessor: if self._field and item == self._field.name: return self._field - if item in self._model.Meta.model_fields: - field = self._model.Meta.model_fields.get(item) + if self._model and item in self._model.Meta.model_fields: + field = self._model.Meta.model_fields[item] if field.is_relation: return FieldAccessor( source_model=self._source_model, @@ -61,43 +68,43 @@ class FieldAccessor: def __le__(self, other: Any) -> FilterGroup: return self._select_operator(op="__le__", other=other) - def __lt__(self, other) -> FilterGroup: + def __lt__(self, other: Any) -> FilterGroup: return self._select_operator(op="__lt__", other=other) - def __mod__(self, other) -> FilterGroup: + def __mod__(self, other: Any) -> FilterGroup: return self._select_operator(op="__mod__", other=other) - def __lshift__(self, other) -> FilterGroup: + def __lshift__(self, other: Any) -> FilterGroup: return self._select_operator(op="in", other=other) - def __rshift__(self, other) -> FilterGroup: + def __rshift__(self, other: Any) -> FilterGroup: return self._select_operator(op="isnull", other=True) - def in_(self, other) -> FilterGroup: + def in_(self, other: Any) -> FilterGroup: return self._select_operator(op="in", other=other) - def iexact(self, other) -> FilterGroup: + def iexact(self, other: Any) -> FilterGroup: return self._select_operator(op="iexact", other=other) - def contains(self, other) -> FilterGroup: + def contains(self, other: Any) -> FilterGroup: return self._select_operator(op="contains", other=other) - def icontains(self, other) -> FilterGroup: + def icontains(self, other: Any) -> FilterGroup: return self._select_operator(op="icontains", other=other) - def startswith(self, other) -> FilterGroup: + def startswith(self, other: Any) -> FilterGroup: return self._select_operator(op="startswith", other=other) - def istartswith(self, other) -> FilterGroup: + def istartswith(self, other: Any) -> FilterGroup: return self._select_operator(op="istartswith", other=other) - def endswith(self, other) -> FilterGroup: + def endswith(self, other: Any) -> FilterGroup: return self._select_operator(op="endswith", other=other) - def iendswith(self, other) -> FilterGroup: + def iendswith(self, other: Any) -> FilterGroup: return self._select_operator(op="iendswith", other=other) - def isnull(self, other) -> FilterGroup: + def isnull(self, other: Any) -> FilterGroup: return self._select_operator(op="isnull", other=other) def asc(self) -> OrderAction: diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index 1eae1f6..0024a7e 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -541,7 +541,8 @@ class QuerySet(Generic[T]): orders_by = [ OrderAction(order_str=x, model_cls=self.model_cls) # type: ignore - if not isinstance(x, OrderAction) else x + if not isinstance(x, OrderAction) + else x for x in columns ] @@ -672,7 +673,7 @@ class QuerySet(Generic[T]): ) return await self.database.execute(expr) - async def delete(self, *args, each: bool = False, **kwargs: Any) -> int: + async def delete(self, *args: Any, each: bool = False, **kwargs: Any) -> int: """ Deletes from the model table after applying the filters from kwargs. @@ -754,7 +755,7 @@ class QuerySet(Generic[T]): limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql return self.rebuild_self(offset=offset, limit_raw_sql=limit_raw_sql,) - async def first(self, *args, **kwargs: Any) -> "T": + async def first(self, *args: Any, **kwargs: Any) -> "T": """ Gets the first row from the db ordered by primary key column ascending. @@ -785,7 +786,7 @@ class QuerySet(Generic[T]): self.check_single_result_rows_count(processed_rows) return processed_rows[0] # type: ignore - async def get_or_none(self, *args, **kwargs: Any) -> Optional["T"]: + async def get_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]: """ Get's the first row from the db meeting the criteria set by kwargs. @@ -805,7 +806,7 @@ class QuerySet(Generic[T]): except ormar.NoMatch: return None - async def get(self, *args, **kwargs: Any) -> "T": + async def get(self, *args: Any, **kwargs: Any) -> "T": """ Get's the first row from the db meeting the criteria set by kwargs. @@ -844,7 +845,7 @@ class QuerySet(Generic[T]): self.check_single_result_rows_count(processed_rows) return processed_rows[0] # type: ignore - async def get_or_create(self, *args, **kwargs: Any) -> "T": + async def get_or_create(self, *args: Any, **kwargs: Any) -> "T": """ Combination of create and get methods. @@ -879,7 +880,7 @@ class QuerySet(Generic[T]): model = await self.get(pk=kwargs[pk_name]) return await model.update(**kwargs) - async def all(self, *args, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003 + async def all(self, *args: Any, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003 """ Returns all rows from a database for given model for set filter options. diff --git a/ormar/relations/querysetproxy.py b/ormar/relations/querysetproxy.py index 6014e29..c6ae52d 100644 --- a/ormar/relations/querysetproxy.py +++ b/ormar/relations/querysetproxy.py @@ -276,7 +276,7 @@ class QuerysetProxy(Generic[T]): ) return await queryset.delete(**kwargs) # type: ignore - async def first(self, *args, **kwargs: Any) -> "T": + async def first(self, *args: Any, **kwargs: Any) -> "T": """ Gets the first row from the db ordered by primary key column ascending. @@ -294,7 +294,7 @@ class QuerysetProxy(Generic[T]): self._register_related(first) return first - async def get_or_none(self, *args, **kwargs: Any) -> Optional["T"]: + async def get_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]: """ Get's the first row from the db meeting the criteria set by kwargs. @@ -318,7 +318,7 @@ class QuerysetProxy(Generic[T]): self._register_related(get) return get - async def get(self, *args, **kwargs: Any) -> "T": + async def get(self, *args: Any, **kwargs: Any) -> "T": """ Get's the first row from the db meeting the criteria set by kwargs. @@ -342,7 +342,7 @@ class QuerysetProxy(Generic[T]): self._register_related(get) return get - async def all(self, *args, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003 + async def all(self, *args: Any, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003 """ Returns all rows from a database for given model for set filter options. @@ -425,7 +425,7 @@ class QuerysetProxy(Generic[T]): ) return len(children) - async def get_or_create(self, *args, **kwargs: Any) -> "T": + async def get_or_create(self, *args: Any, **kwargs: Any) -> "T": """ Combination of create and get methods. diff --git a/tests/test_inheritance/test_inheritance_concrete.py b/tests/test_inheritance/test_inheritance_concrete.py index 4bbdf5b..ac059e4 100644 --- a/tests/test_inheritance/test_inheritance_concrete.py +++ b/tests/test_inheritance/test_inheritance_concrete.py @@ -173,6 +173,7 @@ def test_init_of_abstract_model(): def test_duplicated_related_name_on_different_model(): with pytest.raises(ModelDefinitionError): + class Bus3(Car2): # pragma: no cover class Meta: tablename = "buses3" @@ -202,6 +203,7 @@ def test_field_redefining_in_concrete_models(): def test_model_subclassing_that_redefines_constraints_column_names(): with pytest.raises(ModelDefinitionError): + class WrongField2(DateFieldsModel): # pragma: no cover class Meta(ormar.ModelMeta): tablename = "wrongs" @@ -214,6 +216,7 @@ def test_model_subclassing_that_redefines_constraints_column_names(): def test_model_subclassing_non_abstract_raises_error(): with pytest.raises(ModelDefinitionError): + class WrongField2(DateFieldsModelNoSubclass): # pragma: no cover class Meta(ormar.ModelMeta): tablename = "wrongs" @@ -231,7 +234,7 @@ def test_params_are_inherited(): def round_date_to_seconds( - date: datetime.datetime, + date: datetime.datetime, ) -> datetime.datetime: # pragma: no cover if date.microsecond >= 500000: date = date + datetime.timedelta(seconds=1) @@ -274,9 +277,9 @@ async def test_fields_inherited_from_mixin(): sub2 = ( await Subject.objects.select_related("category") - .order_by("-created_date") - .exclude_fields("updated_date") - .get() + .order_by("-created_date") + .exclude_fields("updated_date") + .get() ) assert round_date_to_seconds(sub2.created_date) == round_date_to_seconds( sub.created_date @@ -291,9 +294,9 @@ async def test_fields_inherited_from_mixin(): sub3 = ( await Subject.objects.prefetch_related("category") - .order_by("-created_date") - .exclude_fields({"updated_date": ..., "category": {"updated_date"}}) - .get() + .order_by("-created_date") + .exclude_fields({"updated_date": ..., "category": {"updated_date"}}) + .get() ) assert round_date_to_seconds(sub3.created_date) == round_date_to_seconds( sub.created_date @@ -346,8 +349,8 @@ async def test_inheritance_with_relation(): "coowned_buses": {"created_date"}, } ) - .prefetch_related(["coowned_trucks", "coowned_buses"]) - .get(name="Joe") + .prefetch_related(["coowned_trucks", "coowned_buses"]) + .get(name="Joe") ) assert joe_check.pk == joe.pk assert joe_check.coowned_trucks[0] == shelby @@ -394,8 +397,8 @@ async def test_inheritance_with_multi_relation(): unicorn = ( await Bus2.objects.select_related(["owner", "co_owners"]) - .order_by("-co_owners__name") - .get() + .order_by("-co_owners__name") + .get() ) assert unicorn.name == "Unicorn 2" assert unicorn.owner.name == "Sam" @@ -404,8 +407,8 @@ async def test_inheritance_with_multi_relation(): unicorn = ( await Bus2.objects.select_related(["owner", "co_owners"]) - .order_by("co_owners__name") - .get() + .order_by("co_owners__name") + .get() ) assert unicorn.name == "Unicorn 2" assert unicorn.owner.name == "Sam" @@ -428,8 +431,8 @@ async def test_inheritance_with_multi_relation(): "coowned_buses2": {"created_date"}, } ) - .prefetch_related(["coowned_trucks2", "coowned_buses2"]) - .get(name="Joe") + .prefetch_related(["coowned_trucks2", "coowned_buses2"]) + .get(name="Joe") ) assert joe_check.pk == joe.pk assert joe_check.coowned_trucks2[0] == shelby @@ -443,8 +446,8 @@ async def test_inheritance_with_multi_relation(): unicorn = ( await Bus2.objects.select_related(["owner", "co_owners"]) - .filter(co_owners__name="Joe") - .get() + .filter(co_owners__name="Joe") + .get() ) assert unicorn.name == "Unicorn 2" assert unicorn.owner.name == "Sam" @@ -454,8 +457,8 @@ async def test_inheritance_with_multi_relation(): unicorn = ( await Bus2.objects.select_related(["owner", "co_owners"]) - .exclude(co_owners__name="Joe") - .get() + .exclude(co_owners__name="Joe") + .get() ) assert unicorn.name == "Unicorn 2" assert unicorn.owner.name == "Sam" @@ -477,9 +480,9 @@ async def test_inheritance_with_multi_relation(): unicorns = ( await Bus2.objects.select_related(["owner", "co_owners"]) - .filter(name__contains="Unicorn") - .order_by("-name") - .all() + .filter(name__contains="Unicorn") + .order_by("-name") + .all() ) assert unicorns[0].name == "Unicorn 3" assert unicorns[0].owner.name == "Joe" @@ -493,10 +496,10 @@ async def test_inheritance_with_multi_relation(): unicorns = ( await Bus2.objects.select_related(["owner", "co_owners"]) - .filter(name__contains="Unicorn") - .order_by("-name") - .limit(2, limit_raw_sql=True) - .all() + .filter(name__contains="Unicorn") + .order_by("-name") + .limit(2, limit_raw_sql=True) + .all() ) assert len(unicorns) == 2 assert unicorns[1].name == "Unicorn 2" diff --git a/tests/test_model_definition/test_fields_access.py b/tests/test_model_definition/test_fields_access.py index 8298242..3c98cdc 100644 --- a/tests/test_model_definition/test_fields_access.py +++ b/tests/test_model_definition/test_fields_access.py @@ -112,11 +112,11 @@ def test_operator_return_proper_filter_action(method, expected, expected_value): group_ = getattr(PriceList.categories.products.rating, method)("Test") assert group_._kwargs_dict == { - f"categories__products__rating__{expected}": expected_value} + f"categories__products__rating__{expected}": expected_value + } -@pytest.mark.parametrize("method, expected_direction", - [("asc", ""), ("desc", "desc"), ]) +@pytest.mark.parametrize("method, expected_direction", [("asc", ""), ("desc", "desc"),]) def test_operator_return_proper_order_action(method, expected_direction): action = getattr(Product.name, method)() assert action.source_model == Product @@ -141,17 +141,20 @@ def test_combining_groups_together(): group = (Product.name == "Test") & (Product.rating >= 3.0) group.resolve(model_cls=Product) assert len(group._nested_groups) == 2 - assert str(group.get_text_clause()) == ("( ( product.name = 'Test' ) AND" - " ( product.rating >= 3.0 ) )") + assert str(group.get_text_clause()) == ( + "( ( product.name = 'Test' ) AND" " ( product.rating >= 3.0 ) )" + ) group = ~((Product.name == "Test") & (Product.rating >= 3.0)) group.resolve(model_cls=Product) assert len(group._nested_groups) == 2 - assert str(group.get_text_clause()) == (" NOT ( ( product.name = 'Test' ) AND" - " ( product.rating >= 3.0 ) )") + assert str(group.get_text_clause()) == ( + " NOT ( ( product.name = 'Test' ) AND" " ( product.rating >= 3.0 ) )" + ) group = ((Product.name == "Test") & (Product.rating >= 3.0)) | ( - Product.category.name << (["Toys", "Books"])) + Product.category.name << (["Toys", "Books"]) + ) group.resolve(model_cls=Product) assert len(group._nested_groups) == 2 assert len(group._nested_groups[0]._nested_groups) == 2 @@ -159,22 +162,27 @@ def test_combining_groups_together(): category_prefix = group._nested_groups[1].actions[0].table_prefix assert group_str == ( "( ( ( product.name = 'Test' ) AND ( product.rating >= 3.0 ) ) " - f"OR ( {category_prefix}_categories.name IN ('Toys', 'Books') ) )") + f"OR ( {category_prefix}_categories.name IN ('Toys', 'Books') ) )" + ) group = (Product.name % "Test") | ( - (Product.category.price_lists.name.startswith("Aa")) | ( - Product.category.name << (["Toys", "Books"]))) + (Product.category.price_lists.name.startswith("Aa")) + | (Product.category.name << (["Toys", "Books"])) + ) group.resolve(model_cls=Product) assert len(group._nested_groups) == 2 assert len(group._nested_groups[1]._nested_groups) == 2 group_str = str(group.get_text_clause()) - price_list_prefix = group._nested_groups[1]._nested_groups[0].actions[ - 0].table_prefix + price_list_prefix = ( + group._nested_groups[1]._nested_groups[0].actions[0].table_prefix + ) category_prefix = group._nested_groups[1]._nested_groups[1].actions[0].table_prefix assert group_str == ( f"( ( product.name LIKE '%Test%' ) " f"OR ( ( {price_list_prefix}_price_lists.name LIKE 'Aa%' ) " - f"OR ( {category_prefix}_categories.name IN ('Toys', 'Books') ) ) )") + f"OR ( {category_prefix}_categories.name IN ('Toys', 'Books') ) ) )" + ) + # @pytest.mark.asyncio # async def test_filtering_by_field_access(): @@ -195,9 +203,6 @@ def test_combining_groups_together(): # * overload operators and add missing functions that return FilterAction (V) # * return OrderAction for desc() and asc() (V) # * create filter groups for & and | (and ~ - NOT?) (V) - -# * accept args in all functions that accept filters? or only filter and exclude? +# * accept args in all functions that accept filters? or only filter and exclude? (V) # all functions: delete, first, get, get_or_none, get_or_create, all, filter, exclude -# and same from queryset, should they also accept filter groups? -# * accept OrderActions in order_by -# +# * accept OrderActions in order_by (V) diff --git a/tests/test_model_definition/test_pydantic_fields.py b/tests/test_model_definition/test_pydantic_fields.py new file mode 100644 index 0000000..d41669a --- /dev/null +++ b/tests/test_model_definition/test_pydantic_fields.py @@ -0,0 +1,66 @@ +from typing import Optional + +import databases +import pytest +import sqlalchemy +from pydantic import EmailStr, HttpUrl, ValidationError + +import ormar +from tests.settings import DATABASE_URL + +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() + + +class BaseMeta(ormar.ModelMeta): + metadata = metadata + database = database + + +class Test(ormar.Model): + class Meta(BaseMeta): + pass + + def __init__(self, **kwargs): + # you need to pop non - db fields as ormar will complain that it's unknown field + email = kwargs.pop("email", self.__fields__["email"].get_default()) + url = kwargs.pop("url", self.__fields__["url"].get_default()) + super().__init__(**kwargs) + self.email = email + self.url = url + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=200) + email: Optional[EmailStr] # field optional - default to None + url: HttpUrl = "www.example.com" # field with default + + +@pytest.fixture(autouse=True, scope="module") +def create_test_database(): + engine = sqlalchemy.create_engine(DATABASE_URL) + metadata.drop_all(engine) + metadata.create_all(engine) + yield + metadata.drop_all(engine) + + +@pytest.mark.asyncio +async def test_working_with_pydantic_fields(): + async with database: + test = Test(name="Test", email="aka@go2.com") + assert test.name == "Test" + assert test.email == "aka@go2.com" + assert test.url == "www.example.com" + + test.email = "sdta@ada.pt" + assert test.email == "sdta@ada.pt" + + await test.save() + test_check = await Test.objects.get() + + assert test_check.name == "Test" + assert test_check.email is None + assert test_check.url == "www.example.com" + + # TODO add validate assignment to pydantic config + # test_check.email = 1 diff --git a/tests/test_queries/test_or_filters.py b/tests/test_queries/test_or_filters.py index 81a412c..f419d15 100644 --- a/tests/test_queries/test_or_filters.py +++ b/tests/test_queries/test_or_filters.py @@ -70,6 +70,14 @@ async def test_or_filters(): assert len(books) == 4 assert not any([x.title == "The Tower of Fools" for x in books]) + books = ( + await Book.objects.select_related("author") + .filter((Book.author.name == "J.R.R. Tolkien") | (Book.year < 1995)) + .all() + ) + assert len(books) == 4 + assert not any([x.title == "The Tower of Fools" for x in books]) + books = ( await Book.objects.select_related("author") .filter(ormar.or_(year__gt=1960, year__lt=1940)) @@ -110,6 +118,26 @@ async def test_or_filters(): assert books[0].title == "The Silmarillion" assert books[1].title == "The Witcher" + books = ( + await Book.objects.select_related("author") + .filter( + ( + ( + (Book.year > 1960) & (Book.author.name == "J.R.R. Tolkien") + | ( + (Book.year < 2000) + & (Book.author.name == "Andrzej Sapkowski") + ) + ) + & (Book.title.startswith("The")) + ), + ) + .all() + ) + assert len(books) == 2 + assert books[0].title == "The Silmarillion" + assert books[1].title == "The Witcher" + books = ( await Book.objects.select_related("author") .filter( diff --git a/tests/test_queries/test_order_by.py b/tests/test_queries/test_order_by.py index 7c2f10d..d32cf68 100644 --- a/tests/test_queries/test_order_by.py +++ b/tests/test_queries/test_order_by.py @@ -122,11 +122,21 @@ async def test_sort_order_on_main_model(): assert songs[1].name == "Song 2" assert songs[2].name == "Song 1" + songs = await Song.objects.order_by(Song.sort_order.desc()).all() + assert songs[0].name == "Song 3" + assert songs[1].name == "Song 2" + assert songs[2].name == "Song 1" + songs = await Song.objects.order_by("sort_order").all() assert songs[0].name == "Song 1" assert songs[1].name == "Song 2" assert songs[2].name == "Song 3" + songs = await Song.objects.order_by(Song.sort_order.asc()).all() + assert songs[0].name == "Song 1" + assert songs[1].name == "Song 2" + assert songs[2].name == "Song 3" + songs = await Song.objects.order_by("name").all() assert songs[0].name == "Song 1" assert songs[1].name == "Song 2" @@ -145,6 +155,14 @@ async def test_sort_order_on_main_model(): assert songs[2].name == "Song 2" assert songs[3].name == "Song 3" + songs = await Song.objects.order_by( + [Song.sort_order.asc(), Song.name.asc()] + ).all() + assert songs[0].name == "Song 1" + assert songs[1].name == "Song 4" + assert songs[2].name == "Song 2" + assert songs[3].name == "Song 3" + @pytest.mark.asyncio async def test_sort_order_on_related_model():