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>
This commit is contained in:
collerek
2024-03-23 19:28:28 +01:00
committed by GitHub
parent 3a206dd8dc
commit 500625f0ec
294 changed files with 8132 additions and 9311 deletions

View File

@ -1,114 +1,93 @@
from datetime import datetime
from typing import List, Optional, Union
import databases
import ormar as orm
import pydantic
import pytest
import sqlalchemy
import ormar as orm
from tests.lifespan import init_tests
from tests.settings import create_config
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class MainMeta(orm.ModelMeta):
database = database
metadata = metadata
base_ormar_config = create_config()
class ChagenlogRelease(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "changelog_release"
ormar_config = base_ormar_config.copy(tablename="changelog_release")
class CommitIssue(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "commit_issues"
ormar_config = base_ormar_config.copy(tablename="commit_issues")
class CommitLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "commit_label"
ormar_config = base_ormar_config.copy(tablename="commit_label")
class MergeRequestCommit(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "merge_request_commits"
ormar_config = base_ormar_config.copy(tablename="merge_request_commits")
class MergeRequestIssue(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "merge_request_issues"
ormar_config = base_ormar_config.copy(tablename="merge_request_issues")
class MergeRequestLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "merge_request_labels"
ormar_config = base_ormar_config.copy(tablename="merge_request_labels")
class ProjectLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "project_label"
ormar_config = base_ormar_config.copy(tablename="project_label")
class PushCommit(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "push_commit"
ormar_config = base_ormar_config.copy(tablename="push_commit")
class PushLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "push_label"
ormar_config = base_ormar_config.copy(tablename="push_label")
class TagCommit(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "tag_commits"
ormar_config = base_ormar_config.copy(tablename="tag_commits")
class TagIssue(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "tag_issue"
ormar_config = base_ormar_config.copy(tablename="tag_issue")
class TagLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
class Meta(MainMeta):
tablename = "tag_label"
ormar_config = base_ormar_config.copy(tablename="tag_label")
class UserProject(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
access_level: int = orm.Integer(default=0)
class Meta(MainMeta):
tablename = "user_project"
ormar_config = base_ormar_config.copy(tablename="user_project")
class Label(orm.Model):
@ -117,8 +96,7 @@ class Label(orm.Model):
description: str = orm.Text(default="")
type: str = orm.String(max_length=100, default="")
class Meta(MainMeta):
tablename = "labels"
ormar_config = base_ormar_config.copy(tablename="labels")
class Project(orm.Model):
@ -139,8 +117,7 @@ class Project(orm.Model):
changelog_file: str = orm.String(max_length=250, default="")
version_file: str = orm.String(max_length=250, default="")
class Meta(MainMeta):
tablename = "projects"
ormar_config = base_ormar_config.copy(tablename="projects")
class Issue(orm.Model):
@ -154,8 +131,7 @@ class Issue(orm.Model):
change_type: str = orm.String(max_length=100, default="")
data: pydantic.Json = orm.JSON(default={})
class Meta(MainMeta):
tablename = "issues"
ormar_config = base_ormar_config.copy(tablename="issues")
class User(orm.Model):
@ -163,8 +139,7 @@ class User(orm.Model):
username: str = orm.String(max_length=100, unique=True)
name: str = orm.String(max_length=200, default="")
class Meta(MainMeta):
tablename = "users"
ormar_config = base_ormar_config.copy(tablename="users")
class Branch(orm.Model):
@ -177,8 +152,7 @@ class Branch(orm.Model):
postfix_tag: str = orm.String(max_length=50, default="")
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
class Meta(MainMeta):
tablename = "branches"
ormar_config = base_ormar_config.copy(tablename="branches")
class Changelog(orm.Model):
@ -192,8 +166,7 @@ class Changelog(orm.Model):
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
created_date: datetime = orm.DateTime(default=datetime.utcnow())
class Meta(MainMeta):
tablename = "changelogs"
ormar_config = base_ormar_config.copy(tablename="changelogs")
class Commit(orm.Model):
@ -210,8 +183,7 @@ class Commit(orm.Model):
Issue, through=CommitIssue, ondelete="CASCADE", onupdate="CASCADE"
)
class Meta(MainMeta):
tablename = "commits"
ormar_config = base_ormar_config.copy(tablename="commits")
class MergeRequest(orm.Model):
@ -234,8 +206,7 @@ class MergeRequest(orm.Model):
)
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
class Meta(MainMeta):
tablename = "merge_requests"
ormar_config = base_ormar_config.copy(tablename="merge_requests")
class Push(orm.Model):
@ -259,8 +230,7 @@ class Push(orm.Model):
author: User = orm.ForeignKey(User, ondelete="CASCADE", onupdate="CASCADE")
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
class Meta(MainMeta):
tablename = "pushes"
ormar_config = base_ormar_config.copy(tablename="pushes")
class Tag(orm.Model):
@ -291,8 +261,7 @@ class Tag(orm.Model):
Branch, nullable=True, ondelete="CASCADE", onupdate="CASCADE"
)
class Meta(MainMeta):
tablename = "tags"
ormar_config = base_ormar_config.copy(tablename="tags")
class Release(orm.Model):
@ -305,8 +274,7 @@ class Release(orm.Model):
)
data: pydantic.Json = orm.JSON(default={})
class Meta(MainMeta):
tablename = "releases"
ormar_config = base_ormar_config.copy(tablename="releases")
class Webhook(orm.Model):
@ -328,18 +296,12 @@ class Webhook(orm.Model):
error: str = orm.Text(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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_very_complex_relation_map():
async with database:
async with base_ormar_config.database:
tags = [
{"id": 18, "name": "name-18", "ref": "ref-18"},
{"id": 17, "name": "name-17", "ref": "ref-17"},
@ -349,19 +311,25 @@ async def test_very_complex_relation_map():
{
"id": 9,
"title": "prueba-2321",
"description": "\n<!--- start changelog ver.v.1.3.0.0 -->\n### [v.1.3.0.0] - 2021-08-19\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n<!--- end changelog ver.v.1.3.0.0 -->\n",
"description": "\n<!--- start changelog ver.v.1.3.0.0 -->"
"Description 1"
"<!--- end changelog ver.v.1.3.0.0 -->\n",
"data": {},
},
{
"id": 8,
"title": "prueba-123-prod",
"description": "\n<!--- start changelog ver.v.1.2.0.0 -->\n### [v.1.2.0.0] - 2021-08-19\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n<!--- end changelog ver.v.1.2.0.0 -->\n",
"description": "\n<!--- start changelog ver.v.1.3.0.0 -->"
"Description 2"
"<!--- end changelog ver.v.1.3.0.0 -->\n",
"data": {},
},
{
"id": 6,
"title": "prueba-3-2",
"description": "\n<!--- start changelog ver.v.1.1.0.0 -->\n### [v.1.1.0.0] - 2021-07-29\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n<!--- end changelog ver.v.1.1.0.0 -->\n",
"description": "\n<!--- start changelog ver.v.1.3.0.0 -->"
"Description 3"
"<!--- end changelog ver.v.1.3.0.0 -->\n",
"data": {},
},
]
@ -373,45 +341,42 @@ async def test_very_complex_relation_map():
await Release(**pay, tag=saved_tags[ind]).save()
releases = await Release.objects.order_by(Release.id.desc()).all()
dicts = [release.dict() for release in releases]
dicts = [release.model_dump() for release in releases]
result = [
{
"id": 9,
"title": "prueba-2321",
"description": "\n<!--- start changelog ver.v.1.3.0.0 -->\n### [v.1.3.0.0] - 2021-08-19\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n<!--- end changelog ver.v.1.3.0.0 -->\n",
"description": "\n<!--- start changelog ver.v.1.3.0.0 -->"
"Description 1"
"<!--- end changelog ver.v.1.3.0.0 -->\n",
"data": {},
"tag": {
"id": 18,
"taglabel": None,
"tagcommit": None,
"tagissue": None,
},
"changelogs": [],
},
{
"id": 8,
"title": "prueba-123-prod",
"description": "\n<!--- start changelog ver.v.1.2.0.0 -->\n### [v.1.2.0.0] - 2021-08-19\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n<!--- end changelog ver.v.1.2.0.0 -->\n",
"description": "\n<!--- start changelog ver.v.1.3.0.0 -->"
"Description 2"
"<!--- end changelog ver.v.1.3.0.0 -->\n",
"data": {},
"tag": {
"id": 17,
"taglabel": None,
"tagcommit": None,
"tagissue": None,
},
"changelogs": [],
},
{
"id": 6,
"title": "prueba-3-2",
"description": "\n<!--- start changelog ver.v.1.1.0.0 -->\n### [v.1.1.0.0] - 2021-07-29\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n<!--- end changelog ver.v.1.1.0.0 -->\n",
"description": "\n<!--- start changelog ver.v.1.3.0.0 -->"
"Description 3"
"<!--- end changelog ver.v.1.3.0.0 -->\n",
"data": {},
"tag": {
"id": 12,
"taglabel": None,
"tagcommit": None,
"tagissue": None,
},
"changelogs": [],
},

View File

@ -1,32 +1,23 @@
from typing import List, Optional
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
import pytest
metadata = sqlalchemy.MetaData()
database = databases.Database(DATABASE_URL, force_rollback=True)
from tests.lifespan import init_tests
from tests.settings import create_config
class MainMeta(ormar.ModelMeta):
metadata = metadata
database = database
base_ormar_config = create_config()
class Role(ormar.Model):
class Meta(MainMeta):
pass
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=255, nullable=False)
class User(ormar.Model):
class Meta(MainMeta):
tablename: str = "users"
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255, nullable=False)
@ -36,20 +27,14 @@ class User(ormar.Model):
class Tier(ormar.Model):
class Meta:
tablename = "tiers"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Category(ormar.Model):
class Meta:
tablename = "categories"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -57,10 +42,7 @@ class Category(ormar.Model):
class Item(ormar.Model):
class Meta:
tablename = "items"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -68,6 +50,9 @@ class Item(ormar.Model):
created_by: Optional[User] = ormar.ForeignKey(User)
create_test_database = init_tests(base_ormar_config)
@pytest.fixture(autouse=True, scope="module")
def sample_data():
role = Role(name="User", id=1)
@ -90,13 +75,13 @@ def sample_data():
def test_dumping_to_dict_no_exclusion(sample_data):
item1, item2 = sample_data
dict1 = item1.dict()
dict1 = item1.model_dump()
assert dict1["name"] == "Teddy Bear"
assert dict1["category"]["name"] == "Toys"
assert dict1["category"]["tier"]["name"] == "Tier I"
assert dict1["created_by"]["email"] == "test@test.com"
dict2 = item2.dict()
dict2 = item2.model_dump()
assert dict2["name"] == "M16"
assert dict2["category"]["name"] == "Weapons"
assert dict2["created_by"]["email"] == "test@test.com"
@ -104,17 +89,17 @@ def test_dumping_to_dict_no_exclusion(sample_data):
def test_dumping_to_dict_exclude_set(sample_data):
item1, item2 = sample_data
dict3 = item2.dict(exclude={"name"})
dict3 = item2.model_dump(exclude={"name"})
assert "name" not in dict3
assert dict3["category"]["name"] == "Weapons"
assert dict3["created_by"]["email"] == "test@test.com"
dict4 = item2.dict(exclude={"category"})
dict4 = item2.model_dump(exclude={"category"})
assert dict4["name"] == "M16"
assert "category" not in dict4
assert dict4["created_by"]["email"] == "test@test.com"
dict5 = item2.dict(exclude={"category", "name"})
dict5 = item2.model_dump(exclude={"category", "name"})
assert "name" not in dict5
assert "category" not in dict5
assert dict5["created_by"]["email"] == "test@test.com"
@ -122,7 +107,7 @@ def test_dumping_to_dict_exclude_set(sample_data):
def test_dumping_to_dict_exclude_dict(sample_data):
item1, item2 = sample_data
dict6 = item2.dict(exclude={"category": {"name"}, "name": ...})
dict6 = item2.model_dump(exclude={"category": {"name"}, "name": ...})
assert "name" not in dict6
assert "category" in dict6
assert "name" not in dict6["category"]
@ -131,7 +116,7 @@ def test_dumping_to_dict_exclude_dict(sample_data):
def test_dumping_to_dict_exclude_nested_dict(sample_data):
item1, item2 = sample_data
dict1 = item2.dict(exclude={"category": {"tier": {"name"}}, "name": ...})
dict1 = item2.model_dump(exclude={"category": {"tier": {"name"}}, "name": ...})
assert "name" not in dict1
assert "category" in dict1
assert dict1["category"]["name"] == "Weapons"
@ -141,7 +126,7 @@ def test_dumping_to_dict_exclude_nested_dict(sample_data):
def test_dumping_to_dict_exclude_and_include_nested_dict(sample_data):
item1, item2 = sample_data
dict1 = item2.dict(
dict1 = item2.model_dump(
exclude={"category": {"tier": {"name"}}}, include={"name", "category"}
)
assert dict1.get("name") == "M16"
@ -150,7 +135,7 @@ def test_dumping_to_dict_exclude_and_include_nested_dict(sample_data):
assert "created_by" not in dict1
assert dict1["category"]["tier"].get("name") is None
dict2 = item1.dict(
dict2 = item1.model_dump(
exclude={"id": ...},
include={"name": ..., "category": {"name": ..., "tier": {"id"}}},
)
@ -164,25 +149,31 @@ def test_dumping_to_dict_exclude_and_include_nested_dict(sample_data):
def test_dumping_dict_without_primary_keys(sample_data):
item1, item2 = sample_data
dict1 = item2.dict(exclude_primary_keys=True)
dict1 = item2.model_dump(exclude_primary_keys=True)
assert dict1 == {
"category": {"name": "Weapons", "tier": {"name": "Tier I"}},
"created_by": {
"email": "test@test.com",
"first_name": "Anna",
"password": "ijacids7^*&",
"roles": [{"name": "User"}, {"name": "Admin"}],
"roles": [
{"name": "User"},
{"name": "Admin"},
],
},
"name": "M16",
}
dict2 = item1.dict(exclude_primary_keys=True)
dict2 = item1.model_dump(exclude_primary_keys=True)
assert dict2 == {
"category": {"name": "Toys", "tier": {"name": "Tier I"}},
"created_by": {
"email": "test@test.com",
"first_name": "Anna",
"password": "ijacids7^*&",
"roles": [{"name": "User"}, {"name": "Admin"}],
"roles": [
{"name": "User"},
{"name": "Admin"},
],
},
"name": "Teddy Bear",
}

View File

@ -1,24 +1,16 @@
from typing import List, Optional
import databases
import sqlalchemy
import ormar
from ormar.models.excludable import ExcludableItems
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
from tests.lifespan import init_tests
from tests.settings import create_config
class BaseMeta(ormar.ModelMeta):
database = database
metadata = metadata
base_ormar_config = create_config()
class NickNames(ormar.Model):
class Meta(BaseMeta):
tablename = "nicks"
ormar_config = base_ormar_config.copy(tablename="nicks")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False, name="hq_name")
@ -26,13 +18,11 @@ class NickNames(ormar.Model):
class NicksHq(ormar.Model):
class Meta(BaseMeta):
tablename = "nicks_x_hq"
ormar_config = base_ormar_config.copy(tablename="nicks_x_hq")
class HQ(ormar.Model):
class Meta(BaseMeta):
pass
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False, name="hq_name")
@ -40,8 +30,7 @@ class HQ(ormar.Model):
class Company(ormar.Model):
class Meta(BaseMeta):
tablename = "companies"
ormar_config = base_ormar_config.copy(tablename="companies")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False, name="company_name")
@ -50,8 +39,7 @@ class Company(ormar.Model):
class Car(ormar.Model):
class Meta(BaseMeta):
pass
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
manufacturer: Optional[Company] = ormar.ForeignKey(Company)
@ -62,6 +50,9 @@ class Car(ormar.Model):
aircon_type: str = ormar.String(max_length=20, nullable=True)
create_test_database = init_tests(base_ormar_config)
def compare_results(excludable):
car_excludable = excludable.get(Car)
assert car_excludable.exclude == {"year", "gearbox_type", "gears", "aircon_type"}
@ -69,7 +60,9 @@ def compare_results(excludable):
assert car_excludable.is_excluded("year")
alias = Company.Meta.alias_manager.resolve_relation_alias(Car, "manufacturer")
alias = Company.ormar_config.alias_manager.resolve_relation_alias(
Car, "manufacturer"
)
manu_excludable = excludable.get(Company, alias=alias)
assert manu_excludable.exclude == {"founded"}
assert manu_excludable.include == set()
@ -78,7 +71,7 @@ def compare_results(excludable):
def compare_results_include(excludable):
manager = Company.Meta.alias_manager
manager = Company.ormar_config.alias_manager
car_excludable = excludable.get(Car)
assert car_excludable.include == {"id", "name"}
assert car_excludable.exclude == set()
@ -204,7 +197,9 @@ def test_includes_and_excludes_combo():
assert car_excludable.is_excluded("aircon_type")
assert car_excludable.is_included("name")
alias = Company.Meta.alias_manager.resolve_relation_alias(Car, "manufacturer")
alias = Company.ormar_config.alias_manager.resolve_relation_alias(
Car, "manufacturer"
)
manu_excludable = excludable.get(Company, alias=alias)
assert manu_excludable.include == {"name"}
assert manu_excludable.exclude == {"founded"}

View File

@ -1,44 +1,28 @@
import datetime
import string
import random
import string
import databases
import ormar
import pydantic
import pytest
import sqlalchemy
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import AsyncClient
from ormar import post_save
from pydantic import ConfigDict, computed_field
import ormar
from ormar import post_save, property_field
from tests.settings import DATABASE_URL
from tests.lifespan import init_tests, lifespan
from tests.settings import create_config
app = FastAPI()
metadata = sqlalchemy.MetaData()
database = databases.Database(DATABASE_URL, force_rollback=True)
app.state.database = database
@app.on_event("startup")
async def startup() -> None:
database_ = app.state.database
if not database_.is_connected:
await database_.connect()
@app.on_event("shutdown")
async def shutdown() -> None:
database_ = app.state.database
if database_.is_connected:
await database_.disconnect()
base_ormar_config = create_config()
app = FastAPI(lifespan=lifespan(base_ormar_config))
# note that you can set orm_mode here
# and in this case UserSchema become unnecessary
class UserBase(pydantic.BaseModel):
class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
email: str
first_name: str
@ -51,8 +35,7 @@ class UserCreateSchema(UserBase):
class UserSchema(UserBase):
class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
def gen_pass():
@ -61,10 +44,7 @@ def gen_pass():
class RandomModel(ormar.Model):
class Meta:
tablename: str = "random_users"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="random_users")
id: int = ormar.Integer(primary_key=True)
password: str = ormar.String(max_length=255, default=gen_pass)
@ -74,16 +54,13 @@ class RandomModel(ormar.Model):
server_default=sqlalchemy.func.now()
)
@property_field
@computed_field
def full_name(self) -> str:
return " ".join([self.first_name, self.last_name])
class User(ormar.Model):
class Meta:
tablename: str = "users"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="users")
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255)
@ -94,10 +71,7 @@ class User(ormar.Model):
class User2(ormar.Model):
class Meta:
tablename: str = "users2"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="users2")
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255, nullable=False)
@ -105,17 +79,10 @@ class User2(ormar.Model):
first_name: str = ormar.String(max_length=255)
last_name: str = ormar.String(max_length=255)
category: str = ormar.String(max_length=255, nullable=True)
timestamp: datetime.datetime = ormar.DateTime(
pydantic_only=True, default=datetime.datetime.now
)
timestamp: datetime.datetime = pydantic.Field(default=datetime.datetime.now)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
create_test_database = init_tests(base_ormar_config)
@app.post("/users/", response_model=User, response_model_exclude={"password"})
@ -126,7 +93,7 @@ async def create_user(user: User):
@app.post("/users2/", response_model=User)
async def create_user2(user: User):
user = await user.save()
return user.dict(exclude={"password"})
return user.model_dump(exclude={"password"})
@app.post("/users3/", response_model=UserBase)
@ -136,7 +103,7 @@ async def create_user3(user: User2):
@app.post("/users4/")
async def create_user4(user: User2):
return (await user.save()).dict(exclude={"password"})
return (await user.save()).model_dump(exclude={"password"})
@app.post("/random/", response_model=RandomModel)
@ -276,12 +243,11 @@ async def test_adding_fields_in_endpoints2():
@pytest.mark.asyncio
async def test_excluding_property_field_in_endpoints2():
dummy_registry = {}
@post_save(RandomModel)
async def after_save(sender, instance, **kwargs):
dummy_registry[instance.pk] = instance.dict()
dummy_registry[instance.pk] = instance.model_dump()
client = AsyncClient(app=app, base_url="http://testserver")
async with client as client, LifespanManager(app):

View File

@ -1,15 +1,13 @@
import random
from typing import Optional
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
import pytest
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
def get_position() -> int:
@ -17,10 +15,7 @@ def get_position() -> int:
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="albums")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -28,10 +23,7 @@ class Album(ormar.Model):
class Track(ormar.Model):
class Meta:
tablename = "tracks"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="tracks")
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
@ -40,19 +32,13 @@ class Track(ormar.Model):
play_count: int = ormar.Integer(nullable=True, default=0)
@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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_excluding_field_with_default():
async with database:
async with database.transaction(force_rollback=True):
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
album = await Album.objects.create(name="Miami")
await Track.objects.create(title="Vice City", album=album, play_count=10)
await Track.objects.create(title="Beach Sand", album=album, play_count=20)

View File

@ -1,22 +1,17 @@
from typing import Optional
import databases
import ormar
import pydantic
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
from tests.lifespan import init_tests
from tests.settings import create_config
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
base_ormar_config = create_config()
class Company(ormar.Model):
class Meta:
tablename = "companies"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="companies")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False)
@ -24,10 +19,7 @@ class Company(ormar.Model):
class Car(ormar.Model):
class Meta:
tablename = "cars"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="cars")
id: int = ormar.Integer(primary_key=True)
manufacturer: Optional[Company] = ormar.ForeignKey(Company)
@ -38,19 +30,13 @@ class Car(ormar.Model):
aircon_type: str = ormar.String(max_length=20, nullable=True)
@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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_selecting_subset():
async with database:
async with database.transaction(force_rollback=True):
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
toyota = await Company.objects.create(name="Toyota", founded=1937)
await Car.objects.create(
manufacturer=toyota,
@ -173,8 +159,62 @@ async def test_selecting_subset():
assert car.manufacturer.name == "Toyota"
assert car.manufacturer.founded is None
with pytest.raises(pydantic.error_wrappers.ValidationError):
with pytest.raises(pydantic.ValidationError):
# cannot exclude mandatory model columns - company__name in this example
await Car.objects.select_related("manufacturer").exclude_fields(
["manufacturer__name"]
).all()
@pytest.mark.asyncio
async def test_excluding_nested_lists_in_dump():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
toyota = await Company.objects.create(name="Toyota", founded=1937)
car1 = await Car.objects.create(
manufacturer=toyota,
name="Corolla",
year=2020,
gearbox_type="Manual",
gears=5,
aircon_type="Manual",
)
car2 = await Car.objects.create(
manufacturer=toyota,
name="Yaris",
year=2019,
gearbox_type="Manual",
gears=5,
aircon_type="Manual",
)
manufacturer = await Company.objects.select_related("cars").get(
name="Toyota"
)
assert manufacturer.model_dump() == {
"cars": [
{
"aircon_type": "Manual",
"gearbox_type": "Manual",
"gears": 5,
"id": car1.id,
"name": "Corolla",
"year": 2020,
},
{
"aircon_type": "Manual",
"gearbox_type": "Manual",
"gears": 5,
"id": car2.id,
"name": "Yaris",
"year": 2019,
},
],
"founded": 1937,
"id": toyota.id,
"name": "Toyota",
}
assert manufacturer.model_dump(exclude_list=True) == {
"founded": 1937,
"id": toyota.id,
"name": "Toyota",
}

View File

@ -1,21 +1,16 @@
from typing import List
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
import pytest
metadata = sqlalchemy.MetaData()
database = databases.Database(DATABASE_URL, force_rollback=True)
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Category(ormar.Model):
class Meta:
tablename = "categories"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, default="Test", nullable=True)
@ -23,10 +18,7 @@ class Category(ormar.Model):
class Item(ormar.Model):
class Meta:
tablename = "items"
metadata = metadata
database = database
ormar_config = base_ormar_config.copy(tablename="items")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@ -34,49 +26,44 @@ class Item(ormar.Model):
categories: List[Category] = ormar.ManyToMany(Category)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_exclude_default():
async with database:
async with base_ormar_config.database:
category = Category()
assert category.dict() == {
assert category.model_dump() == {
"id": None,
"items": [],
"name": "Test",
"visibility": True,
}
assert category.dict(exclude_defaults=True) == {"items": []}
assert category.model_dump(exclude_defaults=True) == {"items": []}
await category.save()
category2 = await Category.objects.get()
assert category2.dict() == {
assert category2.model_dump() == {
"id": 1,
"items": [],
"name": "Test",
"visibility": True,
}
assert category2.dict(exclude_defaults=True) == {"id": 1, "items": []}
assert category2.json(exclude_defaults=True) == '{"id": 1, "items": []}'
assert category2.model_dump(exclude_defaults=True) == {"id": 1, "items": []}
assert category2.model_dump_json(exclude_defaults=True) == '{"id":1,"items":[]}'
@pytest.mark.asyncio
async def test_exclude_none():
async with database:
async with base_ormar_config.database:
category = Category(id=2, name=None)
assert category.dict() == {
assert category.model_dump() == {
"id": 2,
"items": [],
"name": None,
"visibility": True,
}
assert category.dict(exclude_none=True) == {
assert category.model_dump(exclude_none=True) == {
"id": 2,
"items": [],
"visibility": True,
@ -84,34 +71,34 @@ async def test_exclude_none():
await category.save()
category2 = await Category.objects.get()
assert category2.dict() == {
assert category2.model_dump() == {
"id": 2,
"items": [],
"name": None,
"visibility": True,
}
assert category2.dict(exclude_none=True) == {
assert category2.model_dump(exclude_none=True) == {
"id": 2,
"items": [],
"visibility": True,
}
assert (
category2.json(exclude_none=True)
== '{"id": 2, "visibility": true, "items": []}'
category2.model_dump_json(exclude_none=True)
== '{"id":2,"visibility":true,"items":[]}'
)
@pytest.mark.asyncio
async def test_exclude_unset():
async with database:
async with base_ormar_config.database:
category = Category(id=3, name="Test 2")
assert category.dict() == {
assert category.model_dump() == {
"id": 3,
"items": [],
"name": "Test 2",
"visibility": True,
}
assert category.dict(exclude_unset=True) == {
assert category.model_dump(exclude_unset=True) == {
"id": 3,
"items": [],
"name": "Test 2",
@ -119,7 +106,7 @@ async def test_exclude_unset():
await category.save()
category2 = await Category.objects.get()
assert category2.dict() == {
assert category2.model_dump() == {
"id": 3,
"items": [],
"name": "Test 2",
@ -127,7 +114,7 @@ async def test_exclude_unset():
}
# NOTE how after loading from db all fields are set explicitly
# as this is what happens when you populate a model from db
assert category2.dict(exclude_unset=True) == {
assert category2.model_dump(exclude_unset=True) == {
"id": 3,
"items": [],
"name": "Test 2",