* WIP * WIP - make test_model_definition tests pass * WIP - make test_model_methods pass * WIP - make whole test suit at least run - failing 49/443 tests * WIP fix part of the getting pydantic tests as types of fields are now kept in core schema and not on fieldsinfo * WIP fix validation in update by creating individual fields validators, failing 36/443 * WIP fix __pydantic_extra__ in intializing model, fix test related to pydantic config checks, failing 32/442 * WIP - fix enum schema in model_json_schema, failing 31/442 * WIP - fix copying through model, fix setting pydantic fields on through, fix default config and inheriting from it, failing 26/442 * WIP fix tests checking pydantic schema, fix excluding parent fields, failing 21/442 * WIP some missed files * WIP - fix validators inheritance and fix validators in generated pydantic, failing 17/442 * WIP - fix through models setting - only on reverse side of relation, but always on reverse side, failing 15/442 * WIP - fix through models setting - only on reverse side of relation, but always on reverse side, failing 15/442 * WIP - working on proper populating __dict__ for relations for new schema dumping, some work on openapi docs, failing 13/442 * WIP - remove property fields as pydantic has now computed_field on its own, failing 9/442 * WIP - fixes in docs, failing 8/442 * WIP - fix tests for largebinary schema, wrapped bytes fields fail in pydantic, will be fixed in pydantic-core, remaining is circural schema for related models, failing 6/442 * WIP - fix to pk only models in schemas * Getting test suites to pass (#1249) * wip, fixing tests * iteration, fixing some more tests * iteration, fixing some more tests * adhere to comments * adhere to comments * remove unnecessary dict call, re-add getattribute for testing * todo for reverse relationship * adhere to comments, remove prints * solve circular refs * all tests pass 🎉 * remove 3.7 from tests * add lint and type check jobs * reforat with ruff, fix jobs * rename jobs * fix imports * fix evaluate in py3.8 * partially fix coverage * fix coverage, add more tests * fix test ids * fix test ids * fix lint, fix docs, make docs fully working scripts, add test docs job * fix pyproject * pin py ver in test docs * change dir in test docs * fix pydantic warning hack * rm poetry call in test_docs * switch to pathlib in test docs * remove coverage req test docs * fix type check tests, fix part of types * fix/skip next part of types * fix next part of types * fix next part of types * fix coverage * fix coverage * fix type (bit dirty 🤷) * fix some code smells * change pre-commit * tweak workflows * remove no root from tests * switch to full python path by passing sys.executable * some small refactor in new base model, one sample test, change makefile * small refactors to reduce complexity of methods * temp add tests for prs against pydantic_v2 * remove all references to __fields__ * remove all references to construct, deprecate the method and update model_construct to be in line with pydantic * deprecate dict and add model_dump, todo switch to model_dict in calls * fix tests * change to union * change to union * change to model_dump and model_dump_json from dict and json deprecated methods, deprecate them in ormar too * finish switching dict() -> model_dump() * finish switching json() -> model_dump_json() * remove fully pydantic_only * switch to extra for payment card, change missed json calls * fix coverage - no more warnings internal * fix coverage - no more warnings internal - part 2 * split model_construct into own and pydantic parts * split determine pydantic field type * change to new field validators * fix benchmarks, add codspeed instead of pytest-benchmark, add action and gh workflow * restore pytest-benchmark * remove codspeed * pin pydantic version, restore codspeed * change on push to pydantic_v2 to trigger first one * Use lifespan function instead of event (#1259) * check return types * fix imports order, set warnings=False on json that passes the dict, fix unnecessary loop in one of the test * remove references to model's meta as it's now ormar config, rename related methods too * filter out pydantic serializer warnings * remove choices leftovers * remove leftovers after property_fields, keep only enough to exclude them in initialization * add migration guide * fix meta references * downgrade databases for now * Change line numbers in documentation (#1265) * proofread and fix the docs, part 1 * proofread and fix the docs for models * proofread and fix the docs for fields * proofread and fix the docs for relations * proofread and fix rest of the docs, add release notes for 0.20 * create tables in new docs src * cleanup old deps, uncomment docs publish on tag * fix import reorder --------- Co-authored-by: TouwaStar <30479449+TouwaStar@users.noreply.github.com> Co-authored-by: Goran Mekić <meka@tilda.center>
472 lines
15 KiB
Python
472 lines
15 KiB
Python
from enum import Enum
|
|
from typing import Optional
|
|
|
|
import ormar
|
|
import pydantic
|
|
import pytest
|
|
from ormar import QuerySet
|
|
from ormar.exceptions import (
|
|
ModelListEmptyError,
|
|
ModelPersistenceError,
|
|
QueryDefinitionError,
|
|
)
|
|
from pydantic import Json
|
|
|
|
from tests.lifespan import init_tests
|
|
from tests.settings import create_config
|
|
|
|
base_ormar_config = create_config(force_rollback=True)
|
|
|
|
|
|
class MySize(Enum):
|
|
SMALL = 0
|
|
BIG = 1
|
|
|
|
|
|
class Book(ormar.Model):
|
|
ormar_config = base_ormar_config.copy(tablename="books")
|
|
|
|
id: int = ormar.Integer(primary_key=True)
|
|
title: str = ormar.String(max_length=200)
|
|
author: str = ormar.String(max_length=100)
|
|
genre: str = ormar.String(
|
|
max_length=100,
|
|
default="Fiction",
|
|
)
|
|
|
|
|
|
class ToDo(ormar.Model):
|
|
ormar_config = base_ormar_config.copy(tablename="todos")
|
|
|
|
id: int = ormar.Integer(primary_key=True)
|
|
text: str = ormar.String(max_length=500)
|
|
completed: bool = ormar.Boolean(default=False)
|
|
pairs: pydantic.Json = ormar.JSON(default=[])
|
|
size = ormar.Enum(enum_class=MySize, default=MySize.SMALL)
|
|
|
|
|
|
class Category(ormar.Model):
|
|
ormar_config = base_ormar_config.copy(tablename="categories")
|
|
|
|
id: int = ormar.Integer(primary_key=True)
|
|
name: str = ormar.String(max_length=500)
|
|
|
|
|
|
class Note(ormar.Model):
|
|
ormar_config = base_ormar_config.copy(tablename="notes")
|
|
|
|
id: int = ormar.Integer(primary_key=True)
|
|
text: str = ormar.String(max_length=500)
|
|
category: Optional[Category] = ormar.ForeignKey(Category)
|
|
|
|
|
|
class ItemConfig(ormar.Model):
|
|
ormar_config = base_ormar_config.copy(tablename="item_config")
|
|
|
|
id: Optional[int] = ormar.Integer(primary_key=True)
|
|
item_id: str = ormar.String(max_length=32, index=True)
|
|
pairs: pydantic.Json = ormar.JSON(default=["2", "3"])
|
|
size = ormar.Enum(enum_class=MySize, default=MySize.SMALL)
|
|
|
|
|
|
class QuerySetCls(QuerySet):
|
|
async def first_or_404(self, *args, **kwargs):
|
|
entity = await self.get_or_none(*args, **kwargs)
|
|
if not entity:
|
|
# maybe HTTPException in fastapi
|
|
raise ValueError("customer not found")
|
|
return entity
|
|
|
|
|
|
class Customer(ormar.Model):
|
|
ormar_config = base_ormar_config.copy(
|
|
tablename="customer",
|
|
queryset_class=QuerySetCls,
|
|
)
|
|
|
|
id: Optional[int] = ormar.Integer(primary_key=True)
|
|
name: str = ormar.String(max_length=32)
|
|
|
|
|
|
class JsonTestModel(ormar.Model):
|
|
ormar_config = base_ormar_config.copy(tablename="test_model")
|
|
|
|
id: int = ormar.Integer(primary_key=True)
|
|
json_field: Json = ormar.JSON()
|
|
|
|
|
|
create_test_database = init_tests(base_ormar_config)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_and_update():
|
|
async with base_ormar_config.database:
|
|
async with base_ormar_config.database.transaction(force_rollback=True):
|
|
await Book.objects.create(
|
|
title="Tom Sawyer", author="Twain, Mark", genre="Adventure"
|
|
)
|
|
await Book.objects.create(
|
|
title="War and Peace", author="Tolstoy, Leo", genre="Fiction"
|
|
)
|
|
await Book.objects.create(
|
|
title="Anna Karenina", author="Tolstoy, Leo", genre="Fiction"
|
|
)
|
|
await Book.objects.create(
|
|
title="Harry Potter", author="Rowling, J.K.", genre="Fantasy"
|
|
)
|
|
await Book.objects.create(
|
|
title="Lord of the Rings", author="Tolkien, J.R.", genre="Fantasy"
|
|
)
|
|
|
|
all_books = await Book.objects.all()
|
|
assert len(all_books) == 5
|
|
|
|
await Book.objects.filter(author="Tolstoy, Leo").update(
|
|
author="Lenin, Vladimir"
|
|
)
|
|
all_books = await Book.objects.filter(author="Lenin, Vladimir").all()
|
|
assert len(all_books) == 2
|
|
|
|
historic_books = await Book.objects.filter(genre="Historic").all()
|
|
assert len(historic_books) == 0
|
|
|
|
with pytest.raises(QueryDefinitionError):
|
|
await Book.objects.update(genre="Historic")
|
|
|
|
await Book.objects.filter(author="Lenin, Vladimir").update(genre="Historic")
|
|
|
|
historic_books = await Book.objects.filter(genre="Historic").all()
|
|
assert len(historic_books) == 2
|
|
|
|
await Book.objects.delete(genre="Fantasy")
|
|
all_books = await Book.objects.all()
|
|
assert len(all_books) == 3
|
|
|
|
await Book.objects.update(each=True, genre="Fiction")
|
|
all_books = await Book.objects.filter(genre="Fiction").all()
|
|
assert len(all_books) == 3
|
|
|
|
with pytest.raises(QueryDefinitionError):
|
|
await Book.objects.delete()
|
|
|
|
await Book.objects.delete(each=True)
|
|
all_books = await Book.objects.all()
|
|
assert len(all_books) == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create():
|
|
async with base_ormar_config.database:
|
|
tom, created = await Book.objects.get_or_create(
|
|
title="Volume I", author="Anonymous", genre="Fiction"
|
|
)
|
|
assert await Book.objects.count() == 1
|
|
assert created is True
|
|
|
|
second_tom, created = await Book.objects.get_or_create(
|
|
title="Volume I", author="Anonymous", genre="Fiction"
|
|
)
|
|
|
|
assert second_tom.pk == tom.pk
|
|
assert created is False
|
|
assert await Book.objects.count() == 1
|
|
|
|
assert await Book.objects.create(
|
|
title="Volume I", author="Anonymous", genre="Fiction"
|
|
)
|
|
with pytest.raises(ormar.exceptions.MultipleMatches):
|
|
await Book.objects.get_or_create(
|
|
title="Volume I", author="Anonymous", genre="Fiction"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create_with_defaults():
|
|
async with base_ormar_config.database:
|
|
book, created = await Book.objects.get_or_create(
|
|
title="Nice book", _defaults={"author": "Mojix", "genre": "Historic"}
|
|
)
|
|
assert created is True
|
|
assert book.author == "Mojix"
|
|
assert book.title == "Nice book"
|
|
assert book.genre == "Historic"
|
|
|
|
book2, created = await Book.objects.get_or_create(
|
|
author="Mojix", _defaults={"title": "Book2"}
|
|
)
|
|
assert created is False
|
|
assert book2 == book
|
|
assert book2.title == "Nice book"
|
|
assert book2.author == "Mojix"
|
|
assert book2.genre == "Historic"
|
|
assert await Book.objects.count() == 1
|
|
|
|
book, created = await Book.objects.get_or_create(
|
|
title="doesn't exist",
|
|
_defaults={"title": "overwritten", "author": "Mojix", "genre": "Historic"},
|
|
)
|
|
assert created is True
|
|
assert book.title == "overwritten"
|
|
|
|
book2, created = await Book.objects.get_or_create(
|
|
title="overwritten", _defaults={"title": "doesn't work"}
|
|
)
|
|
assert created is False
|
|
assert book2.title == "overwritten"
|
|
assert book2 == book
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_or_create():
|
|
async with base_ormar_config.database:
|
|
tom = await Book.objects.update_or_create(
|
|
title="Volume I", author="Anonymous", genre="Fiction"
|
|
)
|
|
assert await Book.objects.count() == 1
|
|
|
|
assert await Book.objects.update_or_create(id=tom.id, genre="Historic")
|
|
assert await Book.objects.count() == 1
|
|
|
|
assert await Book.objects.update_or_create(pk=tom.id, genre="Fantasy")
|
|
assert await Book.objects.count() == 1
|
|
|
|
assert await Book.objects.create(
|
|
title="Volume I", author="Anonymous", genre="Fantasy"
|
|
)
|
|
with pytest.raises(ormar.exceptions.MultipleMatches):
|
|
await Book.objects.get(
|
|
title="Volume I", author="Anonymous", genre="Fantasy"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bulk_create():
|
|
async with base_ormar_config.database:
|
|
await ToDo.objects.bulk_create(
|
|
[
|
|
ToDo(text="Buy the groceries."),
|
|
ToDo(text="Call Mum.", completed=True),
|
|
ToDo(text="Send invoices.", completed=True),
|
|
]
|
|
)
|
|
|
|
todoes = await ToDo.objects.all()
|
|
assert len(todoes) == 3
|
|
for todo in todoes:
|
|
assert todo.pk is not None
|
|
|
|
completed = await ToDo.objects.filter(completed=True).all()
|
|
assert len(completed) == 2
|
|
|
|
with pytest.raises(ormar.exceptions.ModelListEmptyError):
|
|
await ToDo.objects.bulk_create([])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bulk_create_json_field():
|
|
async with base_ormar_config.database:
|
|
json_value = {"a": 1}
|
|
test_model_1 = JsonTestModel(id=1, json_field=json_value)
|
|
test_model_2 = JsonTestModel(id=2, json_field=json_value)
|
|
|
|
# store one with .save() and the other with .bulk_create()
|
|
await test_model_1.save()
|
|
await JsonTestModel.objects.bulk_create([test_model_2])
|
|
|
|
# refresh from the database
|
|
await test_model_1.load()
|
|
await test_model_2.load()
|
|
|
|
assert test_model_1.json_field == test_model_2.json_field # True
|
|
|
|
# try to query the json field
|
|
table = JsonTestModel.ormar_config.table
|
|
query = table.select().where(table.c.json_field["a"].as_integer() == 1)
|
|
res = [
|
|
JsonTestModel.from_row(record, source_model=JsonTestModel)
|
|
for record in await base_ormar_config.database.fetch_all(query)
|
|
]
|
|
|
|
assert test_model_1 in res
|
|
assert test_model_2 in res
|
|
assert len(res) == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bulk_create_with_relation():
|
|
async with base_ormar_config.database:
|
|
category = await Category.objects.create(name="Sample Category")
|
|
|
|
await Note.objects.bulk_create(
|
|
[
|
|
Note(text="Buy the groceries.", category=category),
|
|
Note(text="Call Mum.", category=category),
|
|
]
|
|
)
|
|
|
|
todoes = await Note.objects.all()
|
|
assert len(todoes) == 2
|
|
for todo in todoes:
|
|
assert todo.category.pk == category.pk
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bulk_update():
|
|
async with base_ormar_config.database:
|
|
await ToDo.objects.bulk_create(
|
|
[
|
|
ToDo(text="Buy the groceries."),
|
|
ToDo(text="Call Mum.", completed=True),
|
|
ToDo(text="Send invoices.", completed=True),
|
|
]
|
|
)
|
|
todoes = await ToDo.objects.all()
|
|
assert len(todoes) == 3
|
|
|
|
for todo in todoes:
|
|
todo.text = todo.text + "_1"
|
|
todo.completed = False
|
|
todo.size = MySize.BIG
|
|
|
|
await ToDo.objects.bulk_update(todoes)
|
|
|
|
completed = await ToDo.objects.filter(completed=False).all()
|
|
assert len(completed) == 3
|
|
|
|
todoes = await ToDo.objects.all()
|
|
assert len(todoes) == 3
|
|
|
|
for todo in todoes:
|
|
assert todo.text[-2:] == "_1"
|
|
assert todo.size == MySize.BIG
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bulk_update_with_only_selected_columns():
|
|
async with base_ormar_config.database:
|
|
await ToDo.objects.bulk_create(
|
|
[
|
|
ToDo(text="Reset the world simulation.", completed=False),
|
|
ToDo(text="Watch kittens.", completed=True),
|
|
]
|
|
)
|
|
|
|
todoes = await ToDo.objects.all()
|
|
assert len(todoes) == 2
|
|
|
|
for todo in todoes:
|
|
todo.text = todo.text + "_1"
|
|
todo.completed = False
|
|
|
|
await ToDo.objects.bulk_update(todoes, columns=["completed"])
|
|
|
|
completed = await ToDo.objects.filter(completed=False).all()
|
|
assert len(completed) == 2
|
|
|
|
todoes = await ToDo.objects.all()
|
|
assert len(todoes) == 2
|
|
|
|
for todo in todoes:
|
|
assert todo.text[-2:] != "_1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bulk_update_with_relation():
|
|
async with base_ormar_config.database:
|
|
category = await Category.objects.create(name="Sample Category")
|
|
category2 = await Category.objects.create(name="Sample II Category")
|
|
|
|
await Note.objects.bulk_create(
|
|
[
|
|
Note(text="Buy the groceries.", category=category),
|
|
Note(text="Call Mum.", category=category),
|
|
Note(text="Text skynet.", category=category),
|
|
]
|
|
)
|
|
|
|
notes = await Note.objects.all()
|
|
assert len(notes) == 3
|
|
|
|
for note in notes:
|
|
note.category = category2
|
|
|
|
await Note.objects.bulk_update(notes)
|
|
|
|
notes_upd = await Note.objects.all()
|
|
assert len(notes_upd) == 3
|
|
|
|
for note in notes_upd:
|
|
assert note.category.pk == category2.pk
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bulk_update_not_saved_objts():
|
|
async with base_ormar_config.database:
|
|
category = await Category.objects.create(name="Sample Category")
|
|
with pytest.raises(ModelPersistenceError):
|
|
await Note.objects.bulk_update(
|
|
[
|
|
Note(text="Buy the groceries.", category=category),
|
|
Note(text="Call Mum.", category=category),
|
|
]
|
|
)
|
|
|
|
with pytest.raises(ModelListEmptyError):
|
|
await Note.objects.bulk_update([])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bulk_operations_with_json():
|
|
async with base_ormar_config.database:
|
|
items = [
|
|
ItemConfig(item_id="test1"),
|
|
ItemConfig(item_id="test2"),
|
|
ItemConfig(item_id="test3"),
|
|
]
|
|
await ItemConfig.objects.bulk_create(items)
|
|
items = await ItemConfig.objects.all()
|
|
assert all(x.pairs == ["2", "3"] for x in items)
|
|
|
|
for item in items:
|
|
item.pairs = ["1"]
|
|
|
|
await ItemConfig.objects.bulk_update(items)
|
|
items = await ItemConfig.objects.all()
|
|
assert all(x.pairs == ["1"] for x in items)
|
|
|
|
items = await ItemConfig.objects.filter(ItemConfig.id > 1).all()
|
|
for item in items:
|
|
item.pairs = {"b": 2}
|
|
await ItemConfig.objects.bulk_update(items)
|
|
items = await ItemConfig.objects.filter(ItemConfig.id > 1).all()
|
|
assert all(x.pairs == {"b": 2} for x in items)
|
|
|
|
table = ItemConfig.ormar_config.table
|
|
query = table.select().where(table.c.pairs["b"].as_integer() == 2)
|
|
res = [
|
|
ItemConfig.from_row(record, source_model=ItemConfig)
|
|
for record in await base_ormar_config.database.fetch_all(query)
|
|
]
|
|
assert len(res) == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_custom_queryset_cls():
|
|
async with base_ormar_config.database:
|
|
with pytest.raises(ValueError):
|
|
await Customer.objects.first_or_404(id=1)
|
|
|
|
await Customer(name="test").save()
|
|
c = await Customer.objects.first_or_404(name="test")
|
|
assert c.name == "test"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filter_enum():
|
|
async with base_ormar_config.database:
|
|
it = ItemConfig(item_id="test_1")
|
|
await it.save()
|
|
|
|
it = await ItemConfig.objects.filter(size=MySize.SMALL).first()
|
|
assert it
|