Files
ormar/tests/test_queries/test_queryset_level_methods.py
collerek 500625f0ec WIP - Pydantic v2 support (#1238)
* 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>
2024-03-23 19:28:28 +01:00

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