From 36300f90569b18cb560537bb168de97ed1b18b8a Mon Sep 17 00:00:00 2001 From: collerek Date: Mon, 26 Oct 2020 14:50:04 +0100 Subject: [PATCH 1/5] refactor translating of aliases from queryset to modelproxy --- .coverage | Bin 53248 -> 0 bytes .gitignore | 1 + ormar/models/model.py | 4 ++-- ormar/models/modelproxy.py | 18 ++++++++++++++++++ ormar/queryset/queryset.py | 22 +++------------------- tests/test_models.py | 3 ++- 6 files changed, 26 insertions(+), 22 deletions(-) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index a5c0d9f8600790e0c9b3a198fa799cd7b76e1475..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 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 diff --git a/.gitignore b/.gitignore index cf175d2..fb0cf92 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ p38venv .idea .pytest_cache .mypy_cache +.coverage *.pyc *.log test.db diff --git a/ormar/models/model.py b/ormar/models/model.py index 3805a00..eaf4935 100644 --- a/ormar/models/model.py +++ b/ormar/models/model.py @@ -145,7 +145,7 @@ class Model(NewBaseModel): self_fields = self._extract_model_db_fields() self_fields.pop(self.get_column_name_from_alias(self.Meta.pkname)) - self_fields = self.objects._translate_columns_to_aliases(self_fields) + self_fields = self.translate_columns_to_aliases(self_fields) expr = self.Meta.table.update().values(**self_fields) expr = expr.where(self.pk_column == getattr(self, self.Meta.pkname)) @@ -166,6 +166,6 @@ class Model(NewBaseModel): "Instance was deleted from database and cannot be refreshed" ) kwargs = dict(row) - kwargs = self.objects._translate_aliases_to_columns(kwargs) + kwargs = self.translate_aliases_to_columns(kwargs) self.from_dict(kwargs) return self diff --git a/ormar/models/modelproxy.py b/ormar/models/modelproxy.py index 760232d..61d9bc8 100644 --- a/ormar/models/modelproxy.py +++ b/ormar/models/modelproxy.py @@ -140,6 +140,24 @@ class ModelTableProxy: ) return to_field + @classmethod + def translate_columns_to_aliases(cls, new_kwargs: dict) -> dict: + for field_name, field in cls.Meta.model_fields.items(): + if ( + field_name in new_kwargs + and field.name is not None + and field.name != field_name + ): + new_kwargs[field.name] = new_kwargs.pop(field_name) + return new_kwargs + + @classmethod + def translate_aliases_to_columns(cls, new_kwargs: dict) -> dict: + for field_name, field in cls.Meta.model_fields.items(): + if field.name in new_kwargs and field.name != field_name: + new_kwargs[field_name] = new_kwargs.pop(field.name) + return new_kwargs + @classmethod def merge_instances_list(cls, result_rows: List["Model"]) -> List["Model"]: merged_rows: List["Model"] = [] diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index df1bc01..a6c6b26 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -74,7 +74,7 @@ class QuerySet: new_kwargs = self._remove_pk_from_kwargs(new_kwargs) new_kwargs = self.model.substitute_models_with_pks(new_kwargs) new_kwargs = self._populate_default_values(new_kwargs) - new_kwargs = self._translate_columns_to_aliases(new_kwargs) + new_kwargs = self.model.translate_columns_to_aliases(new_kwargs) return new_kwargs def _populate_default_values(self, new_kwargs: dict) -> dict: @@ -83,22 +83,6 @@ class QuerySet: new_kwargs[field_name] = field.get_default() return new_kwargs - def _translate_columns_to_aliases(self, new_kwargs: dict) -> dict: - for field_name, field in self.model_meta.model_fields.items(): - if ( - field_name in new_kwargs - and field.name is not None - and field.name != field_name - ): - new_kwargs[field.name] = new_kwargs.pop(field_name) - return new_kwargs - - def _translate_aliases_to_columns(self, new_kwargs: dict) -> dict: - for field_name, field in self.model_meta.model_fields.items(): - if field.name in new_kwargs and field.name != field_name: - new_kwargs[field_name] = new_kwargs.pop(field.name) - return new_kwargs - def _remove_pk_from_kwargs(self, new_kwargs: dict) -> dict: pkname = self.model_meta.pkname pk = self.model_meta.model_fields[pkname] @@ -207,7 +191,7 @@ class QuerySet: async def update(self, each: bool = False, **kwargs: Any) -> int: self_fields = self.model.extract_db_own_fields() updates = {k: v for k, v in kwargs.items() if k in self_fields} - updates = self._translate_columns_to_aliases(updates) + updates = self.model.translate_columns_to_aliases(updates) if not each and not self.filter_clauses: raise QueryDefinitionError( "You cannot update without filtering the queryset first. " @@ -353,7 +337,7 @@ class QuerySet: f"{self.model.__name__} has to have {pk_name} filled." ) new_kwargs = self.model.substitute_models_with_pks(new_kwargs) - new_kwargs = self._translate_columns_to_aliases(new_kwargs) + new_kwargs = self.model.translate_columns_to_aliases(new_kwargs) new_kwargs = {"new_" + k: v for k, v in new_kwargs.items() if k in columns} ready_objects.append(new_kwargs) diff --git a/tests/test_models.py b/tests/test_models.py index f33d855..26de3f1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,5 @@ import asyncio +import uuid from datetime import datetime from typing import List @@ -6,7 +7,6 @@ import databases import pydantic import pytest import sqlalchemy -import uuid import ormar from ormar.exceptions import QueryDefinitionError, NoMatch @@ -437,3 +437,4 @@ async def test_start_and_end_filters(): users = await User.objects.filter(name__endswith="igo").all() assert len(users) == 2 + From d3091c404f9abaf8eb994e31255cabcf6f9af278 Mon Sep 17 00:00:00 2001 From: collerek Date: Tue, 27 Oct 2020 13:49:07 +0100 Subject: [PATCH 2/5] fix many_to_many lazy registration in fastapi cloned models, fixed ForeignKey not treated as subclasses of BaseModels in json schema --- ormar/fields/foreign_key.py | 1 + ormar/models/metaclass.py | 48 +++++------ ormar/models/modelproxy.py | 60 +++++++------ ormar/models/newbasemodel.py | 30 +++---- ormar/relations/relation_manager.py | 12 +-- ormar/relations/relation_proxy.py | 12 +-- tests/test_fastapi_docs.py | 125 ++++++++++++++++++++++++++++ tests/test_more_reallife_fastapi.py | 23 ++++- 8 files changed, 232 insertions(+), 79 deletions(-) create mode 100644 tests/test_fastapi_docs.py diff --git a/ormar/fields/foreign_key.py b/ormar/fields/foreign_key.py index 2959f32..854f83d 100644 --- a/ormar/fields/foreign_key.py +++ b/ormar/fields/foreign_key.py @@ -58,6 +58,7 @@ def ForeignKey( # noqa CFQ002 pydantic_only=False, default=None, server_default=None, + __pydantic_model__=to, ) return type("ForeignKey", (ForeignKeyField, BaseField), namespace) diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index d7c57b9..8bb0168 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -41,7 +41,7 @@ def register_relation_on_build(table_name: str, field: Type[ForeignKeyField]) -> def register_many_to_many_relation_on_build( - table_name: str, field: Type[ManyToManyField] + table_name: str, field: Type[ManyToManyField] ) -> None: alias_manager.add_relation_type(field.through.Meta.tablename, table_name) alias_manager.add_relation_type( @@ -50,11 +50,11 @@ def register_many_to_many_relation_on_build( def reverse_field_not_already_registered( - child: Type["Model"], child_model_name: str, parent_model: Type["Model"] + child: Type["Model"], child_model_name: str, parent_model: Type["Model"] ) -> bool: return ( - child_model_name not in parent_model.__fields__ - and child.get_name() not in parent_model.__fields__ + child_model_name not in parent_model.__fields__ + and child.get_name() not in parent_model.__fields__ ) @@ -65,7 +65,7 @@ def expand_reverse_relationships(model: Type["Model"]) -> None: parent_model = model_field.to child = model if reverse_field_not_already_registered( - child, child_model_name, parent_model + child, child_model_name, parent_model ): register_reverse_model_fields( parent_model, child, child_model_name, model_field @@ -73,10 +73,10 @@ def expand_reverse_relationships(model: Type["Model"]) -> None: def register_reverse_model_fields( - model: Type["Model"], - child: Type["Model"], - child_model_name: str, - model_field: Type["ForeignKeyField"], + model: Type["Model"], + child: Type["Model"], + child_model_name: str, + model_field: Type["ForeignKeyField"], ) -> None: if issubclass(model_field, ManyToManyField): model.Meta.model_fields[child_model_name] = ManyToMany( @@ -91,7 +91,7 @@ def register_reverse_model_fields( def adjust_through_many_to_many_model( - model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField] + model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField] ) -> None: model_field.through.Meta.model_fields[model.get_name()] = ForeignKey( model, name=model.get_name(), ondelete="CASCADE" @@ -108,7 +108,7 @@ def adjust_through_many_to_many_model( def create_pydantic_field( - field_name: str, model: Type["Model"], model_field: Type[ManyToManyField] + field_name: str, model: Type["Model"], model_field: Type[ManyToManyField] ) -> None: model_field.through.__fields__[field_name] = ModelField( name=field_name, @@ -120,7 +120,7 @@ def create_pydantic_field( def create_and_append_m2m_fk( - model: Type["Model"], model_field: Type[ManyToManyField] + model: Type["Model"], model_field: Type[ManyToManyField] ) -> None: column = sqlalchemy.Column( model.get_name(), @@ -136,7 +136,7 @@ def create_and_append_m2m_fk( def check_pk_column_validity( - field_name: str, field: BaseField, pkname: Optional[str] + field_name: str, field: BaseField, pkname: Optional[str] ) -> Optional[str]: if pkname is not None: raise ModelDefinitionError("Only one primary key column is allowed.") @@ -146,7 +146,7 @@ def check_pk_column_validity( def sqlalchemy_columns_from_model_fields( - model_fields: Dict, table_name: str + model_fields: Dict, table_name: str ) -> Tuple[Optional[str], List[sqlalchemy.Column]]: columns = [] pkname = None @@ -160,9 +160,9 @@ def sqlalchemy_columns_from_model_fields( if field.primary_key: pkname = check_pk_column_validity(field_name, field, pkname) if ( - not field.pydantic_only - and not field.virtual - and not issubclass(field, ManyToManyField) + not field.pydantic_only + and not field.virtual + and not issubclass(field, ManyToManyField) ): columns.append(field.get_column(field_name)) register_relation_in_alias_manager(table_name, field) @@ -170,7 +170,7 @@ def sqlalchemy_columns_from_model_fields( def register_relation_in_alias_manager( - table_name: str, field: Type[ForeignKeyField] + table_name: str, field: Type[ForeignKeyField] ) -> None: if issubclass(field, ManyToManyField): register_many_to_many_relation_on_build(table_name, field) @@ -179,7 +179,7 @@ def register_relation_in_alias_manager( def populate_default_pydantic_field_value( - type_: Type[BaseField], field: str, attrs: dict + type_: Type[BaseField], field: str, attrs: dict ) -> dict: def_value = type_.default_value() curr_def_value = attrs.get(field, "NONE") @@ -208,7 +208,7 @@ def extract_annotations_and_default_vals(attrs: dict, bases: Tuple) -> dict: def populate_meta_orm_model_fields( - attrs: dict, new_model: Type["Model"] + attrs: dict, new_model: Type["Model"] ) -> Type["Model"]: model_fields = { field_name: field @@ -220,7 +220,7 @@ def populate_meta_orm_model_fields( def populate_meta_tablename_columns_and_pk( - name: str, new_model: Type["Model"] + name: str, new_model: Type["Model"] ) -> Type["Model"]: tablename = name.lower() + "s" new_model.Meta.tablename = ( @@ -246,7 +246,7 @@ def populate_meta_tablename_columns_and_pk( def populate_meta_sqlalchemy_table_if_required( - new_model: Type["Model"], + new_model: Type["Model"], ) -> Type["Model"]: if not hasattr(new_model.Meta, "table"): new_model.Meta.table = sqlalchemy.Table( @@ -288,7 +288,7 @@ def choices_validator(cls: Type["Model"], values: Dict[str, Any]) -> Dict[str, A def populate_choices_validators( # noqa CCR001 - model: Type["Model"], attrs: Dict + model: Type["Model"], attrs: Dict ) -> None: if model_initialized_and_has_model_fields(model): for _, field in model.Meta.model_fields.items(): @@ -301,7 +301,7 @@ def populate_choices_validators( # noqa CCR001 class ModelMetaclass(pydantic.main.ModelMetaclass): def __new__( # type: ignore - mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict + mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict ) -> "ModelMetaclass": attrs["Config"] = get_pydantic_base_orm_config() attrs["__name__"] = name diff --git a/ormar/models/modelproxy.py b/ormar/models/modelproxy.py index 61d9bc8..7acfc0a 100644 --- a/ormar/models/modelproxy.py +++ b/ormar/models/modelproxy.py @@ -5,7 +5,7 @@ import ormar from ormar.exceptions import RelationshipInstanceError from ormar.fields import BaseField, ManyToManyField from ormar.fields.foreign_key import ForeignKeyField -from ormar.models.metaclass import ModelMeta +from ormar.models.metaclass import ModelMeta, expand_reverse_relationships if TYPE_CHECKING: # pragma no cover from ormar import Model @@ -76,10 +76,10 @@ class ModelTableProxy: related_names = set() for name, field in cls.Meta.model_fields.items(): if ( - inspect.isclass(field) - and issubclass(field, ForeignKeyField) - and not issubclass(field, ManyToManyField) - and not field.virtual + inspect.isclass(field) + and issubclass(field, ForeignKeyField) + and not issubclass(field, ManyToManyField) + and not field.virtual ): related_names.add(name) return related_names @@ -91,9 +91,9 @@ class ModelTableProxy: related_names = set() for name, field in cls.Meta.model_fields.items(): if ( - inspect.isclass(field) - and issubclass(field, ForeignKeyField) - and field.nullable + inspect.isclass(field) + and issubclass(field, ForeignKeyField) + and field.nullable ): related_names.add(name) return related_names @@ -113,8 +113,9 @@ class ModelTableProxy: @staticmethod def resolve_relation_name( - item: Union["NewBaseModel", Type["NewBaseModel"]], - related: Union["NewBaseModel", Type["NewBaseModel"]], + item: Union["NewBaseModel", Type["NewBaseModel"]], + related: Union["NewBaseModel", Type["NewBaseModel"]], + register_missing: bool = True ) -> str: for name, field in item.Meta.model_fields.items(): if issubclass(field, ForeignKeyField): @@ -123,13 +124,18 @@ class ModelTableProxy: # so we need to compare Meta too as this one is copied as is if field.to == related.__class__ or field.to.Meta == related.Meta: return name + # fallback for not registered relation + if register_missing: + expand_reverse_relationships(related.__class__) + return ModelTableProxy.resolve_relation_name(item, related, register_missing=False) + raise ValueError( f"No relation between {item.get_name()} and {related.get_name()}" ) # pragma nocover @staticmethod def resolve_relation_field( - item: Union["Model", Type["Model"]], related: Union["Model", Type["Model"]] + item: Union["Model", Type["Model"]], related: Union["Model", Type["Model"]] ) -> Union[Type[BaseField], Type[ForeignKeyField]]: name = ModelTableProxy.resolve_relation_name(item, related) to_field = item.Meta.model_fields.get(name) @@ -144,9 +150,9 @@ class ModelTableProxy: def translate_columns_to_aliases(cls, new_kwargs: dict) -> dict: for field_name, field in cls.Meta.model_fields.items(): if ( - field_name in new_kwargs - and field.name is not None - and field.name != field_name + field_name in new_kwargs + and field.name is not None + and field.name != field_name ): new_kwargs[field.name] = new_kwargs.pop(field_name) return new_kwargs @@ -173,12 +179,12 @@ class ModelTableProxy: for field in one.Meta.model_fields.keys(): current_field = getattr(one, field) if isinstance(current_field, list) and not isinstance( - current_field, ormar.Model + current_field, ormar.Model ): setattr(other, field, current_field + getattr(other, field)) elif ( - isinstance(current_field, ormar.Model) - and current_field.pk == getattr(other, field).pk + isinstance(current_field, ormar.Model) + and current_field.pk == getattr(other, field).pk ): setattr( other, @@ -189,10 +195,10 @@ class ModelTableProxy: @staticmethod def _get_not_nested_columns_from_fields( - model: Type["Model"], - fields: List, - column_names: List[str], - use_alias: bool = False, + model: Type["Model"], + fields: List, + column_names: List[str], + use_alias: bool = False, ) -> List[str]: fields = [model.get_column_alias(k) if not use_alias else k for k in fields] columns = [name for name in fields if "__" not in name and name in column_names] @@ -200,11 +206,11 @@ class ModelTableProxy: @staticmethod def _get_nested_columns_from_fields( - model: Type["Model"], fields: List, use_alias: bool = False, + model: Type["Model"], fields: List, use_alias: bool = False, ) -> List[str]: model_name = f"{model.get_name()}__" columns = [ - name[(name.find(model_name) + len(model_name)) :] # noqa: E203 + name[(name.find(model_name) + len(model_name)):] # noqa: E203 for name in fields if f"{model.get_name()}__" in name ] @@ -213,10 +219,10 @@ class ModelTableProxy: @staticmethod def own_table_columns( - model: Type["Model"], - fields: List, - nested: bool = False, - use_alias: bool = False, + model: Type["Model"], + fields: List, + nested: bool = False, + use_alias: bool = False, ) -> List[str]: column_names = [ model.get_column_name_from_alias(col.name) if use_alias else col.name diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index 88664fb..2809db7 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -132,12 +132,12 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass return super().__getattribute__(item) def _extract_related_model_instead_of_field( - self, item: str + self, item: str ) -> Optional[Union["Model", List["Model"]]]: alias = self.get_column_alias(item) if alias in self._orm: return self._orm.get(alias) - return None + return None # pragma no cover def __eq__(self, other: object) -> bool: if isinstance(other, NewBaseModel): @@ -146,9 +146,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass def __same__(self, other: "NewBaseModel") -> bool: return ( - self._orm_id == other._orm_id - or self.dict() == other.dict() - or (self.pk == other.pk and self.pk is not None) + self._orm_id == other._orm_id + or self.dict() == other.dict() + or (self.pk == other.pk and self.pk is not None) ) @classmethod @@ -170,16 +170,16 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass self._orm.remove_parent(self, name) def dict( # noqa A003 - self, - *, - include: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - by_alias: bool = False, - skip_defaults: bool = None, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - nested: bool = False + self, + *, + include: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, + exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, + by_alias: bool = False, + skip_defaults: bool = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + nested: bool = False ) -> "DictStrAny": # noqa: A003' dict_instance = super().dict( include=include, diff --git a/ormar/relations/relation_manager.py b/ormar/relations/relation_manager.py index a176bec..c716d61 100644 --- a/ormar/relations/relation_manager.py +++ b/ormar/relations/relation_manager.py @@ -17,9 +17,9 @@ if TYPE_CHECKING: # pragma no cover class RelationsManager: def __init__( - self, - related_fields: List[Type[ForeignKeyField]] = None, - owner: "NewBaseModel" = None, + self, + related_fields: List[Type[ForeignKeyField]] = None, + owner: "NewBaseModel" = None, ) -> None: self.owner = proxy(owner) self._related_fields = related_fields or [] @@ -40,6 +40,8 @@ class RelationsManager: to=field.to, through=getattr(field, "through", None), ) + if field.name not in self._related_names: + self._related_names.append(field.name) def __contains__(self, item: str) -> bool: return item in self._related_names @@ -74,7 +76,7 @@ class RelationsManager: child_relation.add(parent) def remove( - self, name: str, child: Union["NewBaseModel", Type["NewBaseModel"]] + self, name: str, child: Union["NewBaseModel", Type["NewBaseModel"]] ) -> None: relation = self._get(name) if relation: @@ -82,7 +84,7 @@ class RelationsManager: @staticmethod def remove_parent( - item: Union["NewBaseModel", Type["NewBaseModel"]], name: "Model" + item: Union["NewBaseModel", Type["NewBaseModel"]], name: "Model" ) -> None: related_model = name rel_name = item.resolve_relation_name(item, related_model) diff --git a/ormar/relations/relation_proxy.py b/ormar/relations/relation_proxy.py index 88130d5..f658e57 100644 --- a/ormar/relations/relation_proxy.py +++ b/ormar/relations/relation_proxy.py @@ -6,7 +6,7 @@ from ormar.relations.querysetproxy import QuerysetProxy if TYPE_CHECKING: # pragma no cover from ormar import Model - from ormar.relations import Relation + from ormar.relations import Relation, register_missing_relation from ormar.queryset import QuerySet @@ -33,8 +33,8 @@ class RelationProxy(list): def _check_if_queryset_is_initialized(self) -> bool: return ( - hasattr(self.queryset_proxy, "queryset") - and self.queryset_proxy.queryset is not None + hasattr(self.queryset_proxy, "queryset") + and self.queryset_proxy.queryset is not None ) def _set_queryset(self) -> "QuerySet": @@ -48,8 +48,8 @@ class RelationProxy(list): kwargs = {f"{owner_table}__{pkname}": pk_value} queryset = ( ormar.QuerySet(model_cls=self.relation.to) - .select_related(owner_table) - .filter(**kwargs) + .select_related(owner_table) + .filter(**kwargs) ) return queryset @@ -72,4 +72,6 @@ class RelationProxy(list): if self.relation._type == ormar.RelationType.MULTIPLE: await self.queryset_proxy.create_through_instance(item) rel_name = item.resolve_relation_name(item, self._owner) + if not rel_name in item._orm: + item._orm._add_relation(item.Meta.model_fields[rel_name]) setattr(item, rel_name, self._owner) diff --git a/tests/test_fastapi_docs.py b/tests/test_fastapi_docs.py new file mode 100644 index 0000000..c6cd35b --- /dev/null +++ b/tests/test_fastapi_docs.py @@ -0,0 +1,125 @@ +from typing import List + +import databases +import pytest +import sqlalchemy +from fastapi import FastAPI +from starlette.testclient import TestClient + +import ormar +from tests.settings import DATABASE_URL + +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() + + +class LocalMeta: + metadata = metadata + database = database + + +class Category(ormar.Model): + class Meta(LocalMeta): + tablename = "categories" + + id: ormar.Integer(primary_key=True) + name: ormar.String(max_length=100) + + +class ItemsXCategories(ormar.Model): + class Meta(LocalMeta): + tablename = 'items_x_categories' + + +class Item(ormar.Model): + class Meta(LocalMeta): + pass + + id: ormar.Integer(primary_key=True) + name: ormar.String(max_length=100) + categories: ormar.ManyToMany(Category, through=ItemsXCategories) + + +@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) + + +@app.get("/items/", response_model=List[Item]) +async def get_items(): + items = await Item.objects.select_related("categories").all() + return items + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item): + await item.save() + return item + + +@app.post("/items/add_category/", response_model=Item) +async def create_item(item: Item, category: Category): + await item.categories.add(category) + return item + + +@app.post("/categories/", response_model=Category) +async def create_category(category: Category): + await category.save() + return category + + +def test_all_endpoints(): + client = TestClient(app) + with client as client: + response = client.post("/categories/", json={"name": "test cat"}) + category = response.json() + response = client.post("/categories/", json={"name": "test cat2"}) + category2 = response.json() + + response = client.post( + "/items/", json={"name": "test", "id": 1} + ) + item = Item(**response.json()) + assert item.pk is not None + + response = client.post( + "/items/add_category/", json={"item": item.dict(), "category": category} + ) + item = Item(**response.json()) + assert len(item.categories) == 1 + assert item.categories[0].name == 'test cat' + + client.post( + "/items/add_category/", json={"item": item.dict(), "category": category2} + ) + + response = client.get("/items/") + items = [Item(**item) for item in response.json()] + assert items[0] == item + assert len(items[0].categories) == 2 + assert items[0].categories[0].name == 'test cat' + assert items[0].categories[1].name == 'test cat2' + + response = client.get("/docs/") + assert response.status_code == 200 + assert b'FastAPI - Swagger UI' in response.content diff --git a/tests/test_more_reallife_fastapi.py b/tests/test_more_reallife_fastapi.py index f0b9b88..3a5e909 100644 --- a/tests/test_more_reallife_fastapi.py +++ b/tests/test_more_reallife_fastapi.py @@ -77,13 +77,15 @@ async def create_category(category: Category): @app.put("/items/{item_id}") -async def get_item(item_id: int, item: Item): +async def update_item(item_id: int, item: Item): item_db = await Item.objects.get(pk=item_id) return await item_db.update(**item.dict()) @app.delete("/items/{item_id}") -async def delete_item(item_id: int, item: Item): +async def delete_item(item_id: int, item: Item = None): + if item: + return {"deleted_rows": await item.delete()} item_db = await Item.objects.get(pk=item_id) return {"deleted_rows": await item_db.delete()} @@ -111,8 +113,23 @@ def test_all_endpoints(): items = [Item(**item) for item in response.json()] assert items[0].name == "New name" - response = client.delete(f"/items/{item.pk}", json=item.dict()) + response = client.delete(f"/items/{item.pk}") assert response.json().get("deleted_rows", "__UNDEFINED__") != "__UNDEFINED__" response = client.get("/items/") items = response.json() assert len(items) == 0 + + client.post( + "/items/", json={"name": "test_2", "id": 2, "category": category} + ) + response = client.get("/items/") + items = response.json() + assert len(items) == 1 + + item = Item(**items[0]) + response = client.delete(f"/items/{item.pk}", json=item.dict()) + assert response.json().get("deleted_rows", "__UNDEFINED__") != "__UNDEFINED__" + + response = client.get("/docs/") + assert response.status_code == 200 + From 82e3eb94ae142c669034770ecc1084d6ff1c1fe9 Mon Sep 17 00:00:00 2001 From: collerek Date: Tue, 27 Oct 2020 17:55:41 +0100 Subject: [PATCH 3/5] modify schema to show many to many as list of nested models, check openapi generation in tests --- ormar/fields/many_to_many.py | 14 +++- ormar/models/metaclass.py | 48 ++++++------- ormar/models/modelproxy.py | 66 +++++++++--------- ormar/models/newbasemodel.py | 30 ++++---- ormar/relations/relation_manager.py | 10 +-- ormar/relations/relation_proxy.py | 12 ++-- tests/test_aliases.py | 103 +++++++++++++++++----------- tests/test_fastapi_docs.py | 26 ++++--- tests/test_models.py | 1 - tests/test_more_reallife_fastapi.py | 5 +- 10 files changed, 178 insertions(+), 137 deletions(-) diff --git a/ormar/fields/many_to_many.py b/ormar/fields/many_to_many.py index 0fed9e8..1f73a0d 100644 --- a/ormar/fields/many_to_many.py +++ b/ormar/fields/many_to_many.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Type +from typing import Dict, TYPE_CHECKING, Type from ormar.fields import BaseField from ormar.fields.foreign_key import ForeignKeyField @@ -6,6 +6,8 @@ from ormar.fields.foreign_key import ForeignKeyField if TYPE_CHECKING: # pragma no cover from ormar.models import Model +REF_PREFIX = "#/components/schemas/" + def ManyToMany( to: Type["Model"], @@ -31,6 +33,9 @@ def ManyToMany( pydantic_only=False, default=None, server_default=None, + __pydantic_model__=to, + # __origin__=List, + # __args__=[Optional[to]] ) return type("ManyToMany", (ManyToManyField, BaseField), namespace) @@ -38,3 +43,10 @@ def ManyToMany( class ManyToManyField(ForeignKeyField): through: Type["Model"] + + @classmethod + def __modify_schema__(cls, field_schema: Dict) -> None: + field_schema["type"] = "array" + field_schema["title"] = cls.name.title() + field_schema["definitions"] = {f"{cls.to.__name__}": cls.to.schema()} + field_schema["items"] = {"$ref": f"{REF_PREFIX}{cls.to.__name__}"} diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 8bb0168..d7c57b9 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -41,7 +41,7 @@ def register_relation_on_build(table_name: str, field: Type[ForeignKeyField]) -> def register_many_to_many_relation_on_build( - table_name: str, field: Type[ManyToManyField] + table_name: str, field: Type[ManyToManyField] ) -> None: alias_manager.add_relation_type(field.through.Meta.tablename, table_name) alias_manager.add_relation_type( @@ -50,11 +50,11 @@ def register_many_to_many_relation_on_build( def reverse_field_not_already_registered( - child: Type["Model"], child_model_name: str, parent_model: Type["Model"] + child: Type["Model"], child_model_name: str, parent_model: Type["Model"] ) -> bool: return ( - child_model_name not in parent_model.__fields__ - and child.get_name() not in parent_model.__fields__ + child_model_name not in parent_model.__fields__ + and child.get_name() not in parent_model.__fields__ ) @@ -65,7 +65,7 @@ def expand_reverse_relationships(model: Type["Model"]) -> None: parent_model = model_field.to child = model if reverse_field_not_already_registered( - child, child_model_name, parent_model + child, child_model_name, parent_model ): register_reverse_model_fields( parent_model, child, child_model_name, model_field @@ -73,10 +73,10 @@ def expand_reverse_relationships(model: Type["Model"]) -> None: def register_reverse_model_fields( - model: Type["Model"], - child: Type["Model"], - child_model_name: str, - model_field: Type["ForeignKeyField"], + model: Type["Model"], + child: Type["Model"], + child_model_name: str, + model_field: Type["ForeignKeyField"], ) -> None: if issubclass(model_field, ManyToManyField): model.Meta.model_fields[child_model_name] = ManyToMany( @@ -91,7 +91,7 @@ def register_reverse_model_fields( def adjust_through_many_to_many_model( - model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField] + model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField] ) -> None: model_field.through.Meta.model_fields[model.get_name()] = ForeignKey( model, name=model.get_name(), ondelete="CASCADE" @@ -108,7 +108,7 @@ def adjust_through_many_to_many_model( def create_pydantic_field( - field_name: str, model: Type["Model"], model_field: Type[ManyToManyField] + field_name: str, model: Type["Model"], model_field: Type[ManyToManyField] ) -> None: model_field.through.__fields__[field_name] = ModelField( name=field_name, @@ -120,7 +120,7 @@ def create_pydantic_field( def create_and_append_m2m_fk( - model: Type["Model"], model_field: Type[ManyToManyField] + model: Type["Model"], model_field: Type[ManyToManyField] ) -> None: column = sqlalchemy.Column( model.get_name(), @@ -136,7 +136,7 @@ def create_and_append_m2m_fk( def check_pk_column_validity( - field_name: str, field: BaseField, pkname: Optional[str] + field_name: str, field: BaseField, pkname: Optional[str] ) -> Optional[str]: if pkname is not None: raise ModelDefinitionError("Only one primary key column is allowed.") @@ -146,7 +146,7 @@ def check_pk_column_validity( def sqlalchemy_columns_from_model_fields( - model_fields: Dict, table_name: str + model_fields: Dict, table_name: str ) -> Tuple[Optional[str], List[sqlalchemy.Column]]: columns = [] pkname = None @@ -160,9 +160,9 @@ def sqlalchemy_columns_from_model_fields( if field.primary_key: pkname = check_pk_column_validity(field_name, field, pkname) if ( - not field.pydantic_only - and not field.virtual - and not issubclass(field, ManyToManyField) + not field.pydantic_only + and not field.virtual + and not issubclass(field, ManyToManyField) ): columns.append(field.get_column(field_name)) register_relation_in_alias_manager(table_name, field) @@ -170,7 +170,7 @@ def sqlalchemy_columns_from_model_fields( def register_relation_in_alias_manager( - table_name: str, field: Type[ForeignKeyField] + table_name: str, field: Type[ForeignKeyField] ) -> None: if issubclass(field, ManyToManyField): register_many_to_many_relation_on_build(table_name, field) @@ -179,7 +179,7 @@ def register_relation_in_alias_manager( def populate_default_pydantic_field_value( - type_: Type[BaseField], field: str, attrs: dict + type_: Type[BaseField], field: str, attrs: dict ) -> dict: def_value = type_.default_value() curr_def_value = attrs.get(field, "NONE") @@ -208,7 +208,7 @@ def extract_annotations_and_default_vals(attrs: dict, bases: Tuple) -> dict: def populate_meta_orm_model_fields( - attrs: dict, new_model: Type["Model"] + attrs: dict, new_model: Type["Model"] ) -> Type["Model"]: model_fields = { field_name: field @@ -220,7 +220,7 @@ def populate_meta_orm_model_fields( def populate_meta_tablename_columns_and_pk( - name: str, new_model: Type["Model"] + name: str, new_model: Type["Model"] ) -> Type["Model"]: tablename = name.lower() + "s" new_model.Meta.tablename = ( @@ -246,7 +246,7 @@ def populate_meta_tablename_columns_and_pk( def populate_meta_sqlalchemy_table_if_required( - new_model: Type["Model"], + new_model: Type["Model"], ) -> Type["Model"]: if not hasattr(new_model.Meta, "table"): new_model.Meta.table = sqlalchemy.Table( @@ -288,7 +288,7 @@ def choices_validator(cls: Type["Model"], values: Dict[str, Any]) -> Dict[str, A def populate_choices_validators( # noqa CCR001 - model: Type["Model"], attrs: Dict + model: Type["Model"], attrs: Dict ) -> None: if model_initialized_and_has_model_fields(model): for _, field in model.Meta.model_fields.items(): @@ -301,7 +301,7 @@ def populate_choices_validators( # noqa CCR001 class ModelMetaclass(pydantic.main.ModelMetaclass): def __new__( # type: ignore - mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict + mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict ) -> "ModelMetaclass": attrs["Config"] = get_pydantic_base_orm_config() attrs["__name__"] = name diff --git a/ormar/models/modelproxy.py b/ormar/models/modelproxy.py index 7acfc0a..c965154 100644 --- a/ormar/models/modelproxy.py +++ b/ormar/models/modelproxy.py @@ -76,10 +76,10 @@ class ModelTableProxy: related_names = set() for name, field in cls.Meta.model_fields.items(): if ( - inspect.isclass(field) - and issubclass(field, ForeignKeyField) - and not issubclass(field, ManyToManyField) - and not field.virtual + inspect.isclass(field) + and issubclass(field, ForeignKeyField) + and not issubclass(field, ManyToManyField) + and not field.virtual ): related_names.add(name) return related_names @@ -91,9 +91,9 @@ class ModelTableProxy: related_names = set() for name, field in cls.Meta.model_fields.items(): if ( - inspect.isclass(field) - and issubclass(field, ForeignKeyField) - and field.nullable + inspect.isclass(field) + and issubclass(field, ForeignKeyField) + and field.nullable ): related_names.add(name) return related_names @@ -112,10 +112,10 @@ class ModelTableProxy: return self_fields @staticmethod - def resolve_relation_name( - item: Union["NewBaseModel", Type["NewBaseModel"]], - related: Union["NewBaseModel", Type["NewBaseModel"]], - register_missing: bool = True + def resolve_relation_name( # noqa CCR001 + item: Union["NewBaseModel", Type["NewBaseModel"]], + related: Union["NewBaseModel", Type["NewBaseModel"]], + register_missing: bool = True, ) -> str: for name, field in item.Meta.model_fields.items(): if issubclass(field, ForeignKeyField): @@ -126,8 +126,10 @@ class ModelTableProxy: return name # fallback for not registered relation if register_missing: - expand_reverse_relationships(related.__class__) - return ModelTableProxy.resolve_relation_name(item, related, register_missing=False) + expand_reverse_relationships(related.__class__) # type: ignore + return ModelTableProxy.resolve_relation_name( + item, related, register_missing=False + ) raise ValueError( f"No relation between {item.get_name()} and {related.get_name()}" @@ -135,7 +137,7 @@ class ModelTableProxy: @staticmethod def resolve_relation_field( - item: Union["Model", Type["Model"]], related: Union["Model", Type["Model"]] + item: Union["Model", Type["Model"]], related: Union["Model", Type["Model"]] ) -> Union[Type[BaseField], Type[ForeignKeyField]]: name = ModelTableProxy.resolve_relation_name(item, related) to_field = item.Meta.model_fields.get(name) @@ -147,18 +149,18 @@ class ModelTableProxy: return to_field @classmethod - def translate_columns_to_aliases(cls, new_kwargs: dict) -> dict: + def translate_columns_to_aliases(cls, new_kwargs: Dict) -> Dict: for field_name, field in cls.Meta.model_fields.items(): if ( - field_name in new_kwargs - and field.name is not None - and field.name != field_name + field_name in new_kwargs + and field.name is not None + and field.name != field_name ): new_kwargs[field.name] = new_kwargs.pop(field_name) return new_kwargs @classmethod - def translate_aliases_to_columns(cls, new_kwargs: dict) -> dict: + def translate_aliases_to_columns(cls, new_kwargs: Dict) -> Dict: for field_name, field in cls.Meta.model_fields.items(): if field.name in new_kwargs and field.name != field_name: new_kwargs[field_name] = new_kwargs.pop(field.name) @@ -179,12 +181,12 @@ class ModelTableProxy: for field in one.Meta.model_fields.keys(): current_field = getattr(one, field) if isinstance(current_field, list) and not isinstance( - current_field, ormar.Model + current_field, ormar.Model ): setattr(other, field, current_field + getattr(other, field)) elif ( - isinstance(current_field, ormar.Model) - and current_field.pk == getattr(other, field).pk + isinstance(current_field, ormar.Model) + and current_field.pk == getattr(other, field).pk ): setattr( other, @@ -195,10 +197,10 @@ class ModelTableProxy: @staticmethod def _get_not_nested_columns_from_fields( - model: Type["Model"], - fields: List, - column_names: List[str], - use_alias: bool = False, + model: Type["Model"], + fields: List, + column_names: List[str], + use_alias: bool = False, ) -> List[str]: fields = [model.get_column_alias(k) if not use_alias else k for k in fields] columns = [name for name in fields if "__" not in name and name in column_names] @@ -206,11 +208,11 @@ class ModelTableProxy: @staticmethod def _get_nested_columns_from_fields( - model: Type["Model"], fields: List, use_alias: bool = False, + model: Type["Model"], fields: List, use_alias: bool = False, ) -> List[str]: model_name = f"{model.get_name()}__" columns = [ - name[(name.find(model_name) + len(model_name)):] # noqa: E203 + name[(name.find(model_name) + len(model_name)) :] # noqa: E203 for name in fields if f"{model.get_name()}__" in name ] @@ -219,10 +221,10 @@ class ModelTableProxy: @staticmethod def own_table_columns( - model: Type["Model"], - fields: List, - nested: bool = False, - use_alias: bool = False, + model: Type["Model"], + fields: List, + nested: bool = False, + use_alias: bool = False, ) -> List[str]: column_names = [ model.get_column_name_from_alias(col.name) if use_alias else col.name diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index 2809db7..5cf28bc 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -100,7 +100,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass ) def __setattr__(self, name: str, value: Any) -> None: # noqa CCR001 - if name in self.__slots__: + if name in ("_orm_id", "_orm_saved", "_orm"): object.__setattr__(self, name, value) elif name == "pk": object.__setattr__(self, self.Meta.pkname, value) @@ -132,7 +132,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass return super().__getattribute__(item) def _extract_related_model_instead_of_field( - self, item: str + self, item: str ) -> Optional[Union["Model", List["Model"]]]: alias = self.get_column_alias(item) if alias in self._orm: @@ -146,9 +146,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass def __same__(self, other: "NewBaseModel") -> bool: return ( - self._orm_id == other._orm_id - or self.dict() == other.dict() - or (self.pk == other.pk and self.pk is not None) + self._orm_id == other._orm_id + or self.dict() == other.dict() + or (self.pk == other.pk and self.pk is not None) ) @classmethod @@ -170,16 +170,16 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass self._orm.remove_parent(self, name) def dict( # noqa A003 - self, - *, - include: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - by_alias: bool = False, - skip_defaults: bool = None, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - nested: bool = False + self, + *, + include: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, + exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, + by_alias: bool = False, + skip_defaults: bool = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + nested: bool = False ) -> "DictStrAny": # noqa: A003' dict_instance = super().dict( include=include, diff --git a/ormar/relations/relation_manager.py b/ormar/relations/relation_manager.py index c716d61..6e7eb24 100644 --- a/ormar/relations/relation_manager.py +++ b/ormar/relations/relation_manager.py @@ -17,9 +17,9 @@ if TYPE_CHECKING: # pragma no cover class RelationsManager: def __init__( - self, - related_fields: List[Type[ForeignKeyField]] = None, - owner: "NewBaseModel" = None, + self, + related_fields: List[Type[ForeignKeyField]] = None, + owner: "NewBaseModel" = None, ) -> None: self.owner = proxy(owner) self._related_fields = related_fields or [] @@ -76,7 +76,7 @@ class RelationsManager: child_relation.add(parent) def remove( - self, name: str, child: Union["NewBaseModel", Type["NewBaseModel"]] + self, name: str, child: Union["NewBaseModel", Type["NewBaseModel"]] ) -> None: relation = self._get(name) if relation: @@ -84,7 +84,7 @@ class RelationsManager: @staticmethod def remove_parent( - item: Union["NewBaseModel", Type["NewBaseModel"]], name: "Model" + item: Union["NewBaseModel", Type["NewBaseModel"]], name: "Model" ) -> None: related_model = name rel_name = item.resolve_relation_name(item, related_model) diff --git a/ormar/relations/relation_proxy.py b/ormar/relations/relation_proxy.py index f658e57..29e4b97 100644 --- a/ormar/relations/relation_proxy.py +++ b/ormar/relations/relation_proxy.py @@ -6,7 +6,7 @@ from ormar.relations.querysetproxy import QuerysetProxy if TYPE_CHECKING: # pragma no cover from ormar import Model - from ormar.relations import Relation, register_missing_relation + from ormar.relations import Relation from ormar.queryset import QuerySet @@ -33,8 +33,8 @@ class RelationProxy(list): def _check_if_queryset_is_initialized(self) -> bool: return ( - hasattr(self.queryset_proxy, "queryset") - and self.queryset_proxy.queryset is not None + hasattr(self.queryset_proxy, "queryset") + and self.queryset_proxy.queryset is not None ) def _set_queryset(self) -> "QuerySet": @@ -48,8 +48,8 @@ class RelationProxy(list): kwargs = {f"{owner_table}__{pkname}": pk_value} queryset = ( ormar.QuerySet(model_cls=self.relation.to) - .select_related(owner_table) - .filter(**kwargs) + .select_related(owner_table) + .filter(**kwargs) ) return queryset @@ -72,6 +72,6 @@ class RelationProxy(list): if self.relation._type == ormar.RelationType.MULTIPLE: await self.queryset_proxy.create_through_instance(item) rel_name = item.resolve_relation_name(item, self._owner) - if not rel_name in item._orm: + if rel_name not in item._orm: item._orm._add_relation(item.Meta.model_fields[rel_name]) setattr(item, rel_name, self._owner) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index f169c30..b48bb3f 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -15,10 +15,10 @@ class Child(ormar.Model): metadata = metadata database = database - id: ormar.Integer(name='child_id', primary_key=True) - first_name: ormar.String(name='fname', max_length=100) - last_name: ormar.String(name='lname', max_length=100) - born_year: ormar.Integer(name='year_born', nullable=True) + id: ormar.Integer(name="child_id", primary_key=True) + first_name: ormar.String(name="fname", max_length=100) + last_name: ormar.String(name="lname", max_length=100) + born_year: ormar.Integer(name="year_born", nullable=True) class ArtistChildren(ormar.Model): @@ -34,10 +34,10 @@ class Artist(ormar.Model): metadata = metadata database = database - id: ormar.Integer(name='artist_id', primary_key=True) - first_name: ormar.String(name='fname', max_length=100) - last_name: ormar.String(name='lname', max_length=100) - born_year: ormar.Integer(name='year') + id: ormar.Integer(name="artist_id", primary_key=True) + first_name: ormar.String(name="fname", max_length=100) + last_name: ormar.String(name="lname", max_length=100) + born_year: ormar.Integer(name="year") children: ormar.ManyToMany(Child, through=ArtistChildren) @@ -47,9 +47,9 @@ class Album(ormar.Model): metadata = metadata database = database - id: ormar.Integer(name='album_id', primary_key=True) - name: ormar.String(name='album_name', max_length=100) - artist: ormar.ForeignKey(Artist, name='artist_id') + id: ormar.Integer(name="album_id", primary_key=True) + name: ormar.String(name="album_name", max_length=100) + artist: ormar.ForeignKey(Artist, name="artist_id") @pytest.fixture(autouse=True, scope="module") @@ -62,70 +62,87 @@ def create_test_database(): def test_table_structure(): - assert 'album_id' in [x.name for x in Album.Meta.table.columns] - assert 'album_name' in [x.name for x in Album.Meta.table.columns] - assert 'fname' in [x.name for x in Artist.Meta.table.columns] - assert 'lname' in [x.name for x in Artist.Meta.table.columns] - assert 'year' in [x.name for x in Artist.Meta.table.columns] + assert "album_id" in [x.name for x in Album.Meta.table.columns] + assert "album_name" in [x.name for x in Album.Meta.table.columns] + assert "fname" in [x.name for x in Artist.Meta.table.columns] + assert "lname" in [x.name for x in Artist.Meta.table.columns] + assert "year" in [x.name for x in Artist.Meta.table.columns] @pytest.mark.asyncio async def test_working_with_aliases(): async with database: async with database.transaction(force_rollback=True): - artist = await Artist.objects.create(first_name='Ted', last_name='Mosbey', born_year=1975) + artist = await Artist.objects.create( + first_name="Ted", last_name="Mosbey", born_year=1975 + ) await Album.objects.create(name="Aunt Robin", artist=artist) - await artist.children.create(first_name='Son', last_name='1', born_year=1990) - await artist.children.create(first_name='Son', last_name='2', born_year=1995) + await artist.children.create( + first_name="Son", last_name="1", born_year=1990 + ) + await artist.children.create( + first_name="Son", last_name="2", born_year=1995 + ) - album = await Album.objects.select_related('artist').first() - assert album.artist.last_name == 'Mosbey' + album = await Album.objects.select_related("artist").first() + assert album.artist.last_name == "Mosbey" assert album.artist.id is not None - assert album.artist.first_name == 'Ted' + assert album.artist.first_name == "Ted" assert album.artist.born_year == 1975 - assert album.name == 'Aunt Robin' + assert album.name == "Aunt Robin" - artist = await Artist.objects.select_related('children').get() + artist = await Artist.objects.select_related("children").get() assert len(artist.children) == 2 - assert artist.children[0].first_name == 'Son' - assert artist.children[1].last_name == '2' + assert artist.children[0].first_name == "Son" + assert artist.children[1].last_name == "2" - await artist.update(last_name='Bundy') + await artist.update(last_name="Bundy") await Artist.objects.filter(pk=artist.pk).update(born_year=1974) - artist = await Artist.objects.select_related('children').get() - assert artist.last_name == 'Bundy' + artist = await Artist.objects.select_related("children").get() + assert artist.last_name == "Bundy" assert artist.born_year == 1974 - artist = await Artist.objects.select_related('children').fields( - ['first_name', 'last_name', 'born_year', 'child__first_name', 'child__last_name']).get() + artist = ( + await Artist.objects.select_related("children") + .fields( + [ + "first_name", + "last_name", + "born_year", + "child__first_name", + "child__last_name", + ] + ) + .get() + ) assert artist.children[0].born_year is None @pytest.mark.asyncio async def test_bulk_operations_and_fields(): async with database: - d1 = Child(first_name='Daughter', last_name='1', born_year=1990) - d2 = Child(first_name='Daughter', last_name='2', born_year=1991) + d1 = Child(first_name="Daughter", last_name="1", born_year=1990) + d2 = Child(first_name="Daughter", last_name="2", born_year=1991) await Child.objects.bulk_create([d1, d2]) - children = await Child.objects.filter(first_name='Daughter').all() + children = await Child.objects.filter(first_name="Daughter").all() assert len(children) == 2 - assert children[0].last_name == '1' + assert children[0].last_name == "1" for child in children: child.born_year = child.born_year - 100 await Child.objects.bulk_update(children) - children = await Child.objects.filter(first_name='Daughter').all() + children = await Child.objects.filter(first_name="Daughter").all() assert len(children) == 2 assert children[0].born_year == 1890 - children = await Child.objects.fields(['first_name', 'last_name']).all() + children = await Child.objects.fields(["first_name", "last_name"]).all() assert len(children) == 2 for child in children: assert child.born_year is None @@ -140,17 +157,21 @@ async def test_bulk_operations_and_fields(): async def test_working_with_aliases_get_or_create(): async with database: async with database.transaction(force_rollback=True): - artist = await Artist.objects.get_or_create(first_name='Teddy', last_name='Bear', born_year=2020) + artist = await Artist.objects.get_or_create( + first_name="Teddy", last_name="Bear", born_year=2020 + ) assert artist.pk is not None - artist2 = await Artist.objects.get_or_create(first_name='Teddy', last_name='Bear', born_year=2020) + artist2 = await Artist.objects.get_or_create( + first_name="Teddy", last_name="Bear", born_year=2020 + ) assert artist == artist2 art3 = artist2.dict() - art3['born_year'] = 2019 + art3["born_year"] = 2019 await Artist.objects.update_or_create(**art3) - artist3 = await Artist.objects.get(last_name='Bear') + artist3 = await Artist.objects.get(last_name="Bear") assert artist3.born_year == 2019 artists = await Artist.objects.all() diff --git a/tests/test_fastapi_docs.py b/tests/test_fastapi_docs.py index c6cd35b..030815f 100644 --- a/tests/test_fastapi_docs.py +++ b/tests/test_fastapi_docs.py @@ -44,7 +44,7 @@ class Category(ormar.Model): class ItemsXCategories(ormar.Model): class Meta(LocalMeta): - tablename = 'items_x_categories' + tablename = "items_x_categories" class Item(ormar.Model): @@ -96,9 +96,7 @@ def test_all_endpoints(): response = client.post("/categories/", json={"name": "test cat2"}) category2 = response.json() - response = client.post( - "/items/", json={"name": "test", "id": 1} - ) + response = client.post("/items/", json={"name": "test", "id": 1}) item = Item(**response.json()) assert item.pk is not None @@ -107,7 +105,7 @@ def test_all_endpoints(): ) item = Item(**response.json()) assert len(item.categories) == 1 - assert item.categories[0].name == 'test cat' + assert item.categories[0].name == "test cat" client.post( "/items/add_category/", json={"item": item.dict(), "category": category2} @@ -117,9 +115,21 @@ def test_all_endpoints(): items = [Item(**item) for item in response.json()] assert items[0] == item assert len(items[0].categories) == 2 - assert items[0].categories[0].name == 'test cat' - assert items[0].categories[1].name == 'test cat2' + assert items[0].categories[0].name == "test cat" + assert items[0].categories[1].name == "test cat2" response = client.get("/docs/") assert response.status_code == 200 - assert b'FastAPI - Swagger UI' in response.content + assert b"FastAPI - Swagger UI" in response.content + + +def test_schema_modification(): + schema = Item.schema() + assert schema["properties"]["categories"]["type"] == "array" + assert schema["properties"]["categories"]["title"] == "Categories" + + +def test_schema_gen(): + schema = app.openapi() + assert "Category" in schema["components"]["schemas"] + assert "Item" in schema["components"]["schemas"] diff --git a/tests/test_models.py b/tests/test_models.py index 26de3f1..47b5d50 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -437,4 +437,3 @@ async def test_start_and_end_filters(): users = await User.objects.filter(name__endswith="igo").all() assert len(users) == 2 - diff --git a/tests/test_more_reallife_fastapi.py b/tests/test_more_reallife_fastapi.py index 3a5e909..b538b5c 100644 --- a/tests/test_more_reallife_fastapi.py +++ b/tests/test_more_reallife_fastapi.py @@ -119,9 +119,7 @@ def test_all_endpoints(): items = response.json() assert len(items) == 0 - client.post( - "/items/", json={"name": "test_2", "id": 2, "category": category} - ) + client.post("/items/", json={"name": "test_2", "id": 2, "category": category}) response = client.get("/items/") items = response.json() assert len(items) == 1 @@ -132,4 +130,3 @@ def test_all_endpoints(): response = client.get("/docs/") assert response.status_code == 200 - From 8b37a141315983c416879353cb84b8b431d183b9 Mon Sep 17 00:00:00 2001 From: collerek Date: Tue, 27 Oct 2020 18:18:09 +0100 Subject: [PATCH 4/5] update docs and bump version --- docs/fastapi.md | 25 +++++++++++++++++++++++-- docs_src/fastapi/docs001.py | 4 +++- mkdocs.yml | 6 +++--- ormar/__init__.py | 2 +- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/fastapi.md b/docs/fastapi.md index 9ae1dad..d451fee 100644 --- a/docs/fastapi.md +++ b/docs/fastapi.md @@ -45,7 +45,7 @@ Those models will be used insted of pydantic ones. Define your desired endpoints, note how `ormar` models are used both as `response_model` and as a requests parameters. -```python hl_lines="50-77" +```python hl_lines="50-79" --8<-- "../docs_src/fastapi/docs001.py" ``` @@ -57,6 +57,23 @@ as `response_model` and as a requests parameters. ## Test the application +### Run fastapi + +If you want to run this script and play with fastapi swagger install uvicorn first + +`pip install uvicorn` + +And launch the fastapi. + +`uvicorn :app --reload` + +Now you can navigate to your browser (by default fastapi address is `127.0.0.1:8000/docs`) and play with the api. + +!!!info + You can read more about running fastapi in [fastapi][fastapi] docs. + +### Test with pytest + Here you have a sample test that will prove that everything works as intended. Be sure to create the tables first. If you are using pytest you can use a fixture. @@ -109,9 +126,13 @@ def test_all_endpoints(): assert len(items) == 0 ``` +!!!tip + If you want to see more test cases and how to test ormar/fastapi see [tests][tests] directory in the github repo + !!!info You can read more on testing fastapi in [fastapi][fastapi] docs. [fastapi]: https://fastapi.tiangolo.com/ [models]: ./models.md -[database initialization]: ../models/#database-initialization-migrations \ No newline at end of file +[database initialization]: ../models/#database-initialization-migrations +[tests]: https://github.com/collerek/ormar/tree/master/tests \ No newline at end of file diff --git a/docs_src/fastapi/docs001.py b/docs_src/fastapi/docs001.py index 30ca75f..475756a 100644 --- a/docs_src/fastapi/docs001.py +++ b/docs_src/fastapi/docs001.py @@ -72,6 +72,8 @@ async def get_item(item_id: int, item: Item): @app.delete("/items/{item_id}") -async def delete_item(item_id: int, item: Item): +async def delete_item(item_id: int, item: Item = None): + if item: + return {"deleted_rows": await item.delete()} item_db = await Item.objects.get(pk=item_id) return {"deleted_rows": await item_db.delete()} diff --git a/mkdocs.yml b/mkdocs.yml index a7b99e3..234f18d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,9 +12,9 @@ nav: - Release Notes: releases.md repo_name: collerek/ormar repo_url: https://github.com/collerek/ormar -google_analytics: - - UA-72514911-3 - - auto +#google_analytics: +# - UA-72514911-3 +# - auto theme: name: material highlightjs: true diff --git a/ormar/__init__.py b/ormar/__init__.py index 159fda5..4aefddc 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -28,7 +28,7 @@ class UndefinedType: # pragma no cover Undefined = UndefinedType() -__version__ = "0.3.8" +__version__ = "0.3.9" __all__ = [ "Integer", "BigInteger", From e4221d6d16dd73e081b57c4eac6bd71c86c815f3 Mon Sep 17 00:00:00 2001 From: collerek Date: Tue, 27 Oct 2020 18:22:06 +0100 Subject: [PATCH 5/5] update changelog --- docs/releases.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/releases.md b/docs/releases.md index c674fdb..75c68ae 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,3 +1,10 @@ +# 0.3.9 + +* Fix json schema generation as of [#19][#19] +* Fix for not initialized ManyToMany relations in fastapi copies of ormar.Models +* Update docs in regard of fastapi use +* Add tests to verify fastapi/docs proper generation + # 0.3.8 * Added possibility to provide alternative database column names with name parameter to all fields. @@ -42,4 +49,7 @@ Add queryset level methods # 0.3.0 -* Added ManyToMany field and support for many to many relations \ No newline at end of file +* Added ManyToMany field and support for many to many relations + + +[#19]: https://github.com/collerek/ormar/issues/19 \ No newline at end of file