From bae2d8e1c828094aa5ab424f3860e9c7c39bae02 Mon Sep 17 00:00:00 2001 From: collerek Date: Fri, 11 Dec 2020 13:13:13 +0100 Subject: [PATCH] clean the meta, more tests, partial update of the docs --- docs/relations/index.md | 59 ++++++++++++++++- docs_src/relations/docs003.py | 17 +++++ ormar/models/metaclass.py | 27 ++------ tests/test_inheritance_concrete.py | 8 +-- tests/test_inheritance_concrete_fastapi.py | 77 ++++++++++++++++++++++ tests/test_inheritance_mixins_fastapi.py | 77 ++++++++++++++++++++++ 6 files changed, 240 insertions(+), 25 deletions(-) create mode 100644 docs_src/relations/docs003.py create mode 100644 tests/test_inheritance_concrete_fastapi.py create mode 100644 tests/test_inheritance_mixins_fastapi.py diff --git a/docs/relations/index.md b/docs/relations/index.md index 465f479..2c1eae3 100644 --- a/docs/relations/index.md +++ b/docs/relations/index.md @@ -1,7 +1,64 @@ # Relations +Currently `ormar` supports two types of relations: + +* One-to-many (and many-to-one) with `ForeignKey` field +* Many-to-many with `ManyToMany` field + +Below you can find a very basic examples of definitions for each of those relations. + +To read more about methods, possibilities, definition etc. please read the subsequent section of the documentation. + ## ForeignKey +To define many-to-one relation use `ForeignKey` field. + +```Python hl_lines="17" +--8<-- "../docs_src/relations/docs003.py" +``` + +!!!tip + To read more about one-to-many relations visit [foreign-keys][foreign-keys] section + ## Reverse ForeignKey -##ManyToMany \ No newline at end of file +The definition of one-to-many relation also uses `ForeignKey`, and it's registered for you automatically. + +So in relation ato example above. + +```Python hl_lines="17" +class Department(ormar.Model): + class Meta: + database = database + metadata = metadata + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + # there is a virtual field here like follows + courses: Optional[List[Course]] = ormar.ForeignKey(Course, virtual=True) + # note that you DO NOT define it yourself, ormar does it for you. +``` + +!!!tip + To read more about many-to-one relations (i.e changing the name of generated field) visit [foreign-keys][foreign-keys] section + + +!!!tip + Reverse ForeignKey allows you to query the related models with [queryset-proxy][queryset-proxy]. + +##ManyToMany + +To define many-to-many relation use `ManyToMany` field. + + +!!!tip + To read more about many-to-many relations visit [many-to-many][many-to-many] section + + +!!!tip + ManyToMany allows you to query the related models with [queryset-proxy][queryset-proxy]. + + +[foreign-keys]: ./foreign-key.md +[many-to-many]: ./many-to-many.md +[queryset-proxy]: ./queryset-proxy.md \ No newline at end of file diff --git a/docs_src/relations/docs003.py b/docs_src/relations/docs003.py new file mode 100644 index 0000000..03cc1ec --- /dev/null +++ b/docs_src/relations/docs003.py @@ -0,0 +1,17 @@ +class Department(ormar.Model): + class Meta: + database = database + metadata = metadata + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +class Course(ormar.Model): + class Meta: + database = database + metadata = metadata + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + department: Optional[Union[Department, Dict]] = ormar.ForeignKey(Department) \ No newline at end of file diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 653d2e8..17b5e3f 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -547,8 +547,7 @@ def extract_mixin_fields_from_dict( model_fields: Dict[ str, Union[Type[BaseField], Type[ForeignKeyField], Type[ManyToManyField]] ], - bases: Any, -) -> Tuple[Dict, Dict, Any]: +) -> Tuple[Dict, Dict]: """ Extracts fields from base classes if they have valid oramr fields. @@ -588,15 +587,7 @@ def extract_mixin_fields_from_dict( f"from non abstract class {base_class.__name__}" ) model_fields.update(base_class.Meta.model_fields) # type: ignore - # keep only parent ormar models as they already have all the predecessors - # keeping also Model, NewBaseModel etc. would cause mro conflicts - new_bases = tuple( - base - for base in bases - if issubclass(base, ormar.Model) and base != ormar.Model - ) - - return attrs, model_fields, new_bases + return attrs, model_fields key = "__annotations__" if hasattr(base_class, PARSED_FIELDS_KEY): @@ -620,7 +611,7 @@ def extract_mixin_fields_from_dict( new_model_fields=new_model_fields, new_fields=new_fields, ) - return attrs, model_fields, bases + return attrs, model_fields potential_fields = get_potential_fields(base_class.__dict__) if potential_fields: @@ -648,7 +639,7 @@ def extract_mixin_fields_from_dict( new_model_fields=new_model_fields, new_fields=new_fields, ) - return attrs, model_fields, bases + return attrs, model_fields class ModelMetaclass(pydantic.main.ModelMetaclass): @@ -658,19 +649,15 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): attrs["Config"] = get_pydantic_base_orm_config() attrs["__name__"] = name attrs, model_fields = extract_annotations_and_default_vals(attrs) - new_bases = bases for base in reversed(bases): - attrs, model_fields, new_bases = extract_mixin_fields_from_dict( + attrs, model_fields = extract_mixin_fields_from_dict( base_class=base, curr_class=mcs, attrs=attrs, - model_fields=model_fields, - bases=new_bases, + model_fields=model_fields ) - # print(attrs, model_fields) - new_model = super().__new__( # type: ignore - mcs, name, new_bases, attrs + mcs, name, bases, attrs ) add_cached_properties(new_model) diff --git a/tests/test_inheritance_concrete.py b/tests/test_inheritance_concrete.py index 69e3532..39ab68d 100644 --- a/tests/test_inheritance_concrete.py +++ b/tests/test_inheritance_concrete.py @@ -43,7 +43,7 @@ class DateFieldsModel(ormar.Model): updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) -class Category(ormar.Model, DateFieldsModel, AuditModel): +class Category(DateFieldsModel, AuditModel): class Meta(ormar.ModelMeta): tablename = "categories" metadata = metadata @@ -54,7 +54,7 @@ class Category(ormar.Model, DateFieldsModel, AuditModel): code: int = ormar.Integer() -class Subject(ormar.Model, DateFieldsModel): +class Subject(DateFieldsModel): class Meta(ormar.ModelMeta): tablename = "subjects" metadata = metadata @@ -74,7 +74,7 @@ def create_test_database(): def test_field_redefining_raises_error(): with pytest.raises(ModelDefinitionError): - class WrongField(ormar.Model, DateFieldsModel): # pragma: no cover + class WrongField(DateFieldsModel): # pragma: no cover class Meta(ormar.ModelMeta): tablename = "wrongs" metadata = metadata @@ -86,7 +86,7 @@ def test_field_redefining_raises_error(): def test_model_subclassing_non_abstract_raises_error(): with pytest.raises(ModelDefinitionError): - class WrongField2(ormar.Model, DateFieldsModelNoSubclass): # pragma: no cover + class WrongField2(DateFieldsModelNoSubclass): # pragma: no cover class Meta(ormar.ModelMeta): tablename = "wrongs" metadata = metadata diff --git a/tests/test_inheritance_concrete_fastapi.py b/tests/test_inheritance_concrete_fastapi.py new file mode 100644 index 0000000..c48a223 --- /dev/null +++ b/tests/test_inheritance_concrete_fastapi.py @@ -0,0 +1,77 @@ +import datetime + +import databases +import pytest +import sqlalchemy +from fastapi import FastAPI +from starlette.testclient import TestClient + +from tests.settings import DATABASE_URL +from tests.test_inheritance_concrete import Category, Subject, metadata + +app = FastAPI() +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() + + +@app.post("/subjects/", response_model=Subject) +async def create_item(item: Subject): + return item + + +@app.post("/categories/", response_model=Category) +async def create_category(category: Category): + await category.save() + return 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) + + +def test_read_main(): + client = TestClient(app) + with client as client: + test_category = dict(name="Foo", code=123, created_by="Sam", updated_by="Max") + test_subject = dict(name="Bar") + + response = client.post( + "/categories/", json=test_category + ) + assert response.status_code == 200 + cat = Category(**response.json()) + assert cat.name == 'Foo' + assert cat.created_by == 'Sam' + assert cat.created_date is not None + assert cat.id == 1 + + cat_dict = cat.dict() + cat_dict['updated_date'] = cat_dict['updated_date'].strftime("%Y-%m-%d %H:%M:%S.%f") + cat_dict['created_date'] = cat_dict['created_date'].strftime("%Y-%m-%d %H:%M:%S.%f") + test_subject['category'] = cat_dict + response = client.post( + "/subjects/", json=test_subject + ) + assert response.status_code == 200 + sub = Subject(**response.json()) + assert sub.name == 'Bar' + assert sub.category.pk == cat.pk + assert isinstance(sub.updated_date, datetime.datetime) diff --git a/tests/test_inheritance_mixins_fastapi.py b/tests/test_inheritance_mixins_fastapi.py new file mode 100644 index 0000000..6bece1d --- /dev/null +++ b/tests/test_inheritance_mixins_fastapi.py @@ -0,0 +1,77 @@ +import datetime + +import databases +import pytest +import sqlalchemy +from fastapi import FastAPI +from starlette.testclient import TestClient + +from tests.settings import DATABASE_URL +from tests.test_inheritance_mixins import Category, Subject, metadata + +app = FastAPI() +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() + + +@app.post("/subjects/", response_model=Subject) +async def create_item(item: Subject): + return item + + +@app.post("/categories/", response_model=Category) +async def create_category(category: Category): + await category.save() + return 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) + + +def test_read_main(): + client = TestClient(app) + with client as client: + test_category = dict(name="Foo", code=123, created_by="Sam", updated_by="Max") + test_subject = dict(name="Bar") + + response = client.post( + "/categories/", json=test_category + ) + assert response.status_code == 200 + cat = Category(**response.json()) + assert cat.name == 'Foo' + assert cat.created_by == 'Sam' + assert cat.created_date is not None + assert cat.id == 1 + + cat_dict = cat.dict() + cat_dict['updated_date'] = cat_dict['updated_date'].strftime("%Y-%m-%d %H:%M:%S.%f") + cat_dict['created_date'] = cat_dict['created_date'].strftime("%Y-%m-%d %H:%M:%S.%f") + test_subject['category'] = cat_dict + response = client.post( + "/subjects/", json=test_subject + ) + assert response.status_code == 200 + sub = Subject(**response.json()) + assert sub.name == 'Bar' + assert sub.category.pk == cat.pk + assert isinstance(sub.updated_date, datetime.datetime)