diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 813e192..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "python.linting.flake8Enabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black" -} \ No newline at end of file diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 108c485..0000000 --- a/mypy.ini +++ /dev/null @@ -1,10 +0,0 @@ -[mypy] -python_version = 3.8 -plugins = pydantic.mypy - -[mypy-sqlalchemy.*] -ignore_missing_imports = True - -[mypy-tests.test_model_definition.*] -ignore_errors = True - diff --git a/ormar/fields/sqlalchemy_encrypted.py b/ormar/fields/sqlalchemy_encrypted.py index ff5ef29..97769ba 100644 --- a/ormar/fields/sqlalchemy_encrypted.py +++ b/ormar/fields/sqlalchemy_encrypted.py @@ -6,7 +6,7 @@ from typing import Any, Callable, Optional, TYPE_CHECKING, Type, Union import sqlalchemy.types as types from pydantic.utils import lenient_issubclass -from sqlalchemy.engine.default import DefaultDialect +from sqlalchemy.engine import Dialect import ormar # noqa: I100, I202 from ormar import ModelDefinitionError # noqa: I202, I100 @@ -146,14 +146,14 @@ class EncryptedString(types.TypeDecorator): def __repr__(self) -> str: # pragma: nocover return "TEXT()" - def load_dialect_impl(self, dialect: DefaultDialect) -> Any: + def load_dialect_impl(self, dialect: Dialect) -> Any: return dialect.type_descriptor(types.TEXT()) def _refresh(self) -> None: key = self._key() if callable(self._key) else self._key self.backend._refresh(key) - def process_bind_param(self, value: Any, dialect: DefaultDialect) -> Optional[str]: + def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]: if value is None: return value self._refresh() @@ -167,7 +167,7 @@ class EncryptedString(types.TypeDecorator): encrypted_value = self.backend.encrypt(value) return encrypted_value - def process_result_value(self, value: Any, dialect: DefaultDialect) -> Any: + def process_result_value(self, value: Any, dialect: Dialect) -> Any: if value is None: return value self._refresh() diff --git a/ormar/fields/sqlalchemy_uuid.py b/ormar/fields/sqlalchemy_uuid.py index 826c0c8..f2a8f99 100644 --- a/ormar/fields/sqlalchemy_uuid.py +++ b/ormar/fields/sqlalchemy_uuid.py @@ -2,7 +2,7 @@ import uuid from typing import Any, Optional from sqlalchemy import CHAR -from sqlalchemy.engine.default import DefaultDialect +from sqlalchemy.engine import Dialect from sqlalchemy.types import TypeDecorator @@ -25,22 +25,20 @@ class UUID(TypeDecorator): return "CHAR(36)" return "CHAR(32)" - def load_dialect_impl(self, dialect: DefaultDialect) -> Any: + def load_dialect_impl(self, dialect: Dialect) -> Any: return ( dialect.type_descriptor(CHAR(36)) if self.uuid_format == "string" else dialect.type_descriptor(CHAR(32)) ) - def process_bind_param( - self, value: uuid.UUID, dialect: DefaultDialect - ) -> Optional[str]: + def process_bind_param(self, value: uuid.UUID, dialect: Dialect) -> Optional[str]: if value is None: return value return str(value) if self.uuid_format == "string" else "%.32x" % value.int def process_result_value( - self, value: Optional[str], dialect: DefaultDialect + self, value: Optional[str], dialect: Dialect ) -> Optional[uuid.UUID]: if value is None: return value diff --git a/ormar/models/helpers/validation.py b/ormar/models/helpers/validation.py index 01ebb9e..11db796 100644 --- a/ormar/models/helpers/validation.py +++ b/ormar/models/helpers/validation.py @@ -64,8 +64,11 @@ def convert_choices_if_needed( # noqa: CCR001 value = value.isoformat() if not isinstance(value, str) else value choices = [o.isoformat() for o in field.choices] elif field.__type__ == pydantic.Json: - value = json.dumps(value) if not isinstance(value, str) else value + value = ( + json.dumps(value) if not isinstance(value, str) else re_dump_value(value) + ) value = value.decode("utf-8") if isinstance(value, bytes) else value + choices = [re_dump_value(x) for x in field.choices] elif field.__type__ == uuid.UUID: value = str(value) if not isinstance(value, str) else value choices = [str(o) for o in field.choices] @@ -86,6 +89,21 @@ def convert_choices_if_needed( # noqa: CCR001 return value, choices +def re_dump_value(value: str) -> str: + """ + Rw-dumps choices due to different string representation in orjson and json + :param value: string to re-dump + :type value: str + :return: re-dumped choices + :rtype: List[str] + """ + try: + result: Union[str, bytes] = json.dumps(json.loads(value)) + except json.JSONDecodeError: + result = value + return result.decode("utf-8") if isinstance(result, bytes) else result + + def validate_choices(field: "BaseField", value: Any) -> None: """ Validates if given value is in provided choices. diff --git a/ormar/models/mixins/save_mixin.py b/ormar/models/mixins/save_mixin.py index c092a90..d1769f9 100644 --- a/ormar/models/mixins/save_mixin.py +++ b/ormar/models/mixins/save_mixin.py @@ -1,5 +1,15 @@ import uuid -from typing import Callable, Collection, Dict, List, Optional, Set, TYPE_CHECKING, cast +from typing import ( + Any, + Callable, + Collection, + Dict, + List, + Optional, + Set, + TYPE_CHECKING, + cast, +) import ormar from ormar.exceptions import ModelPersistenceError @@ -93,7 +103,7 @@ class SavePrepareMixin(RelationMixin, AliasMixin): if field.__type__ == uuid.UUID and name in model_dict: parsers = {"string": lambda x: str(x), "hex": lambda x: "%.32x" % x.int} uuid_format = field.column_type.uuid_format - parser = parsers.get(uuid_format, lambda x: x) + parser: Callable[..., Any] = parsers.get(uuid_format, lambda x: x) model_dict[name] = parser(model_dict[name]) return model_dict diff --git a/ormar/models/model_row.py b/ormar/models/model_row.py index 38d0e62..df90368 100644 --- a/ormar/models/model_row.py +++ b/ormar/models/model_row.py @@ -1,7 +1,7 @@ from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, Union, cast try: - from sqlalchemy.engine.result import ResultProxy + from sqlalchemy.engine.result import ResultProxy # type: ignore except ImportError: # pragma: no cover from sqlalchemy.engine.result import Row as ResultProxy # type: ignore diff --git a/ormar/queryset/query.py b/ormar/queryset/query.py index 3cfe84c..ddee8ad 100644 --- a/ormar/queryset/query.py +++ b/ormar/queryset/query.py @@ -1,8 +1,9 @@ from collections import OrderedDict -from typing import List, Optional, TYPE_CHECKING, Tuple, Type +from typing import List, Optional, TYPE_CHECKING, Tuple, Type, Union import sqlalchemy -from sqlalchemy import text +from sqlalchemy import Table, text +from sqlalchemy.sql import Join import ormar # noqa I100 from ormar.models.helpers.models import group_related_list @@ -41,7 +42,7 @@ class Query: self.used_aliases: List[str] = [] - self.select_from: List[str] = [] + self.select_from: Union[Join, Table, List[str]] = [] self.columns = [sqlalchemy.Column] self.order_columns = order_bys self.sorted_orders: OrderedDict[OrderAction, text] = OrderedDict() diff --git a/pyproject.toml b/pyproject.toml index 344d9a6..c1cbe65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,3 +130,20 @@ dev = [ [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.mypy] +# TODO: Enable mypy plugin after pydantic release supporting toml file +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_calls = false +disallow_untyped_defs = false +disallow_incomplete_defs = false + +[[tool.mypy.overrides]] +module = ["sqlalchemy.*", "asyncpg"] +ignore_missing_imports = true + diff --git a/tests/test_fastapi/test_choices_schema.py b/tests/test_fastapi/test_choices_schema.py index 7639ee5..86336c5 100644 --- a/tests/test_fastapi/test_choices_schema.py +++ b/tests/test_fastapi/test_choices_schema.py @@ -1,7 +1,6 @@ import datetime import decimal import uuid -from base64 import b64encode from enum import Enum import databases @@ -60,7 +59,7 @@ class Organisation(ormar.Model): random_decimal: decimal.Decimal = ormar.Decimal( scale=2, precision=4, choices=[decimal.Decimal(12.4), decimal.Decimal(58.2)] ) - random_json: pydantic.Json = ormar.JSON(choices=["aa", '{"aa":"bb"}']) + random_json: pydantic.Json = ormar.JSON(choices=["aa", '{"aa": "bb"}']) random_uuid: uuid.UUID = ormar.UUID(choices=[uuid1, uuid2]) enum_string: str = ormar.String(max_length=100, choices=list(EnumTest)) blob_col: bytes = ormar.LargeBinary(max_length=100000, choices=[blob, blob2]) @@ -116,7 +115,7 @@ def test_all_endpoints(): "expire_datetime": "2022-05-01T12:30:00", "random_val": 3.5, "random_decimal": 12.4, - "random_json": '{"aa":"bb"}', + "random_json": '{"aa": "bb"}', "random_uuid": str(uuid1), "enum_string": EnumTest.val1.value, "blob_col": blob.decode("utf-8"), diff --git a/tests/test_fastapi/test_more_reallife_fastapi.py b/tests/test_fastapi/test_more_reallife_fastapi.py index 45dab4a..21badbd 100644 --- a/tests/test_fastapi/test_more_reallife_fastapi.py +++ b/tests/test_fastapi/test_more_reallife_fastapi.py @@ -131,10 +131,6 @@ def test_all_endpoints(): assert items[0].name == "New name" assert items[0].category.name is None - loop = asyncio.get_event_loop() - loop.run_until_complete(items[0].category.load()) - assert items[0].category.name is not None - response = client.get(f"/items/{item.pk}") new_item = Item(**response.json()) assert new_item == item diff --git a/tests/test_model_definition/test_create_uses_init_for_consistency.py b/tests/test_model_definition/test_create_uses_init_for_consistency.py index c6fedba..5440527 100644 --- a/tests/test_model_definition/test_create_uses_init_for_consistency.py +++ b/tests/test_model_definition/test_create_uses_init_for_consistency.py @@ -27,7 +27,7 @@ class Mol(ormar.Model): class Meta(BaseMeta): tablename = "mols" - id: str = ormar.UUID(primary_key=True, index=True, uuid_format="hex") + id: uuid.UUID = ormar.UUID(primary_key=True, index=True, uuid_format="hex") smiles: str = ormar.String(nullable=False, unique=True, max_length=256) def __init__(self, **kwargs): diff --git a/tests/test_model_definition/test_overwriting_pydantic_field_type.py b/tests/test_model_definition/test_overwriting_pydantic_field_type.py index 0b77592..bb015a3 100644 --- a/tests/test_model_definition/test_overwriting_pydantic_field_type.py +++ b/tests/test_model_definition/test_overwriting_pydantic_field_type.py @@ -19,9 +19,9 @@ class OverwriteTest(ormar.Model): database = database id: int = ormar.Integer(primary_key=True) - my_int: str = ormar.Integer(overwrite_pydantic_type=PositiveInt) + my_int: int = ormar.Integer(overwrite_pydantic_type=PositiveInt) constraint_dict: Json = ormar.JSON( - overwrite_pydantic_type=Optional[Json[Dict[str, int]]] + overwrite_pydantic_type=Optional[Json[Dict[str, int]]] # type: ignore ) diff --git a/tests/test_model_definition/test_pydantic_fields.py b/tests/test_model_definition/test_pydantic_fields.py index de5fb24..12d5753 100644 --- a/tests/test_model_definition/test_pydantic_fields.py +++ b/tests/test_model_definition/test_pydantic_fields.py @@ -24,7 +24,7 @@ class ModelTest(ormar.Model): id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) - url: HttpUrl = "https://www.example.com" + url: HttpUrl = "https://www.example.com" # type: ignore number: Optional[PaymentCardNumber] @@ -47,7 +47,7 @@ class ModelTest2(ormar.Model): id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) - url: HttpUrl = "https://www.example2.com" + url: HttpUrl = "https://www.example2.com" # type: ignore number: PaymentCardNumber = Field(default_factory=get_number) @@ -67,7 +67,7 @@ class ModelTest3(ormar.Model): id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) - url: HttpUrl = "https://www.example3.com" + url: HttpUrl = "https://www.example3.com" # type: ignore number: PaymentCardNumber pydantic_test: PydanticTest