add default exceptions to relations, test one argument, test querysetproxy, test deeply nested
This commit is contained in:
@ -3,6 +3,9 @@ checks:
|
|||||||
method-complexity:
|
method-complexity:
|
||||||
config:
|
config:
|
||||||
threshold: 8
|
threshold: 8
|
||||||
|
argument-count:
|
||||||
|
config:
|
||||||
|
threshold: 6
|
||||||
file-lines:
|
file-lines:
|
||||||
config:
|
config:
|
||||||
threshold: 500
|
threshold: 500
|
||||||
|
|||||||
10
README.md
10
README.md
@ -437,8 +437,8 @@ metadata.drop_all(engine)
|
|||||||
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
|
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
|
||||||
* `delete(each: bool = False, **kwargs) -> int`
|
* `delete(each: bool = False, **kwargs) -> int`
|
||||||
* `all(**kwargs) -> List[Optional[Model]]`
|
* `all(**kwargs) -> List[Optional[Model]]`
|
||||||
* `filter(**kwargs) -> QuerySet`
|
* `filter(*args, **kwargs) -> QuerySet`
|
||||||
* `exclude(**kwargs) -> QuerySet`
|
* `exclude(*args, **kwargs) -> QuerySet`
|
||||||
* `select_related(related: Union[List, str]) -> QuerySet`
|
* `select_related(related: Union[List, str]) -> QuerySet`
|
||||||
* `prefetch_related(related: Union[List, str]) -> QuerySet`
|
* `prefetch_related(related: Union[List, str]) -> QuerySet`
|
||||||
* `limit(limit_count: int) -> QuerySet`
|
* `limit(limit_count: int) -> QuerySet`
|
||||||
@ -453,7 +453,7 @@ metadata.drop_all(engine)
|
|||||||
#### Relation types
|
#### Relation types
|
||||||
|
|
||||||
* One to many - with `ForeignKey(to: Model)`
|
* One to many - with `ForeignKey(to: Model)`
|
||||||
* Many to many - with `ManyToMany(to: Model, through: Model)`
|
* Many to many - with `ManyToMany(to: Model, Optional[through]: Model)`
|
||||||
|
|
||||||
#### Model fields types
|
#### Model fields types
|
||||||
|
|
||||||
@ -491,8 +491,8 @@ The following keyword arguments are supported on all field types.
|
|||||||
All fields are required unless one of the following is set:
|
All fields are required unless one of the following is set:
|
||||||
|
|
||||||
* `nullable` - Creates a nullable column. Sets the default to `None`.
|
* `nullable` - Creates a nullable column. Sets the default to `None`.
|
||||||
* `default` - Set a default value for the field.
|
* `default` - Set a default value for the field. **Not available for relation fields**
|
||||||
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`).
|
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields**
|
||||||
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.
|
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.
|
||||||
Autoincrement is set by default on int primary keys.
|
Autoincrement is set by default on int primary keys.
|
||||||
* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database.
|
* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database.
|
||||||
|
|||||||
@ -437,8 +437,8 @@ metadata.drop_all(engine)
|
|||||||
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
|
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
|
||||||
* `delete(each: bool = False, **kwargs) -> int`
|
* `delete(each: bool = False, **kwargs) -> int`
|
||||||
* `all(**kwargs) -> List[Optional[Model]]`
|
* `all(**kwargs) -> List[Optional[Model]]`
|
||||||
* `filter(**kwargs) -> QuerySet`
|
* `filter(*args, **kwargs) -> QuerySet`
|
||||||
* `exclude(**kwargs) -> QuerySet`
|
* `exclude(*args, **kwargs) -> QuerySet`
|
||||||
* `select_related(related: Union[List, str]) -> QuerySet`
|
* `select_related(related: Union[List, str]) -> QuerySet`
|
||||||
* `prefetch_related(related: Union[List, str]) -> QuerySet`
|
* `prefetch_related(related: Union[List, str]) -> QuerySet`
|
||||||
* `limit(limit_count: int) -> QuerySet`
|
* `limit(limit_count: int) -> QuerySet`
|
||||||
@ -453,7 +453,7 @@ metadata.drop_all(engine)
|
|||||||
#### Relation types
|
#### Relation types
|
||||||
|
|
||||||
* One to many - with `ForeignKey(to: Model)`
|
* One to many - with `ForeignKey(to: Model)`
|
||||||
* Many to many - with `ManyToMany(to: Model, through: Model)`
|
* Many to many - with `ManyToMany(to: Model, Optional[through]: Model)`
|
||||||
|
|
||||||
#### Model fields types
|
#### Model fields types
|
||||||
|
|
||||||
@ -491,8 +491,8 @@ The following keyword arguments are supported on all field types.
|
|||||||
All fields are required unless one of the following is set:
|
All fields are required unless one of the following is set:
|
||||||
|
|
||||||
* `nullable` - Creates a nullable column. Sets the default to `None`.
|
* `nullable` - Creates a nullable column. Sets the default to `None`.
|
||||||
* `default` - Set a default value for the field.
|
* `default` - Set a default value for the field. **Not available for relation fields**
|
||||||
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`).
|
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields**
|
||||||
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.
|
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.
|
||||||
Autoincrement is set by default on int primary keys.
|
Autoincrement is set by default on int primary keys.
|
||||||
* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database.
|
* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database.
|
||||||
|
|||||||
@ -288,6 +288,37 @@ books = (
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want or need to you can nest deeper conditions as deep as you want, in example to
|
||||||
|
acheive a query like this:
|
||||||
|
|
||||||
|
sql:
|
||||||
|
```
|
||||||
|
WHERE ( ( ( books.year > 1960 OR books.year < 1940 )
|
||||||
|
AND authors.name = 'J.R.R. Tolkien' ) OR
|
||||||
|
( books.year < 2000 AND authors.name = 'Andrzej Sapkowski' ) )
|
||||||
|
```
|
||||||
|
|
||||||
|
You can construct a query as follows:
|
||||||
|
```python
|
||||||
|
books = (
|
||||||
|
await Book.objects.select_related("author")
|
||||||
|
.filter(
|
||||||
|
ormar.or_(
|
||||||
|
ormar.and_(
|
||||||
|
ormar.or_(year__gt=1960, year__lt=1940),
|
||||||
|
author__name="J.R.R. Tolkien",
|
||||||
|
),
|
||||||
|
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
assert len(books) == 3
|
||||||
|
assert books[0].title == "The Hobbit"
|
||||||
|
assert books[1].title == "The Silmarillion"
|
||||||
|
assert books[2].title == "The Witcher"
|
||||||
|
```
|
||||||
|
|
||||||
By now you should already have an idea how `ormar.or_` and `ormar.and_` works.
|
By now you should already have an idea how `ormar.or_` and `ormar.and_` works.
|
||||||
Of course, you could chain them in any other methods of queryset, so in example a perfectly
|
Of course, you could chain them in any other methods of queryset, so in example a perfectly
|
||||||
valid query can look like follows:
|
valid query can look like follows:
|
||||||
@ -310,9 +341,48 @@ assert books[0].title == "The Witcher"
|
|||||||
!!!note
|
!!!note
|
||||||
Note that you cannot provide the same keyword argument several times so queries like `filter(ormar.or_(name='Jack', name='John'))` are not allowed. If you want to check the same
|
Note that you cannot provide the same keyword argument several times so queries like `filter(ormar.or_(name='Jack', name='John'))` are not allowed. If you want to check the same
|
||||||
column for several values simply use `in` operator: `filter(name__in=['Jack','John'])`.
|
column for several values simply use `in` operator: `filter(name__in=['Jack','John'])`.
|
||||||
Note that also that technically you can still do `filter(ormar.or_(name='Jack', name__exact='John'))`
|
|
||||||
but it's not recommended. The different operators can be used as long as they do not
|
If you pass only one parameter to `or_` or `and_` functions it's simply wrapped in parenthesis and
|
||||||
repeat so `filter(ormar.or_(year__lt=1560, year__gt=2000))` is fine.
|
has no effect on actual query, so in the end all 3 queries are identical:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await Book.objects.filter(title='The Hobbit').get()
|
||||||
|
await Book.objects.filter(ormar.or_(title='The Hobbit')).get()
|
||||||
|
await Book.objects.filter(ormar.and_(title='The Hobbit')).get()
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!note
|
||||||
|
Note that `or_` and `and_` queries will have `WHERE (title='The Hobbit')` but the parenthesis is redundant and has no real effect.
|
||||||
|
|
||||||
|
This feature can be used if you **really** need to use the same field name twice.
|
||||||
|
Remember that you cannot pass the same keyword arguments twice to the function, so
|
||||||
|
how you can query in example `WHERE (authors.name LIKE '%tolkien%') OR (authors.name LIKE '%sapkowski%'))`?
|
||||||
|
|
||||||
|
You cannot do:
|
||||||
|
```python
|
||||||
|
books = (
|
||||||
|
await Book.objects.select_related("author")
|
||||||
|
.filter(ormar.or_(
|
||||||
|
author__name__icontains="tolkien",
|
||||||
|
author__name__icontains="sapkowski" # you cannot use same keyword twice in or_!
|
||||||
|
)) # python syntax error
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
But you can do this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
books = (
|
||||||
|
await Book.objects.select_related("author")
|
||||||
|
.filter(ormar.or_(
|
||||||
|
ormar.and_(author__name__icontains="tolkien"), # one argument == just wrapped in ()
|
||||||
|
ormar.and_(author__name__icontains="sapkowski")
|
||||||
|
))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
assert len(books) == 5
|
||||||
|
```
|
||||||
|
|
||||||
## get
|
## get
|
||||||
|
|
||||||
|
|||||||
@ -172,7 +172,7 @@ await post.categories.filter(name="Test category3").update(
|
|||||||
|
|
||||||
### filter
|
### filter
|
||||||
|
|
||||||
`filter(**kwargs) -> QuerySet`
|
`filter(*args, **kwargs) -> QuerySet`
|
||||||
|
|
||||||
Allows you to filter by any Model attribute/field as well as to fetch instances, with a filter across an FK relationship.
|
Allows you to filter by any Model attribute/field as well as to fetch instances, with a filter across an FK relationship.
|
||||||
|
|
||||||
@ -181,7 +181,7 @@ Allows you to filter by any Model attribute/field as well as to fetch instances,
|
|||||||
|
|
||||||
### exclude
|
### exclude
|
||||||
|
|
||||||
`exclude(**kwargs) -> QuerySet`
|
`exclude(*args, **kwargs) -> QuerySet`
|
||||||
|
|
||||||
Works exactly the same as filter and all modifiers (suffixes) are the same, but returns a not condition.
|
Works exactly the same as filter and all modifiers (suffixes) are the same, but returns a not condition.
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,9 @@
|
|||||||
```
|
```
|
||||||
Check the updated docs in Queries -> Filtering and sorting -> Complex filters
|
Check the updated docs in Queries -> Filtering and sorting -> Complex filters
|
||||||
|
|
||||||
|
## Other
|
||||||
|
* Setting default on `ForeignKey` or `ManyToMany` raises and `ModelDefinition` exception as it is (and was) not supported
|
||||||
|
|
||||||
# 0.9.6
|
# 0.9.6
|
||||||
|
|
||||||
##Important
|
##Important
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from pydantic.typing import ForwardRef, evaluate_forwardref
|
|||||||
from sqlalchemy import UniqueConstraint
|
from sqlalchemy import UniqueConstraint
|
||||||
|
|
||||||
import ormar # noqa I101
|
import ormar # noqa I101
|
||||||
from ormar.exceptions import RelationshipInstanceError
|
from ormar.exceptions import ModelDefinitionError, RelationshipInstanceError
|
||||||
from ormar.fields.base import BaseField
|
from ormar.fields.base import BaseField
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
@ -184,6 +184,11 @@ def ForeignKey( # noqa CFQ002
|
|||||||
|
|
||||||
owner = kwargs.pop("owner", None)
|
owner = kwargs.pop("owner", None)
|
||||||
self_reference = kwargs.pop("self_reference", False)
|
self_reference = kwargs.pop("self_reference", False)
|
||||||
|
default = kwargs.pop("default", None)
|
||||||
|
if default is not None:
|
||||||
|
raise ModelDefinitionError(
|
||||||
|
"Argument 'default' is not supported " "on relation fields!"
|
||||||
|
)
|
||||||
|
|
||||||
if to.__class__ == ForwardRef:
|
if to.__class__ == ForwardRef:
|
||||||
__type__ = to if not nullable else Optional[to]
|
__type__ = to if not nullable else Optional[to]
|
||||||
|
|||||||
@ -96,6 +96,12 @@ def ManyToMany(
|
|||||||
if through is not None and through.__class__ != ForwardRef:
|
if through is not None and through.__class__ != ForwardRef:
|
||||||
forbid_through_relations(cast(Type["Model"], through))
|
forbid_through_relations(cast(Type["Model"], through))
|
||||||
|
|
||||||
|
default = kwargs.pop("default", None)
|
||||||
|
if default is not None:
|
||||||
|
raise ModelDefinitionError(
|
||||||
|
"Argument 'default' is not supported " "on relation fields!"
|
||||||
|
)
|
||||||
|
|
||||||
if to.__class__ == ForwardRef:
|
if to.__class__ == ForwardRef:
|
||||||
__type__ = to if not nullable else Optional[to]
|
__type__ = to if not nullable else Optional[to]
|
||||||
column_type = None
|
column_type = None
|
||||||
|
|||||||
@ -374,7 +374,7 @@ class QuerysetProxy:
|
|||||||
model = await self.queryset.get(pk=kwargs[pk_name])
|
model = await self.queryset.get(pk=kwargs[pk_name])
|
||||||
return await model.update(**kwargs)
|
return await model.update(**kwargs)
|
||||||
|
|
||||||
def filter(self, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001
|
def filter(self, *args: Any, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001
|
||||||
"""
|
"""
|
||||||
Allows you to filter by any `Model` attribute/field
|
Allows you to filter by any `Model` attribute/field
|
||||||
as well as to fetch instances, with a filter across an FK relationship.
|
as well as to fetch instances, with a filter across an FK relationship.
|
||||||
@ -404,10 +404,10 @@ class QuerysetProxy:
|
|||||||
:return: filtered QuerysetProxy
|
:return: filtered QuerysetProxy
|
||||||
:rtype: QuerysetProxy
|
:rtype: QuerysetProxy
|
||||||
"""
|
"""
|
||||||
queryset = self.queryset.filter(**kwargs)
|
queryset = self.queryset.filter(*args, **kwargs)
|
||||||
return self.__class__(relation=self.relation, type_=self.type_, qryset=queryset)
|
return self.__class__(relation=self.relation, type_=self.type_, qryset=queryset)
|
||||||
|
|
||||||
def exclude(self, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001
|
def exclude(self, *args: Any, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001
|
||||||
"""
|
"""
|
||||||
Works exactly the same as filter and all modifiers (suffixes) are the same,
|
Works exactly the same as filter and all modifiers (suffixes) are the same,
|
||||||
but returns a *not* condition.
|
but returns a *not* condition.
|
||||||
@ -428,7 +428,7 @@ class QuerysetProxy:
|
|||||||
:return: filtered QuerysetProxy
|
:return: filtered QuerysetProxy
|
||||||
:rtype: QuerysetProxy
|
:rtype: QuerysetProxy
|
||||||
"""
|
"""
|
||||||
queryset = self.queryset.exclude(**kwargs)
|
queryset = self.queryset.exclude(*args, **kwargs)
|
||||||
return self.__class__(relation=self.relation, type_=self.type_, qryset=queryset)
|
return self.__class__(relation=self.relation, type_=self.type_, qryset=queryset)
|
||||||
|
|
||||||
def select_related(self, related: Union[List, str]) -> "QuerysetProxy":
|
def select_related(self, related: Union[List, str]) -> "QuerysetProxy":
|
||||||
|
|||||||
@ -110,6 +110,24 @@ async def test_or_filters():
|
|||||||
assert books[0].title == "The Silmarillion"
|
assert books[0].title == "The Silmarillion"
|
||||||
assert books[1].title == "The Witcher"
|
assert books[1].title == "The Witcher"
|
||||||
|
|
||||||
|
books = (
|
||||||
|
await Book.objects.select_related("author")
|
||||||
|
.filter(
|
||||||
|
ormar.or_(
|
||||||
|
ormar.and_(
|
||||||
|
ormar.or_(year__gt=1960, year__lt=1940),
|
||||||
|
author__name="J.R.R. Tolkien",
|
||||||
|
),
|
||||||
|
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
assert len(books) == 3
|
||||||
|
assert books[0].title == "The Hobbit"
|
||||||
|
assert books[1].title == "The Silmarillion"
|
||||||
|
assert books[2].title == "The Witcher"
|
||||||
|
|
||||||
books = (
|
books = (
|
||||||
await Book.objects.select_related("author")
|
await Book.objects.select_related("author")
|
||||||
.exclude(
|
.exclude(
|
||||||
@ -187,6 +205,38 @@ async def test_or_filters():
|
|||||||
with pytest.raises(QueryDefinitionError):
|
with pytest.raises(QueryDefinitionError):
|
||||||
await Book.objects.select_related("author").filter("wrong").all()
|
await Book.objects.select_related("author").filter("wrong").all()
|
||||||
|
|
||||||
|
books = await tolkien.books.filter(
|
||||||
|
ormar.or_(year__lt=1940, year__gt=1960)
|
||||||
|
).all()
|
||||||
|
assert len(books) == 2
|
||||||
|
|
||||||
|
books = await tolkien.books.filter(
|
||||||
|
ormar.and_(
|
||||||
|
ormar.or_(year__lt=1940, year__gt=1960), title__icontains="hobbit"
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
assert len(books) == 1
|
||||||
|
assert tolkien.books[0].title == "The Hobbit"
|
||||||
|
|
||||||
|
books = (
|
||||||
|
await Book.objects.select_related("author")
|
||||||
|
.filter(ormar.or_(author__name="J.R.R. Tolkien"))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
assert len(books) == 3
|
||||||
|
|
||||||
|
books = (
|
||||||
|
await Book.objects.select_related("author")
|
||||||
|
.filter(
|
||||||
|
ormar.or_(
|
||||||
|
ormar.and_(author__name__icontains="tolkien"),
|
||||||
|
ormar.and_(author__name__icontains="sapkowski"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
assert len(books) == 5
|
||||||
|
|
||||||
|
|
||||||
# TODO: Check / modify
|
# TODO: Check / modify
|
||||||
# process and and or into filter groups (V)
|
# process and and or into filter groups (V)
|
||||||
@ -196,5 +246,4 @@ async def test_or_filters():
|
|||||||
# finish docstrings (V)
|
# finish docstrings (V)
|
||||||
# fix types for FilterAction and FilterGroup (X)
|
# fix types for FilterAction and FilterGroup (X)
|
||||||
# add docs (V)
|
# add docs (V)
|
||||||
|
# fix querysetproxy (V)
|
||||||
# fix querysetproxy
|
|
||||||
|
|||||||
65
tests/test_relations_default_exception.py
Normal file
65
tests/test_relations_default_exception.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# type: ignore
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import databases
|
||||||
|
import pytest
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
import ormar
|
||||||
|
from ormar.exceptions import ModelDefinitionError
|
||||||
|
from tests.settings import DATABASE_URL
|
||||||
|
|
||||||
|
database = databases.Database(DATABASE_URL, force_rollback=True)
|
||||||
|
metadata = sqlalchemy.MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
class Author(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "authors"
|
||||||
|
database = database
|
||||||
|
metadata = metadata
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
first_name: str = ormar.String(max_length=80)
|
||||||
|
last_name: str = ormar.String(max_length=80)
|
||||||
|
|
||||||
|
|
||||||
|
class Category(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "categories"
|
||||||
|
database = database
|
||||||
|
metadata = metadata
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=40)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fk_error():
|
||||||
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
|
class Post(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "posts"
|
||||||
|
database = database
|
||||||
|
metadata = metadata
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
title: str = ormar.String(max_length=200)
|
||||||
|
categories: Optional[List[Category]] = ormar.ManyToMany(Category)
|
||||||
|
author: Optional[Author] = ormar.ForeignKey(Author, default="aa")
|
||||||
|
|
||||||
|
|
||||||
|
def test_m2m_error():
|
||||||
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
|
class Post(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "posts"
|
||||||
|
database = database
|
||||||
|
metadata = metadata
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
title: str = ormar.String(max_length=200)
|
||||||
|
categories: Optional[List[Category]] = ormar.ManyToMany(
|
||||||
|
Category, default="aa"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user