From 0e167dc89ff8287536fd5e7cd9006001b73178ae Mon Sep 17 00:00:00 2001 From: collerek Date: Wed, 26 Jan 2022 17:59:00 +0100 Subject: [PATCH] use existing encode_json to avoid code duplication, rename queryset customization param and move it to Meta, move docs to models from inheritance --- .flake8 | 2 +- docs/models/index.md | 38 +++++++++++++++++++ docs/models/inheritance.md | 34 ----------------- ormar/__init__.py | 2 +- ormar/decorators/signals.py | 2 +- ormar/models/descriptors/descriptors.py | 11 +----- ormar/models/helpers/models.py | 24 ++++++------ ormar/models/metaclass.py | 5 +-- ormar/models/mixins/save_mixin.py | 11 ++---- ormar/queryset/queryset.py | 6 +-- ormar/queryset/utils.py | 9 ----- ormar/signals/signal.py | 2 +- .../test_queryset_level_methods.py | 21 +++++----- tests/test_queries/test_utils.py | 19 ---------- 14 files changed, 74 insertions(+), 112 deletions(-) delete mode 100644 tests/test_queries/test_utils.py diff --git a/.flake8 b/.flake8 index abf40d7..59b6009 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -ignore = ANN101, ANN102, W503, S101, CFQ004 +ignore = ANN101, ANN102, W503, S101, CFQ004, S311 max-complexity = 8 max-line-length = 88 import-order-style = pycharm diff --git a/docs/models/index.md b/docs/models/index.md index 428b389..c99a71f 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -266,6 +266,44 @@ But for now you cannot change the ManyToMany column names as they go through oth --8<-- "../docs_src/models/docs010.py" ``` +## Overwriting the default QuerySet + +If you want to customize the queries run by ormar you can define your own queryset class (that extends the ormar `QuerySet`) in your model class, default one is simply the `QuerySet` + +You can provide a new class in `Meta` configuration of your class as `queryset_class` parameter. + +```python +import ormar +from ormar.queryset.queryset import QuerySet +from fastapi import HTTPException + + +class MyQuerySetClass(QuerySet): + + async def first_or_404(self, *args, **kwargs): + entity = await self.get_or_none(*args, **kwargs) + if entity is None: + # in fastapi or starlette + raise HTTPException(404) + + +class Book(ormar.Model): + + class Meta(ormar.ModelMeta): + metadata = metadata + database = database + tablename = "book" + queryset_class = MyQuerySetClass + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=32) + + +# when book not found, raise `404` in your view. +book = await Book.objects.first_or_404(name="123") + +``` + ### Type Hints & Legacy Before version 0.4.0 `ormar` supported only one way of defining `Fields` on a `Model` using python type hints as pydantic. diff --git a/docs/models/inheritance.md b/docs/models/inheritance.md index e4b565f..d0f4be6 100644 --- a/docs/models/inheritance.md +++ b/docs/models/inheritance.md @@ -571,37 +571,3 @@ class Category(CreateDateFieldsModel, AuditCreateModel): ``` That way you can inherit from both create and update classes if needed, and only one of them otherwise. - -## __queryset_cls__ - -You can define your own queryset_class(extends the `Queryset`) in your model class, default is `QuerySet` - -```python -import ormar -from ormar.queryset.queryset import QuerySet -from fastapi import HTTPException - - -class MyQuerySetClass(QuerySet): - - async def first_or_404(self, *args, **kwargs): - entity = await self.get_or_none(*args, **kwargs) - if entity is None: - # in fastapi or starlette - raise HTTPException(404) - -class Book(ormar.Model): - class Meta: - metadata = metadata - database = database - tablename = "book" - - __queryset_cls__ = MyQuerySetClass - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=32) - - # when book not found, raise `404` in your view. - book = await Book.objects.first_or_404(name="123") - -``` diff --git a/ormar/__init__.py b/ormar/__init__.py index fd52f2b..4187d60 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -25,12 +25,12 @@ except ImportError: # pragma: no cover from importlib_metadata import version # type: ignore from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100 from ormar.decorators import ( # noqa: I100 + post_bulk_update, post_delete, post_relation_add, post_relation_remove, post_save, post_update, - post_bulk_update, pre_delete, pre_relation_add, pre_relation_remove, diff --git a/ormar/decorators/signals.py b/ormar/decorators/signals.py index f70b5b7..3e10c1d 100644 --- a/ormar/decorators/signals.py +++ b/ormar/decorators/signals.py @@ -1,4 +1,4 @@ -from typing import Callable, List, Type, TYPE_CHECKING, Union +from typing import Callable, List, TYPE_CHECKING, Type, Union if TYPE_CHECKING: # pragma: no cover from ormar import Model diff --git a/ormar/models/descriptors/descriptors.py b/ormar/models/descriptors/descriptors.py index 112f49e..35f4194 100644 --- a/ormar/models/descriptors/descriptors.py +++ b/ormar/models/descriptors/descriptors.py @@ -1,12 +1,7 @@ import base64 from typing import Any, TYPE_CHECKING, Type -from ormar.queryset.utils import to_str - -try: - import orjson as json -except ImportError: # pragma: no cover - import json # type: ignore +from ormar.fields.parsers import encode_json if TYPE_CHECKING: # pragma: no cover from ormar import Model @@ -42,9 +37,7 @@ class JsonDescriptor: return value def __set__(self, instance: "Model", value: Any) -> None: - if not isinstance(value, str): - value = json.dumps(value) - value = to_str(value) + value = encode_json(value) instance._internal_set(self.name, value) instance.set_save_status(False) diff --git a/ormar/models/helpers/models.py b/ormar/models/helpers/models.py index aa32fee..4f55aeb 100644 --- a/ormar/models/helpers/models.py +++ b/ormar/models/helpers/models.py @@ -47,18 +47,18 @@ def populate_default_options_values( # noqa: CCR001 :param model_fields: dict of model fields :type model_fields: Union[Dict[str, type], Dict] """ - if not hasattr(new_model.Meta, "constraints"): - new_model.Meta.constraints = [] - if not hasattr(new_model.Meta, "model_fields"): - new_model.Meta.model_fields = model_fields - if not hasattr(new_model.Meta, "abstract"): - new_model.Meta.abstract = False - if not hasattr(new_model.Meta, "extra"): - new_model.Meta.extra = Extra.forbid - if not hasattr(new_model.Meta, "orders_by"): - new_model.Meta.orders_by = [] - if not hasattr(new_model.Meta, "exclude_parent_fields"): - new_model.Meta.exclude_parent_fields = [] + defaults = { + "queryset_class": ormar.QuerySet, + "constraints": [], + "model_fields": model_fields, + "abstract": False, + "extra": Extra.forbid, + "orders_by": [], + "exclude_parent_fields": [], + } + for key, value in defaults.items(): + if not hasattr(new_model.Meta, key): + setattr(new_model.Meta, key, value) if any( is_field_an_forward_ref(field) for field in new_model.Meta.model_fields.values() diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 1e51252..bc22727 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -85,6 +85,7 @@ class ModelMeta: orders_by: List[str] exclude_parent_fields: List[str] extra: Extra + queryset_class: Type[QuerySet] def add_cached_properties(new_model: Type["Model"]) -> None: @@ -614,8 +615,6 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): return new_model - __queryset_cls__ = QuerySet - @property def objects(cls: Type["T"]) -> "QuerySet[T]": # type: ignore if cls.Meta.requires_ref_update: @@ -624,7 +623,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): f"ForwardRefs. \nBefore using the model you " f"need to call update_forward_refs()." ) - return cls.__queryset_cls__(model_cls=cls) + return cls.Meta.queryset_class(model_cls=cls) def __getattr__(self, item: str) -> Any: """ diff --git a/ormar/models/mixins/save_mixin.py b/ormar/models/mixins/save_mixin.py index c0868fb..7617a26 100644 --- a/ormar/models/mixins/save_mixin.py +++ b/ormar/models/mixins/save_mixin.py @@ -12,18 +12,13 @@ from typing import ( cast, ) -try: - import orjson as json -except ImportError: # pragma: no cover - import json # type: ignore - import pydantic import ormar # noqa: I100, I202 from ormar.exceptions import ModelPersistenceError +from ormar.fields.parsers import encode_json from ormar.models.mixins import AliasMixin from ormar.models.mixins.relation_mixin import RelationMixin -from ormar.queryset.utils import to_str if TYPE_CHECKING: # pragma: no cover from ormar import ForeignKeyField, Model @@ -208,8 +203,8 @@ class SavePrepareMixin(RelationMixin, AliasMixin): :rtype: Dict """ for key, value in model_dict.items(): - if key in cls._json_fields and not isinstance(value, str): - model_dict[key] = to_str(json.dumps(value)) + if key in cls._json_fields: + model_dict[key] = encode_json(value) return model_dict @classmethod diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index c576547..beae8d7 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -30,9 +30,9 @@ except ImportError: # pragma: no cover import ormar # noqa I100 from ormar import MultipleMatches, NoMatch from ormar.exceptions import ( + ModelListEmptyError, ModelPersistenceError, QueryDefinitionError, - ModelListEmptyError, ) from ormar.queryset import FieldAccessor, FilterQuery, SelectAction from ormar.queryset.actions.order_action import OrderAction @@ -1065,8 +1065,8 @@ class QuerySet(Generic[T]): :type objects: List[Model] """ ready_objects = [obj.prepare_model_to_save(obj.dict()) for obj in objects] - - # don't use execute_many, as in databases it's executed in a loop + + # don't use execute_many, as in databases it's executed in a loop # instead of using execute_many from drivers expr = self.table.insert().values(ready_objects) await self.database.execute(expr) diff --git a/ormar/queryset/utils.py b/ormar/queryset/utils.py index b86c1c4..4ebe07c 100644 --- a/ormar/queryset/utils.py +++ b/ormar/queryset/utils.py @@ -17,15 +17,6 @@ if TYPE_CHECKING: # pragma no cover from ormar import Model, BaseField -def to_str(val: Union[bytes, str, int]) -> str: - """ convert bytes to str simply """ - if isinstance(val, bytes): - return val.decode("utf-8") - elif isinstance(val, str): - return val - return str(val) - - def check_node_not_dict_or_not_last_node( part: str, is_last: bool, current_level: Any ) -> bool: diff --git a/ormar/signals/signal.py b/ormar/signals/signal.py index 2b76787..868b494 100644 --- a/ormar/signals/signal.py +++ b/ormar/signals/signal.py @@ -1,6 +1,6 @@ import asyncio import inspect -from typing import Any, Callable, Dict, Tuple, Type, TYPE_CHECKING, Union +from typing import Any, Callable, Dict, TYPE_CHECKING, Tuple, Type, Union from ormar.exceptions import SignalDefinitionError diff --git a/tests/test_queries/test_queryset_level_methods.py b/tests/test_queries/test_queryset_level_methods.py index 89f69ef..7176010 100644 --- a/tests/test_queries/test_queryset_level_methods.py +++ b/tests/test_queries/test_queryset_level_methods.py @@ -78,22 +78,21 @@ class ItemConfig(ormar.Model): pairs: pydantic.Json = ormar.JSON(default=["2", "3"]) +class QuerySetCls(QuerySet): + async def first_or_404(self, *args, **kwargs): + entity = await self.get_or_none(*args, **kwargs) + if not entity: + # maybe HTTPException in fastapi + raise ValueError("customer not found") + return entity + + class Customer(ormar.Model): class Meta: metadata = metadata database = database tablename = "customer" - - class QuerySetCls(QuerySet): - - async def first_or_404(self, *args, **kwargs): - entity = await self.get_or_none(*args, **kwargs) - if not entity: - # maybe HTTPException in fastapi - raise ValueError("customer not found") - return entity - - __queryset_cls__ = QuerySetCls + queryset_class = QuerySetCls id: Optional[int] = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=32) diff --git a/tests/test_queries/test_utils.py b/tests/test_queries/test_utils.py deleted file mode 100644 index 2ed2dee..0000000 --- a/tests/test_queries/test_utils.py +++ /dev/null @@ -1,19 +0,0 @@ -import orjson -import json - -from ormar.queryset.utils import to_str - - -def test_to_str(): - expected_str = "[]" - val = orjson.dumps([]) - assert expected_str == to_str(val) - - val = json.dumps([]) - assert expected_str == to_str(val) - - expected_bytes = expected_str.encode() - assert isinstance(expected_bytes, bytes) - - assert isinstance(to_str(expected_bytes), str) - assert "1" == to_str(1)