From 0d0c3ca9236ada2b561c8a6c9cedd88281e69ec0 Mon Sep 17 00:00:00 2001 From: Grigi Date: Wed, 11 Sep 2019 12:46:11 +0200 Subject: [PATCH 01/10] Drop py3.5 support --- .travis.yml | 5 +- README.rst | 2 +- docs/CONTRIBUTING.rst | 7 ++- docs/index.rst | 2 +- examples/aggregation.py | 2 +- examples/quart/models.py | 4 +- examples/relations.py | 2 +- examples/sanic/models.py | 2 +- examples/starlette/models.py | 2 +- requirements-dev.in | 7 +-- requirements-dev.txt | 25 +++++---- setup.py | 7 +-- tortoise/__init__.py | 41 ++++++-------- tortoise/aggregation.py | 2 +- tortoise/backends/asyncpg/client.py | 14 ++--- tortoise/backends/asyncpg/schema_generator.py | 6 +-- tortoise/backends/base/client.py | 2 +- tortoise/backends/base/config_generator.py | 2 +- tortoise/backends/base/executor.py | 8 +-- tortoise/backends/mysql/client.py | 8 ++- tortoise/backends/mysql/executor.py | 18 +++---- tortoise/backends/mysql/schema_generator.py | 18 +++---- tortoise/backends/sqlite/client.py | 6 +-- tortoise/backends/sqlite/executor.py | 2 +- tortoise/backends/sqlite/schema_generator.py | 6 +-- tortoise/contrib/pylint/__init__.py | 2 +- tortoise/contrib/test/__init__.py | 6 +-- tortoise/fields.py | 14 ++--- tortoise/filters.py | 54 +++++++++---------- tortoise/models.py | 29 +++++----- tortoise/query_utils.py | 12 ++--- tortoise/queryset.py | 36 +++++-------- tortoise/tests/test_aggregation.py | 2 +- tortoise/tests/test_describe_model.py | 9 ---- tortoise/tests/test_generate_schema.py | 9 +--- tortoise/tests/test_init.py | 7 ++- tortoise/tests/test_model_methods.py | 4 +- tortoise/tests/test_reconnect.py | 10 ++-- tortoise/tests/test_relations.py | 4 +- tortoise/tests/test_two_databases.py | 2 +- tortoise/tests/testfields.py | 6 +-- tox.ini | 2 +- 42 files changed, 163 insertions(+), 245 deletions(-) diff --git a/.travis.yml b/.travis.yml index cf58849da..741a7055e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - 3.6 - - 3.5 - 3.7 env: global: @@ -20,11 +19,11 @@ matrix: env: TORTOISE_TEST_DB="mysql://root:@127.0.0.1:3306/test_{}" install: pip install -r requirements-pypy.txt script: green - - python: 3.6 + - python: 3.7 env: TEST_RUNNER=py.test install: pip install -r requirements-dev.txt script: py.test - - python: 3.6 + - python: 3.7 env: TEST_RUNNER=nose2 install: pip install -r requirements-dev.txt script: "nose2 --plugin tortoise.contrib.test.nose2 --db-module tortoise.tests.testmodels --db-url sqlite://:memory:" diff --git a/README.rst b/README.rst index 0565a7a50..94119017a 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ You can find docs at `ReadTheDocs `_ and it will have possible breakage clearly documented. -Tortoise ORM is supported on CPython >= 3.5.3 for SQLite, MySQL and PostgreSQL, and PyPy3.6 >= 7.1 for SQLite and MySQL only. +Tortoise ORM is supported on CPython >= 3.6 for SQLite, MySQL and PostgreSQL, and PyPy3.6 >= 7.1 for SQLite and MySQL only. Why was Tortoise ORM built? --------------------------- diff --git a/docs/CONTRIBUTING.rst b/docs/CONTRIBUTING.rst index e33e8b48f..b22ef3461 100644 --- a/docs/CONTRIBUTING.rst +++ b/docs/CONTRIBUTING.rst @@ -81,7 +81,7 @@ As this is a value that is different for different people, we have settled on: * Model/QuerySet usage should be explicit and concise. * Keep it simple, as simple code not only often runs faster, but has less bugs too. * Correctness > Ease-Of-Use > Performance > Maintenance -* Test everything. (Currently our test suite is not yet mature) +* Test everything. * Only do performance/memory optimisation when you have a repeatable benchmark to measure with. @@ -97,9 +97,8 @@ Tortoise ORM follows a the following agreed upon style: * Always try to separate out terms clearly rather than concatenate words directly: * ``some_purpose`` instead of ``somepurpose`` * ``SomePurpose`` instead of ``Somepurpose`` -* Keep in mind the targeted Python versions of ``>=3.5.3``: - * Don't use f-strings - * Stick to comment-style variable type annotations +* Keep in mind the targeted Python versions of ``>=3.6``: + * Do use f-strings * Please try and provide type annotations where you can, it will improve auto-completion in editors, and better static analysis. diff --git a/docs/index.rst b/docs/index.rst index a1bb6365f..28ccf3fc7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,7 @@ It's engraved in it's design that you are working not with just tables, you work Source & issue trackers are available at ``_ -Tortoise ORM is supported on CPython >= 3.5.3 for SQLite, MySQL and PostgreSQL, and PyPy3.5 >= 5.10 for SQLite and MySQL only. +Tortoise ORM is supported on CPython >= 3.6 for SQLite, MySQL and PostgreSQL, and PyPy3.6 >= 7.1 for SQLite and MySQL only. Introduction ============ diff --git a/examples/aggregation.py b/examples/aggregation.py index d622350b7..a71348910 100644 --- a/examples/aggregation.py +++ b/examples/aggregation.py @@ -42,7 +42,7 @@ async def run(): await event.save() participants = [] for i in range(2): - team = Team(name="Team {}".format(i + 1)) + team = Team(name=f"Team {(i + 1)}") await team.save() participants.append(team) await event.participants.add(participants[0], participants[1]) diff --git a/examples/quart/models.py b/examples/quart/models.py index 2bea0de24..452776ce3 100644 --- a/examples/quart/models.py +++ b/examples/quart/models.py @@ -6,7 +6,7 @@ class Users(Model): status = fields.CharField(20) def __str__(self): - return "User {}: {}".format(self.id, self.status) + return f"User {self.id}: {self.status}" class Workers(Model): @@ -14,4 +14,4 @@ class Workers(Model): status = fields.CharField(20) def __str__(self): - return "Worker {}: {}".format(self.id, self.status) + return f"Worker {self.id}: {self.status}" diff --git a/examples/relations.py b/examples/relations.py index c706c4b00..d0aa09e9d 100644 --- a/examples/relations.py +++ b/examples/relations.py @@ -49,7 +49,7 @@ async def run(): await event.save() participants = [] for i in range(2): - team = Team(name="Team {}".format(i + 1)) + team = Team(name=f"Team {(i + 1)}") await team.save() participants.append(team) await event.participants.add(participants[0], participants[1]) diff --git a/examples/sanic/models.py b/examples/sanic/models.py index c9a7e8d67..e94feaaad 100644 --- a/examples/sanic/models.py +++ b/examples/sanic/models.py @@ -6,4 +6,4 @@ class Users(Model): name = fields.CharField(50) def __str__(self): - return "User {}: {}".format(self.id, self.name) + return f"User {self.id}: {self.name}" diff --git a/examples/starlette/models.py b/examples/starlette/models.py index 847f64024..0950f7fdd 100644 --- a/examples/starlette/models.py +++ b/examples/starlette/models.py @@ -6,4 +6,4 @@ class Users(models.Model): username = fields.CharField(max_length=20) def __str__(self) -> str: - return "User {}: {}".format(self.id, self.username) + return f"User {self.id}: {self.username}" diff --git a/requirements-dev.in b/requirements-dev.in index 4ba20a72e..09fd04ea9 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -41,10 +41,7 @@ hypercorn;python_version>="3.7" quart;python_version>="3.7" # Sample integration - Sanic -httpcore;python_version>="3.6" -sanic;python_version>="3.6" -requests-async;python_version>="3.6" +sanic # Sample integration - Starlette -uvicorn==0.8.6; python_version>="3.6" -starlette;python_version>="3.6" +starlette diff --git a/requirements-dev.txt b/requirements-dev.txt index fe2dbf364..62f45ee73 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -25,7 +25,7 @@ certifi==2019.6.16 # via httpcore, requests cffi==1.12.3 # via cryptography chardet==3.0.4 # via httpcore, requests ciso8601==2.1.1 -click==7.0 # via black, pip-tools, quart, uvicorn +click==7.0 # via black, pip-tools, quart cloud-sptheme==1.9.4 colorama==0.4.1 # via green coverage==4.5.4 # via coveralls, green, nose2 @@ -40,16 +40,16 @@ flake8==3.7.8 # via flake8-isort gitdb2==2.0.5 # via gitpython gitpython==3.0.2 # via bandit green==3.0.0 -h11==0.8.1 # via httpcore, hypercorn, uvicorn, wsproto +h11==0.8.1 # via httpcore, hypercorn, wsproto h2==3.1.1 # via httpcore, hypercorn hpack==3.0.0 # via h2 -httpcore==0.3.0 ; python_version >= "3.6" -httptools==0.0.13 # via sanic, uvicorn +httpcore==0.3.0 # via requests-async +httptools==0.0.13 # via sanic hypercorn==0.8.2 ; python_version >= "3.7" hyperframe==5.2.0 # via h2 idna==2.8 # via httpcore, requests imagesize==1.1.0 # via sphinx -importlib-metadata==0.20 # via pluggy, pytest, tox +importlib-metadata==0.21 # via pluggy, pytest, tox isort==4.3.21 # via flake8-isort, pylint itsdangerous==1.1.0 # via quart jinja2==2.10.1 # via quart, sphinx @@ -66,7 +66,7 @@ packaging==19.1 # via pytest, sphinx, tox pbr==5.4.3 # via stevedore pip-tools==4.1.0 pkginfo==1.5.0.1 # via twine -pluggy==0.12.0 # via pytest, tox +pluggy==0.13.0 # via pytest, tox priority==1.3.0 # via hypercorn py==1.8.0 # via pytest, tox pycodestyle==2.5.0 # via flake8 @@ -82,11 +82,11 @@ pytz==2019.2 # via babel pyyaml==5.1.2 quart==0.10.0 ; python_version >= "3.7" readme-renderer==24.0 # via twine -requests-async==0.5.0 ; python_version >= "3.6" +requests-async==0.5.0 # via sanic requests-toolbelt==0.9.1 # via twine requests==2.22.0 # via coveralls, requests-async, requests-toolbelt, sphinx, twine rfc3986==1.3.2 # via httpcore -sanic==19.6.3 ; python_version >= "3.6" +sanic==19.6.3 six==1.12.0 # via astroid, bandit, bleach, cryptography, nose2, packaging, pip-tools, readme-renderer, sphinx, stevedore, tox smmap2==2.0.5 # via gitdb2 snowballstemmer==1.9.1 # via sphinx @@ -94,24 +94,23 @@ sortedcontainers==2.1.0 # via quart sphinx-autodoc-typehints==1.6.0 sphinx==1.8.5 sphinxcontrib-websupport==1.1.2 # via sphinx -starlette==0.12.9 ; python_version >= "3.6" +starlette==0.12.9 stevedore==1.31.0 # via bandit testfixtures==6.10.0 # via flake8-isort toml==0.10.0 # via black, hypercorn, tox tox==3.14.0 tqdm==4.35.0 # via twine -twine==1.13.0 +twine==1.14.0 typed-ast==1.4.0 # via astroid, mypy typing-extensions==3.7.4 # via hypercorn, mypy ujson==1.35 # via sanic unidecode==1.1.1 # via green urllib3==1.25.3 # via requests -uvicorn==0.8.6 ; python_version >= "3.6" -uvloop==0.12.2 # via sanic, uvicorn +uvloop==0.13.0 # via sanic virtualenv==16.7.5 # via tox wcwidth==0.1.7 # via pytest webencodings==0.5.1 # via bleach -websockets==7.0 # via sanic, uvicorn +websockets==7.0 # via sanic wrapt==1.11.2 # via astroid wsproto==0.15.0 ; python_version >= "3.7" zipp==0.6.0 # via importlib-metadata diff --git a/setup.py b/setup.py index 323793ba1..161da80b5 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,11 @@ # coding: utf8 import re import sys -import warnings from setuptools import find_packages, setup -if sys.version_info < (3, 5, 3): - raise RuntimeError("tortoise requires Python 3.5.3+") - if sys.version_info < (3, 6): - warnings.warn("Tortoise-ORM is soon going to require Python 3.6", DeprecationWarning) + raise RuntimeError("Tortoise-ORM requires Python >= 3.6") def version() -> str: @@ -51,7 +47,6 @@ def requirements() -> list: "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", diff --git a/tortoise/__init__.py b/tortoise/__init__.py index de5407fc0..28b7b3316 100644 --- a/tortoise/__init__.py +++ b/tortoise/__init__.py @@ -3,8 +3,6 @@ import json import logging import os -import sys -import warnings from copy import deepcopy from inspect import isclass from typing import Any, Coroutine, Dict, List, Optional, Tuple, Type, Union, cast @@ -27,9 +25,6 @@ logger = logging.getLogger("tortoise") -if sys.version_info < (3, 6): # pragma: nocoverage - warnings.warn("Tortoise-ORM is soon going to require Python 3.6", DeprecationWarning) - class Tortoise: apps = {} # type: Dict[str, Dict[str, Type[Model]]] @@ -116,7 +111,7 @@ def describe_model(cls, model: Type[Model], serializable: bool = True) -> dict: def _type_name(typ) -> str: if typ.__module__ == "builtins": return typ.__name__ - return "{}.{}".format(typ.__module__, typ.__name__) + return f"{typ.__module__}.{typ.__name__}" def model_name(typ: Type[Model]) -> str: name = typ._meta.table @@ -124,7 +119,7 @@ def model_name(typ: Type[Model]) -> str: for _name, _model in app.items(): # pragma: nobranch if typ == _model: name = _name - return "{}.{}".format(typ._meta.app, name) + return f"{typ._meta.app}.{name}" def type_name(typ: Any) -> Union[str, List[str]]: try: @@ -141,7 +136,7 @@ def default_name(default: Any) -> Optional[Union[int, float, str, bool]]: if isinstance(default, (int, float, str, bool, type(None))): return default if callable(default): - return "".format(default.__module__, default.__name__) + return f"" return str(default) def describe_field(name: str) -> dict: @@ -239,7 +234,7 @@ def describe_models( models.append(model) return { - "{}.{}".format(model._meta.app, model.__name__): cls.describe_model(model, serializable) + f"{model._meta.app}.{model.__name__}": cls.describe_model(model, serializable) for model in models } @@ -254,9 +249,7 @@ def get_related_model(related_app_name: str, related_model_name: str): return cls.apps[related_app_name][related_model_name] except KeyError: if related_app_name not in cls.apps: - raise ConfigurationError( - "No app with name '{}' registered.".format(related_app_name) - ) + raise ConfigurationError(f"No app with name '{related_app_name}' registered.") raise ConfigurationError( "No model with name '{}' registered in app '{}'.".format( related_model_name, related_app_name @@ -296,7 +289,7 @@ def split_reference(reference: str) -> Tuple[str, str]: related_app_name, related_model_name = split_reference(reference) related_model = get_related_model(related_app_name, related_model_name) - key_field = "{}_id".format(field) + key_field = f"{field}_id" key_fk_object = deepcopy(related_model._meta.pk) key_fk_object.pk = False key_fk_object.index = fk_object.index @@ -316,7 +309,7 @@ def split_reference(reference: str) -> Tuple[str, str]: fk_object.type = related_model backward_relation_name = fk_object.related_name if not backward_relation_name: - backward_relation_name = "{}s".format(model._meta.table) + backward_relation_name = f"{model._meta.table}s" if backward_relation_name in related_model._meta.fields: raise ConfigurationError( 'backward relation "{}" duplicates in model {}'.format( @@ -324,7 +317,7 @@ def split_reference(reference: str) -> Tuple[str, str]: ) ) fk_relation = fields.BackwardFKRelation( - model, "{}_id".format(field), fk_object.null, fk_object.description + model, f"{field}_id", fk_object.null, fk_object.description ) related_model._meta.add_field(backward_relation_name, fk_relation) @@ -335,9 +328,9 @@ def split_reference(reference: str) -> Tuple[str, str]: backward_key = m2m_object.backward_key if not backward_key: - backward_key = "{}_id".format(model._meta.table) + backward_key = f"{model._meta.table}_id" if backward_key == m2m_object.forward_key: - backward_key = "{}_rel_id".format(model._meta.table) + backward_key = f"{model._meta.table}_rel_id" m2m_object.backward_key = backward_key reference = m2m_object.model_name @@ -365,12 +358,10 @@ def split_reference(reference: str) -> Tuple[str, str]: else related_model.__name__.lower() ) - m2m_object.through = "{}_{}".format( - model._meta.table, related_model_table_name - ) + m2m_object.through = f"{model._meta.table}_{related_model_table_name}" m2m_relation = fields.ManyToManyField( - "{}.{}".format(app_name, model_name), + f"{app_name}.{model_name}", m2m_object.through, forward_key=m2m_object.backward_key, backward_key=m2m_object.forward_key, @@ -390,9 +381,7 @@ def _discover_client_class(cls, engine: str) -> BaseDBAsyncClient: try: client_class = engine_module.client_class # type: ignore except AttributeError: - raise ConfigurationError( - 'Backend for engine "{}" does not implement db client'.format(engine) - ) + raise ConfigurationError(f'Backend for engine "{engine}" does not implement db client') return client_class @classmethod @@ -400,7 +389,7 @@ def _discover_models(cls, models_path: str, app_label: str) -> List[Type[Model]] try: module = importlib.import_module(models_path) except ImportError: - raise ConfigurationError('Module "{}" not found'.format(models_path)) + raise ConfigurationError(f'Module "{models_path}" not found') discovered_models = [] possible_models = getattr(module, "__models__", None) try: @@ -472,7 +461,7 @@ def _get_config_from_config_file(cls, config_file: str) -> dict: config = json.load(f) else: raise ConfigurationError( - "Unknown config extension {}, only .yml and .json are supported".format(extension) + f"Unknown config extension {extension}, only .yml and .json are supported" ) return config diff --git a/tortoise/aggregation.py b/tortoise/aggregation.py index e63d7a641..715986207 100644 --- a/tortoise/aggregation.py +++ b/tortoise/aggregation.py @@ -33,7 +33,7 @@ def _resolve_field_for_model(self, field: str, model) -> dict: return {"joins": aggregation_joins, "field": aggregation_field} if field_split[0] not in model._meta.fetch_fields: - raise ConfigurationError("{} not resolvable".format(field)) + raise ConfigurationError(f"{field} not resolvable") related_field = model._meta.fields_map[field_split[0]] join = (Table(model._meta.table), field_split[0], related_field) aggregation = self._resolve_field_for_model("__".join(field_split[1:]), related_field.type) diff --git a/tortoise/backends/asyncpg/client.py b/tortoise/backends/asyncpg/client.py index 5c764ec2d..c17d12f1f 100644 --- a/tortoise/backends/asyncpg/client.py +++ b/tortoise/backends/asyncpg/client.py @@ -46,7 +46,7 @@ async def retry_connection_(self, *args): await self.create_connection(with_db=True) logging.info("Reconnected") except Exception as e: - raise DBConnectionError("Failed to reconnect: {}".format(str(e))) + raise DBConnectionError(f"Failed to reconnect: {str(e)}") finally: self._lock.release() @@ -114,12 +114,10 @@ async def create_connection(self, with_db: bool) -> None: "Created connection %s with params: %s", self._connection, self._template ) except asyncpg.InvalidCatalogNameError: - raise DBConnectionError( - "Can't establish connection to database {}".format(self.database) - ) + raise DBConnectionError(f"Can't establish connection to database {self.database}") # Set post-connection variables if self.schema: - await self.execute_script("SET search_path TO {}".format(self.schema)) + await self.execute_script(f"SET search_path TO {self.schema}") async def _close(self) -> None: if self._connection: # pragma: nobranch @@ -133,15 +131,13 @@ async def close(self) -> None: async def db_create(self) -> None: await self.create_connection(with_db=False) - await self.execute_script( - 'CREATE DATABASE "{}" OWNER "{}"'.format(self.database, self.user) - ) + await self.execute_script(f'CREATE DATABASE "{self.database}" OWNER "{self.user}"') await self.close() async def db_delete(self) -> None: await self.create_connection(with_db=False) try: - await self.execute_script('DROP DATABASE "{}"'.format(self.database)) + await self.execute_script(f'DROP DATABASE "{self.database}"') except asyncpg.InvalidCatalogNameError: # pragma: nocoverage pass await self.close() diff --git a/tortoise/backends/asyncpg/schema_generator.py b/tortoise/backends/asyncpg/schema_generator.py index 69dbe8afb..5a5dbeca6 100644 --- a/tortoise/backends/asyncpg/schema_generator.py +++ b/tortoise/backends/asyncpg/schema_generator.py @@ -23,11 +23,11 @@ def _get_primary_key_create_string( self, field_object: fields.Field, field_name: str, comment: str ) -> Optional[str]: if isinstance(field_object, fields.SmallIntField): - return '"{}" SMALLSERIAL NOT NULL PRIMARY KEY'.format(field_name) + return f'"{field_name}" SMALLSERIAL NOT NULL PRIMARY KEY' if isinstance(field_object, fields.IntField): - return '"{}" SERIAL NOT NULL PRIMARY KEY'.format(field_name) + return f'"{field_name}" SERIAL NOT NULL PRIMARY KEY' if isinstance(field_object, fields.BigIntField): - return '"{}" BIGSERIAL NOT NULL PRIMARY KEY'.format(field_name) + return f'"{field_name}" BIGSERIAL NOT NULL PRIMARY KEY' return None def _escape_comment(self, comment: str) -> str: diff --git a/tortoise/backends/base/client.py b/tortoise/backends/base/client.py index ba2ce6275..52b2ad3b9 100644 --- a/tortoise/backends/base/client.py +++ b/tortoise/backends/base/client.py @@ -37,7 +37,7 @@ def __init__( # Deficiencies to work around: safe_indexes: bool = True, requires_limit: bool = False, - inline_comment: bool = False + inline_comment: bool = False, ) -> None: super().__setattr__("_mutable", True) diff --git a/tortoise/backends/base/config_generator.py b/tortoise/backends/base/config_generator.py index 2ac140674..1b65eea0a 100644 --- a/tortoise/backends/base/config_generator.py +++ b/tortoise/backends/base/config_generator.py @@ -62,7 +62,7 @@ def expand_db_url(db_url: str, testing: bool = False) -> dict: url = urlparse.urlparse(db_url) if url.scheme not in DB_LOOKUP: - raise ConfigurationError("Unknown DB scheme: {}".format(url.scheme)) + raise ConfigurationError(f"Unknown DB scheme: {url.scheme}") db_backend = url.scheme db = DB_LOOKUP[db_backend] diff --git a/tortoise/backends/base/executor.py b/tortoise/backends/base/executor.py index 4fdf461aa..392d49e36 100644 --- a/tortoise/backends/base/executor.py +++ b/tortoise/backends/base/executor.py @@ -25,7 +25,7 @@ def __init__(self, model, db=None, prefetch_map=None, prefetch_queries=None): self.prefetch_map = prefetch_map if prefetch_map else {} self._prefetch_queries = prefetch_queries if prefetch_queries else {} - key = "{}:{}".format(self.db.connection_name, self.model._meta.table) + key = f"{self.db.connection_name}:{self.model._meta.table}" if key not in EXECUTOR_CACHE: self.regular_columns, columns = self._prepare_insert_columns() self.insert_query = self._prepare_insert_statement(columns) @@ -177,7 +177,7 @@ async def _prefetch_reverse_relation( relation_field = self.model._meta.fields_map[field].relation_field related_object_list = await related_query.filter( - **{"{}__in".format(relation_field): list(instance_id_set)} + **{f"{relation_field}__in": list(instance_id_set)} ) related_object_map = {} # type: Dict[str, list] @@ -273,7 +273,7 @@ async def _prefetch_direct_relation( self, instance_list: list, field: str, related_query ) -> list: related_objects_for_fetch = set() - relation_key_field = "{}_id".format(field) + relation_key_field = f"{field}_id" for instance in instance_list: if getattr(instance, relation_key_field): related_objects_for_fetch.add(getattr(instance, relation_key_field)) @@ -320,7 +320,7 @@ async def fetch_for_list(self, instance_list: list, *args) -> list: first_level_field = relation_split[0] if first_level_field not in self.model._meta.fetch_fields: raise OperationalError( - "relation {} for {} not found".format(first_level_field, self.model._meta.table) + f"relation {first_level_field} for {self.model._meta.table} not found" ) if first_level_field not in self.prefetch_map.keys(): self.prefetch_map[first_level_field] = set() diff --git a/tortoise/backends/mysql/client.py b/tortoise/backends/mysql/client.py index 921bf1d26..faf4aa475 100644 --- a/tortoise/backends/mysql/client.py +++ b/tortoise/backends/mysql/client.py @@ -123,9 +123,7 @@ async def create_connection(self, with_db: bool) -> None: "Created connection %s with params: %s", self._connection, self._template ) except pymysql.err.OperationalError: - raise DBConnectionError( - "Can't connect to MySQL server: {template}".format(template=self._template) - ) + raise DBConnectionError(f"Can't connect to MySQL server: {self._template}") async def _close(self) -> None: if self._connection: # pragma: nobranch @@ -139,13 +137,13 @@ async def close(self) -> None: async def db_create(self) -> None: await self.create_connection(with_db=False) - await self.execute_script("CREATE DATABASE {}".format(self.database)) + await self.execute_script(f"CREATE DATABASE {self.database}") await self.close() async def db_delete(self) -> None: await self.create_connection(with_db=False) try: - await self.execute_script("DROP DATABASE {}".format(self.database)) + await self.execute_script(f"DROP DATABASE {self.database}") except pymysql.err.DatabaseError: # pragma: nocoverage pass await self.close() diff --git a/tortoise/backends/mysql/executor.py b/tortoise/backends/mysql/executor.py index b27dae08a..dc7a7831f 100644 --- a/tortoise/backends/mysql/executor.py +++ b/tortoise/backends/mysql/executor.py @@ -15,33 +15,27 @@ def mysql_contains(field, value): - return functions.Cast(field, SqlTypes.CHAR).like("%{}%".format(value)) + return functions.Cast(field, SqlTypes.CHAR).like(f"%{value}%") def mysql_starts_with(field, value): - return functions.Cast(field, SqlTypes.CHAR).like("{}%".format(value)) + return functions.Cast(field, SqlTypes.CHAR).like(f"{value}%") def mysql_ends_with(field, value): - return functions.Cast(field, SqlTypes.CHAR).like("%{}".format(value)) + return functions.Cast(field, SqlTypes.CHAR).like(f"%{value}") def mysql_insensitive_contains(field, value): - return functions.Upper(functions.Cast(field, SqlTypes.CHAR)).like( - functions.Upper("%{}%".format(value)) - ) + return functions.Upper(functions.Cast(field, SqlTypes.CHAR)).like(functions.Upper(f"%{value}%")) def mysql_insensitive_starts_with(field, value): - return functions.Upper(functions.Cast(field, SqlTypes.CHAR)).like( - functions.Upper("{}%".format(value)) - ) + return functions.Upper(functions.Cast(field, SqlTypes.CHAR)).like(functions.Upper(f"{value}%")) def mysql_insensitive_ends_with(field, value): - return functions.Upper(functions.Cast(field, SqlTypes.CHAR)).like( - functions.Upper("%{}".format(value)) - ) + return functions.Upper(functions.Cast(field, SqlTypes.CHAR)).like(functions.Upper(f"%{value}")) class MySQLExecutor(BaseExecutor): diff --git a/tortoise/backends/mysql/schema_generator.py b/tortoise/backends/mysql/schema_generator.py index c9cac1a68..7deaeb1be 100644 --- a/tortoise/backends/mysql/schema_generator.py +++ b/tortoise/backends/mysql/schema_generator.py @@ -28,24 +28,18 @@ def _get_primary_key_create_string( self, field_object: fields.Field, field_name: str, comment: str ) -> Optional[str]: if isinstance(field_object, fields.SmallIntField): - return "`{}` SMALLINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT{}".format( - field_name, comment - ) + return f"`{field_name}` SMALLINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT{comment}" if isinstance(field_object, fields.IntField): - return "`{}` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT{}".format( - field_name, comment - ) + return f"`{field_name}` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT{comment}" if isinstance(field_object, fields.BigIntField): - return "`{}` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT{}".format( - field_name, comment - ) + return f"`{field_name}` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT{comment}" return None def _table_generate_extra(self, table: str) -> str: - return " CHARACTER SET {}".format(self.client.charset) if self.client.charset else "" + return f" CHARACTER SET {self.client.charset}" if self.client.charset else "" def _table_comment_generator(self, table: str, comment: str) -> str: - return " COMMENT='{}'".format(self._escape_comment(comment)) + return f" COMMENT='{self._escape_comment(comment)}'" def _column_comment_generator(self, table: str, column: str, comment: str) -> str: - return " COMMENT '{}'".format(self._escape_comment(comment)) + return f" COMMENT '{self._escape_comment(comment)}'" diff --git a/tortoise/backends/sqlite/client.py b/tortoise/backends/sqlite/client.py index e8b4189c1..1b45410f2 100644 --- a/tortoise/backends/sqlite/client.py +++ b/tortoise/backends/sqlite/client.py @@ -58,13 +58,13 @@ async def create_connection(self, with_db: bool) -> None: await self._connection._connect() self._connection._conn.row_factory = sqlite3.Row for pragma, val in self.pragmas.items(): - cursor = await self._connection.execute("PRAGMA {}={}".format(pragma, val)) + cursor = await self._connection.execute(f"PRAGMA {pragma}={val}") await cursor.close() self.log.debug( "Created connection %s with params: filename=%s %s", self._connection, self.filename, - " ".join(["{}={}".format(k, v) for k, v in self.pragmas.items()]), + " ".join([f"{k}={v}" for k, v in self.pragmas.items()]), ) async def close(self) -> None: @@ -74,7 +74,7 @@ async def close(self) -> None: "Closed connection %s with params: filename=%s %s", self._connection, self.filename, - " ".join(["{}={}".format(k, v) for k, v in self.pragmas.items()]), + " ".join([f"{k}={v}" for k, v in self.pragmas.items()]), ) self._connection = None diff --git a/tortoise/backends/sqlite/executor.py b/tortoise/backends/sqlite/executor.py index d1e710aec..0eecb99b5 100644 --- a/tortoise/backends/sqlite/executor.py +++ b/tortoise/backends/sqlite/executor.py @@ -21,7 +21,7 @@ def to_db_decimal(self, value, instance) -> Optional[str]: if self.decimal_places == 0: quant = "1" else: - quant = "1.{}".format("0" * self.decimal_places) + quant = f"1.{('0' * self.decimal_places)}" return str(Decimal(value).quantize(Decimal(quant)).normalize()) diff --git a/tortoise/backends/sqlite/schema_generator.py b/tortoise/backends/sqlite/schema_generator.py index ea60f3640..8b198b9c3 100644 --- a/tortoise/backends/sqlite/schema_generator.py +++ b/tortoise/backends/sqlite/schema_generator.py @@ -29,11 +29,11 @@ def _get_primary_key_create_string( self, field_object: fields.Field, field_name: str, comment: str ) -> Optional[str]: if isinstance(field_object, (fields.SmallIntField, fields.IntField, fields.BigIntField)): - return '"{}" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL{}'.format(field_name, comment) + return f'"{field_name}" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL{comment}' return None def _table_comment_generator(self, table: str, comment: str) -> str: - return " /* {} */".format(self._escape_comment(comment)) + return f" /* {self._escape_comment(comment)} */" def _column_comment_generator(self, table: str, column: str, comment: str) -> str: - return " /* {} */".format(self._escape_comment(comment)) + return f" /* {self._escape_comment(comment)} */" diff --git a/tortoise/contrib/pylint/__init__.py b/tortoise/contrib/pylint/__init__.py index d4d1038ab..092cd4dc6 100644 --- a/tortoise/contrib/pylint/__init__.py +++ b/tortoise/contrib/pylint/__init__.py @@ -43,7 +43,7 @@ def transform_model(cls) -> None: if attr.targets[0].name == "app": appname = attr.value.value - mname = "{}.{}".format(appname, cls.name) + mname = f"{appname}.{cls.name}" MODELS[mname] = cls for relname, relval in FUTURE_RELATIONS.get(mname, []): diff --git a/tortoise/contrib/test/__init__.py b/tortoise/contrib/test/__init__.py index 1f42a7b6e..3df50d5e1 100644 --- a/tortoise/contrib/test/__init__.py +++ b/tortoise/contrib/test/__init__.py @@ -254,9 +254,7 @@ async def _tearDownDB(self) -> None: # TODO: This is a naïve implementation: Will fail to clear M2M and non-cascade foreign keys for app in Tortoise.apps.values(): for model in app.values(): - await model._meta.db.execute_script( - "DELETE FROM {}".format(model._meta.table) # nosec - ) + await model._meta.db.execute_script(f"DELETE FROM {model._meta.table}") # nosec class TestCase(SimpleTestCase): @@ -301,7 +299,7 @@ def skip_wrapper(*args, **kwargs): db = Tortoise.get_connection(connection_name) for key, val in conditions.items(): if getattr(db.capabilities, key) != val: - raise SkipTest("Capability {key} != {val}".format(key=key, val=val)) + raise SkipTest(f"Capability {key} != {val}") return test_item(*args, **kwargs) return skip_wrapper diff --git a/tortoise/fields.py b/tortoise/fields.py index ca0115021..d077098b9 100644 --- a/tortoise/fields.py +++ b/tortoise/fields.py @@ -59,7 +59,7 @@ def __init__( reference: Optional[str] = None, model: "Optional[Model]" = None, description: Optional[str] = None, - **kwargs + **kwargs, ) -> None: self.type = type self.source_field = source_field @@ -436,14 +436,14 @@ def __init__( forward_key: Optional[str] = None, backward_key: str = "", related_name: str = "", - **kwargs + **kwargs, ) -> None: super().__init__(**kwargs) if len(model_name.split(".")) != 2: raise ConfigurationError('Foreign key accepts model name in format "app.Model"') self.model_name = model_name self.related_name = related_name - self.forward_key = forward_key or "{}_id".format(model_name.split(".")[1].lower()) + self.forward_key = forward_key or f"{model_name.split('.')[1].lower()}_id" self.backward_key = backward_key self.through = through self._generated = False @@ -597,9 +597,7 @@ async def add(self, *instances, using_db=None) -> None: if not instances: return if not self.instance._saved_in_db: - raise OperationalError( - "You should first call .save() on {model}".format(model=self.instance) - ) + raise OperationalError(f"You should first call .save() on {self.instance}") db = using_db if using_db else self.model._meta.db pk_formatting_func = type(self.instance)._meta.pk.to_db_value related_pk_formatting_func = type(instances[0])._meta.pk.to_db_value @@ -642,9 +640,7 @@ async def add(self, *instances, using_db=None) -> None: insert_is_required = False for instance_to_add in instances: if not instance_to_add._saved_in_db: - raise OperationalError( - "You should first call .save() on {model}".format(model=instance_to_add) - ) + raise OperationalError(f"You should first call .save() on {instance_to_add}") pk_f = related_pk_formatting_func(instance_to_add.pk, instance_to_add) pk_b = pk_formatting_func(self.instance.pk, self.instance) if (pk_b, pk_f) in already_existing_relations: diff --git a/tortoise/filters.py b/tortoise/filters.py index 8ae21b909..6d19e4c70 100644 --- a/tortoise/filters.py +++ b/tortoise/filters.py @@ -54,32 +54,32 @@ def not_null(field, value): def contains(field, value): - return functions.Cast(field, SqlTypes.VARCHAR).like("%{}%".format(value)) + return functions.Cast(field, SqlTypes.VARCHAR).like(f"%{value}%") def starts_with(field, value): - return functions.Cast(field, SqlTypes.VARCHAR).like("{}%".format(value)) + return functions.Cast(field, SqlTypes.VARCHAR).like(f"{value}%") def ends_with(field, value): - return functions.Cast(field, SqlTypes.VARCHAR).like("%{}".format(value)) + return functions.Cast(field, SqlTypes.VARCHAR).like(f"%{value}") def insensitive_contains(field, value): return functions.Upper(functions.Cast(field, SqlTypes.VARCHAR)).like( - functions.Upper("%{}%".format(value)) + functions.Upper(f"%{value}%") ) def insensitive_starts_with(field, value): return functions.Upper(functions.Cast(field, SqlTypes.VARCHAR)).like( - functions.Upper("{}%".format(value)) + functions.Upper(f"{value}%") ) def insensitive_ends_with(field, value): return functions.Upper(functions.Cast(field, SqlTypes.VARCHAR)).like( - functions.Upper("%{}".format(value)) + functions.Upper(f"%{value}") ) @@ -93,21 +93,21 @@ def get_m2m_filters(field_name: str, field: fields.ManyToManyField) -> Dict[str, "table": Table(field.through), "value_encoder": target_table_pk.to_db_value, }, - "{}__not".format(field_name): { + f"{field_name}__not": { "field": field.forward_key, "backward_key": field.backward_key, "operator": not_equal, "table": Table(field.through), "value_encoder": target_table_pk.to_db_value, }, - "{}__in".format(field_name): { + f"{field_name}__in": { "field": field.forward_key, "backward_key": field.backward_key, "operator": is_in, "table": Table(field.through), "value_encoder": partial(related_list_encoder, field=target_table_pk), }, - "{}__not_in".format(field_name): { + f"{field_name}__not_in": { "field": field.forward_key, "backward_key": field.backward_key, "operator": not_in, @@ -127,21 +127,21 @@ def get_backward_fk_filters(field_name: str, field: fields.BackwardFKRelation) - "table": Table(field.type._meta.table), "value_encoder": target_table_pk.to_db_value, }, - "{}__not".format(field_name): { + f"{field_name}__not": { "field": "id", "backward_key": field.relation_field, "operator": not_equal, "table": Table(field.type._meta.table), "value_encoder": target_table_pk.to_db_value, }, - "{}__in".format(field_name): { + f"{field_name}__in": { "field": "id", "backward_key": field.relation_field, "operator": is_in, "table": Table(field.type._meta.table), "value_encoder": partial(related_list_encoder, field=target_table_pk), }, - "{}__not_in".format(field_name): { + f"{field_name}__not_in": { "field": "id", "backward_key": field.relation_field, "operator": not_in, @@ -167,86 +167,86 @@ def get_filters_for_field( "source_field": source_field, "operator": operator.eq, }, - "{}__not".format(field_name): { + f"{field_name}__not": { "field": actual_field_name, "source_field": source_field, "operator": not_equal, }, - "{}__in".format(field_name): { + f"{field_name}__in": { "field": actual_field_name, "source_field": source_field, "operator": is_in, "value_encoder": list_encoder, }, - "{}__not_in".format(field_name): { + f"{field_name}__not_in": { "field": actual_field_name, "source_field": source_field, "operator": not_in, "value_encoder": list_encoder, }, - "{}__isnull".format(field_name): { + f"{field_name}__isnull": { "field": actual_field_name, "source_field": source_field, "operator": is_null, "value_encoder": bool_encoder, }, - "{}__not_isnull".format(field_name): { + f"{field_name}__not_isnull": { "field": actual_field_name, "source_field": source_field, "operator": not_null, "value_encoder": bool_encoder, }, - "{}__gte".format(field_name): { + f"{field_name}__gte": { "field": actual_field_name, "source_field": source_field, "operator": operator.ge, }, - "{}__lte".format(field_name): { + f"{field_name}__lte": { "field": actual_field_name, "source_field": source_field, "operator": operator.le, }, - "{}__gt".format(field_name): { + f"{field_name}__gt": { "field": actual_field_name, "source_field": source_field, "operator": operator.gt, }, - "{}__lt".format(field_name): { + f"{field_name}__lt": { "field": actual_field_name, "source_field": source_field, "operator": operator.lt, }, - "{}__contains".format(field_name): { + f"{field_name}__contains": { "field": actual_field_name, "source_field": source_field, "operator": contains, "value_encoder": string_encoder, }, - "{}__startswith".format(field_name): { + f"{field_name}__startswith": { "field": actual_field_name, "source_field": source_field, "operator": starts_with, "value_encoder": string_encoder, }, - "{}__endswith".format(field_name): { + f"{field_name}__endswith": { "field": actual_field_name, "source_field": source_field, "operator": ends_with, "value_encoder": string_encoder, }, - "{}__icontains".format(field_name): { + f"{field_name}__icontains": { "field": actual_field_name, "source_field": source_field, "operator": insensitive_contains, "value_encoder": string_encoder, }, - "{}__istartswith".format(field_name): { + f"{field_name}__istartswith": { "field": actual_field_name, "source_field": source_field, "operator": insensitive_starts_with, "value_encoder": string_encoder, }, - "{}__iendswith".format(field_name): { + f"{field_name}__iendswith": { "field": actual_field_name, "source_field": source_field, "operator": insensitive_ends_with, diff --git a/tortoise/models.py b/tortoise/models.py index d81b45e51..c2bb940a1 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -115,7 +115,7 @@ def __init__(self, meta) -> None: def add_field(self, name: str, value: Field): if name in self.fields_map: - raise ConfigurationError("Field {} already present in meta".format(name)) + raise ConfigurationError(f"Field {name} already present in meta") setattr(self._model, name, value) value.model = self._model self.fields_map[name] = value @@ -172,7 +172,7 @@ def finalise_fields(self) -> None: # Create lazy FK fields on model. for key in self.fk_fields: - _key = "_{}".format(key) + _key = f"_{key}" relation_field = self.fields_map[key].source_field setattr( self._model, @@ -186,7 +186,7 @@ def finalise_fields(self) -> None: # Create lazy reverse FK fields on model. for key in self.backward_fk_fields: - _key = "_{}".format(key) + _key = f"_{key}" field_object = self.fields_map[key] # type: fields.BackwardFKRelation # type: ignore setattr( self._model, @@ -203,7 +203,7 @@ def finalise_fields(self) -> None: # Create lazy M2M fields on model. for key in self.m2m_fields: - _key = "_{}".format(key) + _key = f"_{key}" setattr( self._model, key, @@ -393,7 +393,7 @@ def _set_field_values(self, values_map: Dict[str, Any]) -> Set[str]: if key in meta.fk_fields: if value and not value._saved_in_db: raise OperationalError( - "You should first call .save() on {} before referring to it".format(value) + f"You should first call .save() on {value} before referring to it" ) field_object = meta.fields_map[key] relation_field = field_object.source_field # type: str # type: ignore @@ -402,12 +402,12 @@ def _set_field_values(self, values_map: Dict[str, Any]) -> Set[str]: elif key in meta.fields_db_projection: field_object = meta.fields_map[key] if value is None and not field_object.null: - raise ValueError("{} is non nullable field, but null was passed".format(key)) + raise ValueError(f"{key} is non nullable field, but null was passed") setattr(self, key, field_object.to_python_value(value)) elif key in meta.db_fields: field_object = meta.fields_map[meta.fields_db_projection_reverse[key]] if value is None and not field_object.null: - raise ValueError("{} is non nullable field, but null was passed".format(key)) + raise ValueError(f"{key} is non nullable field, but null was passed") setattr(self, key, field_object.to_python_value(value)) elif key in meta.backward_fk_fields: raise ConfigurationError( @@ -421,12 +421,12 @@ def _set_field_values(self, values_map: Dict[str, Any]) -> Set[str]: return passed_fields def __str__(self) -> str: - return "<{}>".format(self.__class__.__name__) + return f"<{self.__class__.__name__}>" def __repr__(self) -> str: if self.pk: - return "<{}: {}>".format(self.__class__.__name__, self.pk) - return "<{}>".format(self.__class__.__name__) + return f"<{self.__class__.__name__}: {self.pk}>" + return f"<{self.__class__.__name__}>" def __hash__(self) -> int: if not self.pk: @@ -622,16 +622,14 @@ def _check_unique_together(cls) -> None: return if not isinstance(cls._meta.unique_together, (tuple, list)): - raise ConfigurationError( - "'{}.unique_together' must be a list or tuple.".format(cls.__name__) - ) + raise ConfigurationError(f"'{cls.__name__}.unique_together' must be a list or tuple.") if any( not isinstance(unique_fields, (tuple, list)) for unique_fields in cls._meta.unique_together ): raise ConfigurationError( - "All '{}.unique_together' elements must be lists or tuples.".format(cls.__name__) + f"All '{cls.__name__}.unique_together' elements must be lists or tuples." ) for fields_tuple in cls._meta.unique_together: @@ -640,8 +638,7 @@ def _check_unique_together(cls) -> None: if not field: raise ConfigurationError( - "'{}.unique_together' has no '{}' " - "field.".format(cls.__name__, field_name) + f"'{cls.__name__}.unique_together' has no '{field_name}' field." ) if isinstance(field, ManyToManyField): diff --git a/tortoise/query_utils.py b/tortoise/query_utils.py index b6480166a..fb56397fe 100644 --- a/tortoise/query_utils.py +++ b/tortoise/query_utils.py @@ -12,8 +12,8 @@ def _process_filter_kwarg(model, key, value) -> Tuple[Criterion, Optional[Tuple[ join = None table = Table(model._meta.table) - if value is None and "{}__isnull".format(key) in model._meta.filters: - param = model._meta.get_filter("{}__isnull".format(key)) + if value is None and f"{key}__isnull" in model._meta.filters: + param = model._meta.get_filter(f"{key}__isnull") value = True else: param = model._meta.get_filter(key) @@ -76,7 +76,7 @@ def _get_joins_for_related_field( ( related_table, getattr(related_table, related_table_pk) - == getattr(table, "{}_id".format(related_field_name)), + == getattr(table, f"{related_field_name}_id"), ) ) return required_joins @@ -260,9 +260,7 @@ def _get_actual_filter_params(self, model, key, value) -> Tuple[str, Any]: allowed = sorted( list(model._meta.fields | model._meta.fetch_fields | set(self._custom_filters)) ) - raise FieldError( - "Unknown filter param '{}'. Allowed base values are {}".format(key, allowed) - ) + raise FieldError(f"Unknown filter param '{key}'. Allowed base values are {allowed}") return filter_key, filter_value def _resolve_kwargs(self, model) -> QueryModifier: @@ -316,7 +314,7 @@ def resolve_for_queryset(self, queryset) -> None: first_level_field = relation_split[0] if first_level_field not in queryset.model._meta.fetch_fields: raise OperationalError( - "relation {} for {} not found".format(first_level_field, queryset.model._meta.table) + f"relation {first_level_field} for {queryset.model._meta.table} not found" ) forwarded_prefetch = "__".join(relation_split[1:]) if forwarded_prefetch: diff --git a/tortoise/queryset.py b/tortoise/queryset.py index 4d2d7b8b2..d2ec2d278 100644 --- a/tortoise/queryset.py +++ b/tortoise/queryset.py @@ -68,9 +68,7 @@ def resolve_ordering(self, model, orderings, annotations) -> None: else: field_object = self.model._meta.fields_map.get(field_name) if not field_object: - raise FieldError( - "Unknown field {} for model {}".format(field_name, self.model.__name__) - ) + raise FieldError(f"Unknown field {field_name} for model {self.model.__name__}") field_name = field_object.source_field or field_name self.query = self.query.orderby(getattr(table, field_name), order=ordering[1]) @@ -213,9 +211,7 @@ def order_by(self, *orderings: str) -> "QuerySet": field_name.split("__")[0] in self.model._meta.fields or field_name in self._annotations ): - raise FieldError( - "Unknown field {} for model {}".format(field_name, self.model.__name__) - ) + raise FieldError(f"Unknown field {field_name} for model {self.model.__name__}") new_ordering.append((field_name, order_type)) queryset._orderings = new_ordering return queryset @@ -305,12 +301,12 @@ def values(self, *args: str, **kwargs: str) -> "ValuesQuery": fields_for_select = {} # type: Dict[str, str] for field in args: if field in fields_for_select: - raise FieldError("Duplicate key {}".format(field)) + raise FieldError(f"Duplicate key {field}") fields_for_select[field] = field for return_as, field in kwargs.items(): if return_as in fields_for_select: - raise FieldError("Duplicate key {}".format(return_as)) + raise FieldError(f"Duplicate key {return_as}") fields_for_select[return_as] = field else: fields_for_select = { @@ -410,15 +406,11 @@ def prefetch_related(self, *args: str) -> "QuerySet": if first_level_field not in self.model._meta.fetch_fields: if first_level_field in self.model._meta.fields: raise FieldError( - "Field {} on {} is not a relation".format( - first_level_field, self.model._meta.table - ) + f"Field {first_level_field} on {self.model._meta.table} is not a relation" ) else: raise FieldError( - "Relation {} for {} not found".format( - first_level_field, self.model._meta.table - ) + f"Relation {first_level_field} for {self.model._meta.table} not found" ) if first_level_field not in queryset._prefetch_map.keys(): queryset._prefetch_map[first_level_field] = set() @@ -533,9 +525,9 @@ def _make_query(self): for key, value in self.update_kwargs.items(): field_object = self.model._meta.fields_map.get(key) if not field_object: - raise FieldError("Unknown keyword argument {} for model {}".format(key, self.model)) + raise FieldError(f"Unknown keyword argument {key} for model {self.model}") if field_object.pk: - raise IntegrityError("Field {} is PK and can not be updated".format(key)) + raise IntegrityError(f"Field {key} is PK and can not be updated") if isinstance(field_object, fields.ForeignKeyField): fk_field = field_object.source_field db_field = self.model._meta.fields_map[fk_field].source_field @@ -544,7 +536,7 @@ def _make_query(self): try: db_field = self.model._meta.fields_db_projection[key] except KeyError: - raise FieldError("Field {} is virtual and can not be updated".format(key)) + raise FieldError(f"Field {key} is virtual and can not be updated") value = executor.column_map[key](value, None) self.query = self.query.set(db_field, value) @@ -614,9 +606,7 @@ def _join_table_with_forwarded_fields( return table, model._meta.fields_db_projection[field] if field in model._meta.fields_db_projection and forwarded_fields: - raise FieldError( - 'Field "{}" for model "{}" is not relation'.format(field, model.__name__) - ) + raise FieldError(f'Field "{field}" for model "{model.__name__}" is not relation') if field in self.model._meta.fetch_fields and not forwarded_fields: raise ValueError( @@ -626,7 +616,7 @@ def _join_table_with_forwarded_fields( field_object = model._meta.fields_map.get(field) if not field_object: - raise FieldError('Unknown field "{}" for model "{}"'.format(field, model.__name__)) + raise FieldError(f'Unknown field "{field}" for model "{model.__name__}"') self._join_table_by_field(table, field, field_object) forwarded_fields_split = forwarded_fields.split("__") @@ -658,7 +648,7 @@ def add_field_to_select_query(self, field, return_as) -> None: self.query._select_field(getattr(related_table, related_db_field).as_(return_as)) return - raise FieldError('Unknown field "{}" for model "{}"'.format(field, self.model.__name__)) + raise FieldError(f'Unknown field "{field}" for model "{self.model.__name__}"') def resolve_to_python_value(self, model, field): if field in model._meta.fetch_fields: @@ -673,7 +663,7 @@ def resolve_to_python_value(self, model, field): new_model = model._meta.fields_map[field_split[0]].type return self.resolve_to_python_value(new_model, "__".join(field_split[1:])) - raise FieldError('Unknown field "{}" for model "{}"'.format(field, model)) + raise FieldError(f'Unknown field "{field}" for model "{model}"') class ValuesListQuery(FieldSelectQuery): diff --git a/tortoise/tests/test_aggregation.py b/tortoise/tests/test_aggregation.py index ef041c26b..4f8726cf6 100644 --- a/tortoise/tests/test_aggregation.py +++ b/tortoise/tests/test_aggregation.py @@ -14,7 +14,7 @@ async def test_aggregation(self): await event.save() participants = [] for i in range(2): - team = Team(name="Team {}".format(i + 1)) + team = Team(name=f"Team {(i + 1)}") await team.save() participants.append(team) await event.participants.add(participants[0], participants[1]) diff --git a/tortoise/tests/test_describe_model.py b/tortoise/tests/test_describe_model.py index 3c0821035..7c15fc7e6 100644 --- a/tortoise/tests/test_describe_model.py +++ b/tortoise/tests/test_describe_model.py @@ -1,5 +1,4 @@ import json -import sys import uuid from tortoise import Tortoise, fields @@ -41,7 +40,6 @@ async def test_describe_models_some(self): {"models.Event", "models.Tournament", "models.Reporter", "models.Team"}, set(val.keys()) ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_describe_model_straight(self): val = Tortoise.describe_model(StraightFields) @@ -158,7 +156,6 @@ async def test_describe_model_straight(self): }, ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_describe_model_straight_native(self): val = Tortoise.describe_model(StraightFields, serializable=False) @@ -275,7 +272,6 @@ async def test_describe_model_straight_native(self): }, ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_describe_model_source(self): val = Tortoise.describe_model(SourceFields) @@ -392,7 +388,6 @@ async def test_describe_model_source(self): }, ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_describe_model_source_native(self): val = Tortoise.describe_model(SourceFields, serializable=False) @@ -509,7 +504,6 @@ async def test_describe_model_source_native(self): }, ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_describe_model_uuidpk(self): val = Tortoise.describe_model(UUIDPkModel) @@ -576,7 +570,6 @@ async def test_describe_model_uuidpk(self): }, ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_describe_model_uuidpk_native(self): val = Tortoise.describe_model(UUIDPkModel, serializable=False) @@ -643,7 +636,6 @@ async def test_describe_model_uuidpk_native(self): }, ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_describe_model_json(self): val = Tortoise.describe_model(JSONFields) @@ -712,7 +704,6 @@ async def test_describe_model_json(self): }, ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_describe_model_json_native(self): val = Tortoise.describe_model(JSONFields, serializable=False) diff --git a/tortoise/tests/test_generate_schema.py b/tortoise/tests/test_generate_schema.py index e37398a1f..33a1d7b5c 100644 --- a/tortoise/tests/test_generate_schema.py +++ b/tortoise/tests/test_generate_schema.py @@ -1,6 +1,5 @@ # pylint: disable=C0301 import re -import sys import warnings from asynctest.mock import CoroutineMock, patch @@ -92,7 +91,7 @@ async def test_cyclic(self): def continue_if_safe_indexes(supported: bool): db = Tortoise.get_connection("default") if db.capabilities.safe_indexes != supported: - raise test.SkipTest("safe_indexes != {}".format(supported)) + raise test.SkipTest(f"safe_indexes != {supported}") async def test_create_index(self): await self.init_for("tortoise.tests.testmodels") @@ -142,7 +141,6 @@ async def test_table_and_row_comment_generation(self): self.assertRegex(sql, r".*\\n.*") self.assertIn("\\/", sql) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_schema(self): self.maxDiff = None await self.init_for("tortoise.tests.models_schema_create") @@ -193,7 +191,6 @@ async def test_schema(self): """.strip(), # noqa ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_schema_safe(self): self.maxDiff = None await self.init_for("tortoise.tests.models_schema_create") @@ -301,7 +298,6 @@ async def test_table_and_row_comment_generation(self): self.assertRegex(sql, r".*\\n.*") self.assertRegex(sql, r".*it\\'s.*") - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_schema(self): self.maxDiff = None await self.init_for("tortoise.tests.models_schema_create") @@ -352,7 +348,6 @@ async def test_schema(self): """.strip(), # noqa ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_schema_safe(self): self.maxDiff = None await self.init_for("tortoise.tests.models_schema_create") @@ -465,7 +460,6 @@ async def test_table_and_row_comment_generation(self): ) self.assertIn("COMMENT ON COLUMN comments.multiline_comment IS 'Some \\n comment'", sql) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_schema(self): self.maxDiff = None await self.init_for("tortoise.tests.models_schema_create") @@ -526,7 +520,6 @@ async def test_schema(self): """.strip(), ) - @test.skipIf(sys.version_info < (3, 6), "Dict not sorted in 3.5") async def test_schema_safe(self): self.maxDiff = None await self.init_for("tortoise.tests.models_schema_create") diff --git a/tortoise/tests/test_init.py b/tortoise/tests/test_init.py index 4ff6889f1..c824324e3 100644 --- a/tortoise/tests/test_init.py +++ b/tortoise/tests/test_init.py @@ -104,7 +104,7 @@ async def test_url_without_modules(self): with self.assertRaisesRegex( ConfigurationError, 'You must specify "db_url" and "modules" together' ): - await Tortoise.init(db_url="sqlite://{}".format(":memory:")) + await Tortoise.init(db_url=f"sqlite://{':memory:'}") async def test_default_connection_init(self): await Tortoise.init( @@ -124,7 +124,7 @@ async def test_default_connection_init(self): async def test_db_url_init(self): await Tortoise.init( { - "connections": {"default": "sqlite://{}".format(":memory:")}, + "connections": {"default": f"sqlite://{':memory:'}"}, "apps": { "models": { "models": ["tortoise.tests.testmodels"], @@ -138,8 +138,7 @@ async def test_db_url_init(self): async def test_shorthand_init(self): await Tortoise.init( - db_url="sqlite://{}".format(":memory:"), - modules={"models": ["tortoise.tests.testmodels"]}, + db_url=f"sqlite://{':memory:'}", modules={"models": ["tortoise.tests.testmodels"]} ) self.assertIn("models", Tortoise.apps) self.assertIsNotNone(Tortoise.get_connection("default")) diff --git a/tortoise/tests/test_model_methods.py b/tortoise/tests/test_model_methods.py index 2e84e22be..a17311789 100644 --- a/tortoise/tests/test_model_methods.py +++ b/tortoise/tests/test_model_methods.py @@ -57,7 +57,7 @@ def test_str(self): self.assertEqual(str(self.mdl), "Test") def test_repr(self): - self.assertEqual(repr(self.mdl), "".format(self.mdl.id)) + self.assertEqual(repr(self.mdl), f"") self.assertEqual(repr(self.mdl2), "") def test_hash(self): @@ -117,7 +117,7 @@ def test_str(self): self.assertEqual(str(self.mdl), "") def test_repr(self): - self.assertEqual(repr(self.mdl), "".format(self.mdl.id)) + self.assertEqual(repr(self.mdl), f"") self.assertEqual(repr(self.mdl2), "") diff --git a/tortoise/tests/test_reconnect.py b/tortoise/tests/test_reconnect.py index d81177b47..104c1c63f 100644 --- a/tortoise/tests/test_reconnect.py +++ b/tortoise/tests/test_reconnect.py @@ -16,9 +16,7 @@ async def test_reconnect(self): await Tortoise._connections["models"]._close() - self.assertEqual( - ["{}:{}".format(a.id, a.name) for a in await Tournament.all()], ["1:1", "2:2"] - ) + self.assertEqual([f"{a.id}:{a.name}" for a in await Tournament.all()], ["1:1", "2:2"]) @test.requireCapability(daemon=True) async def test_reconnect_fail(self): @@ -46,9 +44,7 @@ async def test_reconnect_transaction_start(self): await Tortoise._connections["models"]._close() async with in_transaction(): - self.assertEqual( - ["{}:{}".format(a.id, a.name) for a in await Tournament.all()], ["1:1", "2:2"] - ) + self.assertEqual([f"{a.id}:{a.name}" for a in await Tournament.all()], ["1:1", "2:2"]) @test.requireCapability(daemon=True) async def test_reconnect_during_transaction_fails(self): @@ -60,4 +56,4 @@ async def test_reconnect_during_transaction_fails(self): await Tortoise._connections["models"]._close() await Tournament.create(name="3") - self.assertEqual(["{}:{}".format(a.id, a.name) for a in await Tournament.all()], ["1:1"]) + self.assertEqual([f"{a.id}:{a.name}" for a in await Tournament.all()], ["1:1"]) diff --git a/tortoise/tests/test_relations.py b/tortoise/tests/test_relations.py index b9173c6b2..6206bb2e3 100644 --- a/tortoise/tests/test_relations.py +++ b/tortoise/tests/test_relations.py @@ -13,7 +13,7 @@ async def test_relations(self): await event.save() participants = [] for i in range(2): - team = Team(name="Team {}".format(i + 1)) + team = Team(name=f"Team {(i + 1)}") await team.save() participants.append(team) await event.participants.add(participants[0], participants[1]) @@ -61,7 +61,7 @@ async def test_reset_queryset_on_query(self): event = await Event.create(name="Test", tournament_id=tournament.id) participants = [] for i in range(2): - team = await Team.create(name="Team {}".format(i + 1)) + team = await Team.create(name=f"Team {(i + 1)}") participants.append(team) await event.participants.add(*participants) queryset = Event.all().annotate(count=Count("participants")) diff --git a/tortoise/tests/test_two_databases.py b/tortoise/tests/test_two_databases.py index d4cde808d..250fc0b60 100644 --- a/tortoise/tests/test_two_databases.py +++ b/tortoise/tests/test_two_databases.py @@ -49,7 +49,7 @@ async def test_two_databases_relation(self): teams = [] for i in range(2): - team = await TeamTwo.create(name="Team {}".format(i + 1)) + team = await TeamTwo.create(name=f"Team {(i + 1)}") teams.append(team) await event.participants.add(team) diff --git a/tortoise/tests/testfields.py b/tortoise/tests/testfields.py index ebcf7d26e..ce48a1496 100644 --- a/tortoise/tests/testfields.py +++ b/tortoise/tests/testfields.py @@ -14,7 +14,7 @@ class EnumField(CharField): def __init__(self, enum_type: Type[Enum], **kwargs): super().__init__(128, **kwargs) if not issubclass(enum_type, Enum): - raise ConfigurationError("{} is not a subclass of Enum!".format(enum_type)) + raise ConfigurationError(f"{enum_type} is not a subclass of Enum!") self._enum_type = enum_type def to_db_value(self, value, instance): @@ -22,7 +22,7 @@ def to_db_value(self, value, instance): return None if not isinstance(value, self._enum_type): - raise TypeError("Expected type {}, got {}".format(self._enum_type, value)) + raise TypeError(f"Expected type {self._enum_type}, got {value}") return value.value @@ -32,7 +32,7 @@ def to_python_value(self, value): except ValueError: if not self.null: raise ValueError( - "Database value {} does not exist on Enum {}.".format(value, self._enum_type) + f"Database value {value} does not exist on Enum {self._enum_type}." ) return None diff --git a/tox.ini b/tox.ini index ab803c578..67989f084 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{35,36,37}-{sqlite,postgres,mysql},pypy3-{sqlite,mysql} +envlist = py{36,37}-{sqlite,postgres,mysql},pypy3-{sqlite,mysql} skip_missing_interpreters = True [testenv] From cfd18a4d82f95716e4dc12f24bd551a6c1f133e6 Mon Sep 17 00:00:00 2001 From: Grigi Date: Wed, 11 Sep 2019 14:08:23 +0200 Subject: [PATCH 02/10] Upgraded py3.5 style type comments --- Makefile | 2 +- tortoise/__init__.py | 8 +-- tortoise/aggregation.py | 2 +- tortoise/backends/asyncpg/client.py | 6 +- tortoise/backends/asyncpg/schema_generator.py | 2 +- tortoise/backends/base/config_generator.py | 12 ++-- tortoise/backends/base/executor.py | 26 +++---- tortoise/backends/base/schema_generator.py | 12 ++-- tortoise/backends/mysql/client.py | 10 +-- tortoise/backends/sqlite/client.py | 6 +- tortoise/contrib/pylint/__init__.py | 4 +- tortoise/contrib/test/__init__.py | 10 +-- tortoise/fields.py | 4 +- tortoise/filters.py | 2 +- tortoise/models.py | 72 +++++++++---------- tortoise/query_utils.py | 10 +-- tortoise/queryset.py | 38 +++++----- tortoise/transactions.py | 4 +- tortoise/utils.py | 2 +- 19 files changed, 116 insertions(+), 116 deletions(-) diff --git a/Makefile b/Makefile index c2ee3dcfe..bfa540f83 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ checkfiles = tortoise/ examples/ setup.py conftest.py -black_opts = -l 100 +black_opts = -l 100 -t py36 py_warn = PYTHONWARNINGS=default PYTHONASYNCIODEBUG=1 PYTHONDEBUG=x PYTHONDEVMODE=dev help: diff --git a/tortoise/__init__.py b/tortoise/__init__.py index 28b7b3316..d71f37bb5 100644 --- a/tortoise/__init__.py +++ b/tortoise/__init__.py @@ -27,9 +27,9 @@ class Tortoise: - apps = {} # type: Dict[str, Dict[str, Type[Model]]] - _connections = {} # type: Dict[str, BaseDBAsyncClient] - _inited = False # type: bool + apps: Dict[str, Dict[str, Type[Model]]] = {} + _connections: Dict[str, BaseDBAsyncClient] = {} + _inited: bool = False @classmethod def get_connection(cls, connection_name: str) -> BaseDBAsyncClient: @@ -433,7 +433,7 @@ def _init_apps(cls, apps_config: dict) -> None: info.get("default_connection", "default"), name ) ) - app_models = [] # type: List[Type[Model]] + app_models: List[Type[Model]] = [] for module in info["models"]: app_models += cls._discover_models(module, name) diff --git a/tortoise/aggregation.py b/tortoise/aggregation.py index 715986207..3cf43613c 100644 --- a/tortoise/aggregation.py +++ b/tortoise/aggregation.py @@ -20,7 +20,7 @@ def __init__(self, field) -> None: def _resolve_field_for_model(self, field: str, model) -> dict: field_split = field.split("__") if not field_split[1:]: - aggregation_joins = [] # type: list + aggregation_joins: list = [] if field_split[0] in model._meta.fetch_fields: related_field = model._meta.fields_map[field_split[0]] join = (Table(model._meta.table), field_split[0], related_field) diff --git a/tortoise/backends/asyncpg/client.py b/tortoise/backends/asyncpg/client.py index c17d12f1f..177f09591 100644 --- a/tortoise/backends/asyncpg/client.py +++ b/tortoise/backends/asyncpg/client.py @@ -1,7 +1,7 @@ import asyncio import logging from functools import wraps -from typing import List, Optional, SupportsInt # noqa +from typing import List, Optional, SupportsInt import asyncpg from pypika import PostgreSQLQuery @@ -92,8 +92,8 @@ def __init__( self.extra.pop("loop", None) self.extra.pop("connection_class", None) - self._template = {} # type: dict - self._connection = None # Type: Optional[asyncpg.Connection] + self._template: dict = {} + self._connection: Optional[asyncpg.Connection] = None self._lock = asyncio.Lock() self._transaction_class = type( diff --git a/tortoise/backends/asyncpg/schema_generator.py b/tortoise/backends/asyncpg/schema_generator.py index 5a5dbeca6..53560b58a 100644 --- a/tortoise/backends/asyncpg/schema_generator.py +++ b/tortoise/backends/asyncpg/schema_generator.py @@ -17,7 +17,7 @@ class AsyncpgSchemaGenerator(BaseSchemaGenerator): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.comments_array = [] # type: List[str] + self.comments_array: List[str] = [] def _get_primary_key_create_string( self, field_object: fields.Field, field_name: str, comment: str diff --git a/tortoise/backends/base/config_generator.py b/tortoise/backends/base/config_generator.py index 1b65eea0a..bac859c50 100644 --- a/tortoise/backends/base/config_generator.py +++ b/tortoise/backends/base/config_generator.py @@ -1,13 +1,13 @@ import urllib.parse as urlparse import uuid -from typing import Any, Dict, List, Optional # noqa +from typing import Any, Dict, List, Optional from tortoise.exceptions import ConfigurationError urlparse.uses_netloc.append("postgres") urlparse.uses_netloc.append("sqlite") urlparse.uses_netloc.append("mysql") -DB_LOOKUP = { +DB_LOOKUP: Dict[str, Dict[str, Any]] = { "postgres": { "engine": "tortoise.backends.asyncpg", "vmap": { @@ -56,7 +56,7 @@ "use_unicode": bool, }, }, -} # type: Dict[str, Dict[str, Any]] +} def expand_db_url(db_url: str, testing: bool = False) -> dict: @@ -67,7 +67,7 @@ def expand_db_url(db_url: str, testing: bool = False) -> dict: db_backend = url.scheme db = DB_LOOKUP[db_backend] if db.get("skip_first_char", True): - path = url.path[1:] # type: Optional[str] + path: Optional[str] = url.path[1:] else: path = url.netloc + url.path @@ -77,7 +77,7 @@ def expand_db_url(db_url: str, testing: bool = False) -> dict: # Other database backend accepts database name being None (but not empty string). path = None - params = {} # type: dict + params: dict = {} for key, val in db["defaults"].items(): params[key] = val for key, val in urlparse.parse_qs(url.query).items(): @@ -88,7 +88,7 @@ def expand_db_url(db_url: str, testing: bool = False) -> dict: path = path.replace("\\{", "{").replace("\\}", "}") path = path.format(uuid.uuid4().hex) - vmap = {} # type: dict + vmap: dict = {} vmap.update(db["vmap"]) params[vmap["path"]] = path if vmap.get("hostname"): diff --git a/tortoise/backends/base/executor.py b/tortoise/backends/base/executor.py index 392d49e36..3631267f8 100644 --- a/tortoise/backends/base/executor.py +++ b/tortoise/backends/base/executor.py @@ -1,6 +1,6 @@ from copy import copy from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Type # noqa +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Type from pypika import JoinType, Parameter, Table @@ -11,12 +11,12 @@ if TYPE_CHECKING: # pragma: nocoverage from tortoise.models import Model -EXECUTOR_CACHE = {} # type: Dict[str, Tuple[list, str, Dict[str, Callable], str, Dict[str, str]]] +EXECUTOR_CACHE: Dict[str, Tuple[list, str, Dict[str, Callable], str, Dict[str, str]]] = {} class BaseExecutor: - TO_DB_OVERRIDE = {} # type: Dict[Type[fields.Field], Callable] - FILTER_FUNC_OVERRIDE = {} # type: Dict[Callable, Callable] + TO_DB_OVERRIDE: Dict[Type[fields.Field], Callable] = {} + FILTER_FUNC_OVERRIDE: Dict[Callable, Callable] = {} EXPLAIN_PREFIX = "EXPLAIN" def __init__(self, model, db=None, prefetch_map=None, prefetch_queries=None): @@ -30,7 +30,7 @@ def __init__(self, model, db=None, prefetch_map=None, prefetch_queries=None): self.regular_columns, columns = self._prepare_insert_columns() self.insert_query = self._prepare_insert_statement(columns) - self.column_map = {} # type: Dict[str, Callable] + self.column_map: Dict[str, Callable] = {} for column in self.regular_columns: field_object = self.model._meta.fields_map[column] if field_object.__class__ in self.TO_DB_OVERRIDE: @@ -45,7 +45,7 @@ def __init__(self, model, db=None, prefetch_map=None, prefetch_queries=None): getattr(table, self.model._meta.db_pk_field) == self.Parameter(0) ).delete() ) - self.update_cache = {} # type: Dict[str, str] + self.update_cache: Dict[str, str] = {} EXECUTOR_CACHE[key] = ( self.regular_columns, @@ -170,17 +170,17 @@ async def execute_delete(self, instance): async def _prefetch_reverse_relation( self, instance_list: list, field: str, related_query ) -> list: - instance_id_set = { + instance_id_set: set = { self._field_to_db(instance._meta.pk, instance.pk, instance) for instance in instance_list - } # type: Set[Any] + } relation_field = self.model._meta.fields_map[field].relation_field related_object_list = await related_query.filter( **{f"{relation_field}__in": list(instance_id_set)} ) - related_object_map = {} # type: Dict[str, list] + related_object_map: Dict[str, list] = {} for entry in related_object_list: object_id = getattr(entry, relation_field) if object_id in related_object_map.keys(): @@ -193,10 +193,10 @@ async def _prefetch_reverse_relation( return instance_list async def _prefetch_m2m_relation(self, instance_list: list, field: str, related_query) -> list: - instance_id_set = { + instance_id_set: set = { self._field_to_db(instance._meta.pk, instance.pk, instance) for instance in instance_list - } # type: Set[Any] + } field_object = self.model._meta.fields_map[field] @@ -223,7 +223,7 @@ async def _prefetch_m2m_relation(self, instance_list: list, field: str, related_ ) if related_query._q_objects: - joined_tables = [] # type: List[Table] + joined_tables: List[Table] = [] modifier = QueryModifier() for node in related_query._q_objects: modifier &= node.resolve( @@ -257,7 +257,7 @@ async def _prefetch_m2m_relation(self, instance_list: list, field: str, related_ model=related_query.model, db=self.db, prefetch_map=related_query._prefetch_map ).fetch_for_list(related_object_list) related_object_map = {e.pk: e for e in related_object_list} - relation_map = {} # type: Dict[str, list] + relation_map: Dict[str, list] = {} for object_id, related_object_id in relations: if object_id not in relation_map: diff --git a/tortoise/backends/base/schema_generator.py b/tortoise/backends/base/schema_generator.py index e241ac8b2..3825336c2 100644 --- a/tortoise/backends/base/schema_generator.py +++ b/tortoise/backends/base/schema_generator.py @@ -1,6 +1,6 @@ import logging import warnings -from typing import List, Optional, Set # noqa +from typing import List, Optional, Set from tortoise import fields from tortoise.exceptions import ConfigurationError @@ -94,7 +94,7 @@ def _table_generate_extra(self, table: str) -> str: # pylint: disable=R0201 @staticmethod def _make_hash(*args: str, length: int) -> str: # Hash a set of string values and get a digest of the given length. - letters = [] # type: List[str] + letters: List[str] = [] for i_th_letters in zip(*args): letters.extend(i_th_letters) return "".join([str(ord(letter)) for letter in letters])[:length] @@ -297,7 +297,7 @@ def _get_models_to_create(self, models_to_create) -> None: models_to_create.append(model) def get_create_schema_sql(self, safe=True) -> str: - models_to_create = [] # type: List + models_to_create: list = [] self._get_models_to_create(models_to_create) @@ -307,9 +307,9 @@ def get_create_schema_sql(self, safe=True) -> str: tables_to_create_count = len(tables_to_create) - created_tables = set() # type: Set[dict] - ordered_tables_for_create = [] - m2m_tables_to_create = [] # type: List[str] + created_tables: Set[dict] = set() + ordered_tables_for_create: List[str] = [] + m2m_tables_to_create: List[str] = [] while True: if len(created_tables) == tables_to_create_count: break diff --git a/tortoise/backends/mysql/client.py b/tortoise/backends/mysql/client.py index faf4aa475..0cab9fa78 100644 --- a/tortoise/backends/mysql/client.py +++ b/tortoise/backends/mysql/client.py @@ -1,7 +1,7 @@ import asyncio import logging from functools import wraps -from typing import List, Optional, SupportsInt # noqa +from typing import List, Optional, SupportsInt import aiomysql import pymysql @@ -99,8 +99,8 @@ def __init__( self.extra.pop("autocommit", None) self.charset = self.extra.pop("charset", "") - self._template = {} # type: dict - self._connection = None # Type: Optional[aiomysql.Connection] + self._template: dict = {} + self._connection: Optional[aiomysql.Connection] = None self._lock = asyncio.Lock() self._transaction_class = type( @@ -194,11 +194,11 @@ async def execute_script(self, query: str) -> None: class TransactionWrapper(MySQLClient, BaseTransactionWrapper): def __init__(self, connection): self.connection_name = connection.connection_name - self._connection = connection._connection # type: aiomysql.Connection + self._connection: aiomysql.Connection = connection._connection self._lock = connection._lock self.log = logging.getLogger("db_client") self._transaction_class = self.__class__ - self._finalized = None # type: Optional[bool] + self._finalized: Optional[bool] = None self._old_context_value = None self.fetch_inserted = connection.fetch_inserted self._parent = connection diff --git a/tortoise/backends/sqlite/client.py b/tortoise/backends/sqlite/client.py index 1b45410f2..f984b9948 100644 --- a/tortoise/backends/sqlite/client.py +++ b/tortoise/backends/sqlite/client.py @@ -3,7 +3,7 @@ import os import sqlite3 from functools import wraps -from typing import List, Optional # noqa +from typing import List, Optional import aiosqlite @@ -48,7 +48,7 @@ def __init__(self, file_path: str, **kwargs) -> None: self._transaction_class = type( "TransactionWrapper", (TransactionWrapper, self.__class__), {} ) - self._connection = None # type: Optional[aiosqlite.Connection] + self._connection: Optional[aiosqlite.Connection] = None self._lock = asyncio.Lock() async def create_connection(self, with_db: bool) -> None: @@ -131,7 +131,7 @@ def __init__( self, connection_name: str, connection: aiosqlite.Connection, lock, fetch_inserted ) -> None: self.connection_name = connection_name - self._connection = connection # type: aiosqlite.Connection + self._connection: aiosqlite.Connection = connection self._lock = lock self.log = logging.getLogger("db_client") self._transaction_class = self.__class__ diff --git a/tortoise/contrib/pylint/__init__.py b/tortoise/contrib/pylint/__init__.py index 092cd4dc6..c1a362144 100644 --- a/tortoise/contrib/pylint/__init__.py +++ b/tortoise/contrib/pylint/__init__.py @@ -7,8 +7,8 @@ from astroid.node_classes import Assign from astroid.nodes import ClassDef -MODELS = {} # type: dict -FUTURE_RELATIONS = {} # type: dict +MODELS: dict = {} +FUTURE_RELATIONS: dict = {} def register(linter) -> None: diff --git a/tortoise/contrib/test/__init__.py b/tortoise/contrib/test/__init__.py index 3df50d5e1..893cf6ec2 100644 --- a/tortoise/contrib/test/__init__.py +++ b/tortoise/contrib/test/__init__.py @@ -38,12 +38,12 @@ On success it will be marked as unexpected success. """ -_CONFIG = {} # type: dict -_CONNECTIONS = {} # type: dict +_CONFIG: dict = {} +_CONNECTIONS: dict = {} _SELECTOR = None -_LOOP = None # type: BaseSelectorEventLoop -_MODULES = [] # type: List[str] -_CONN_MAP = {} # type: dict +_LOOP: BaseSelectorEventLoop = None +_MODULES: List[str] = [] +_CONN_MAP: dict = {} def getDBConfig(app_label: str, modules: List[str]) -> dict: diff --git a/tortoise/fields.py b/tortoise/fields.py index d077098b9..d6604c009 100644 --- a/tortoise/fields.py +++ b/tortoise/fields.py @@ -69,7 +69,7 @@ def __init__( self.null = null self.unique = unique self.index = index - self.model_field_name = "" # type: str + self.model_field_name = "" self.model = model self.reference = reference self.description = description @@ -485,7 +485,7 @@ def __init__(self, model, relation_field: str, instance) -> None: self.instance = instance self._fetched = False self._custom_query = False - self.related_objects = [] # type: list + self.related_objects: list = [] @property def _query(self): diff --git a/tortoise/filters.py b/tortoise/filters.py index 6d19e4c70..0c9c56cb3 100644 --- a/tortoise/filters.py +++ b/tortoise/filters.py @@ -1,6 +1,6 @@ import operator from functools import partial -from typing import Dict, Optional # noqa +from typing import Dict, Optional from pypika import Table, functions from pypika.enums import SqlTypes diff --git a/tortoise/models.py b/tortoise/models.py index c2bb940a1..872be110c 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -1,6 +1,6 @@ from copy import copy, deepcopy from functools import partial -from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar from pypika import Query @@ -87,31 +87,31 @@ class MetaInfo: ) def __init__(self, meta) -> None: - self.abstract = getattr(meta, "abstract", False) # type: bool - self.table = getattr(meta, "table", "") # type: str - self.app = getattr(meta, "app", None) # type: Optional[str] - self.unique_together = get_unique_together(meta) # type: Union[Tuple, List] - self.fields = set() # type: Set[str] - self.db_fields = set() # type: Set[str] - self.m2m_fields = set() # type: Set[str] - self.fk_fields = set() # type: Set[str] - self.backward_fk_fields = set() # type: Set[str] - self.fetch_fields = set() # type: Set[str] - self.fields_db_projection = {} # type: Dict[str,str] - self.fields_db_projection_reverse = {} # type: Dict[str,str] - self._filters = {} # type: Dict[str, Dict[str, dict]] - self.filters = {} # type: Dict[str, dict] - self.fields_map = {} # type: Dict[str, fields.Field] - self._inited = False # type: bool - self.default_connection = None # type: Optional[str] - self.basequery = Query() # type: Query - self.basequery_all_fields = Query() # type: Query - self.pk_attr = getattr(meta, "pk_attr", "") # type: str - self.generated_db_fields = None # type: Tuple[str] # type: ignore - self._model = None # type: "Model" # type: ignore - self.table_description = getattr(meta, "table_description", "") # type: str - self.pk = None # type: fields.Field # type: ignore - self.db_pk_field = "" # type: str + self.abstract: bool = getattr(meta, "abstract", False) + self.table: str = getattr(meta, "table", "") + self.app: Optional[str] = getattr(meta, "app", None) + self.unique_together: Tuple[Tuple[str, ...], ...] = get_unique_together(meta) + self.fields: Set[str] = set() + self.db_fields: Set[str] = set() + self.m2m_fields: Set[str] = set() + self.fk_fields: Set[str] = set() + self.backward_fk_fields: Set[str] = set() + self.fetch_fields: Set[str] = set() + self.fields_db_projection: Dict[str, str] = {} + self.fields_db_projection_reverse: Dict[str, str] = {} + self._filters: Dict[str, Dict[str, dict]] = {} + self.filters: Dict[str, dict] = {} + self.fields_map: Dict[str, fields.Field] = {} + self._inited: bool = False + self.default_connection: Optional[str] = None + self.basequery: Query = Query() + self.basequery_all_fields: Query = Query() + self.pk_attr: str = getattr(meta, "pk_attr", "") + self.generated_db_fields: Tuple[str] = None # type: ignore + self._model: "Model" = None # type: ignore + self.table_description: str = getattr(meta, "table_description", "") + self.pk: fields.Field = None # type: ignore + self.db_pk_field: str = "" def add_field(self, name: str, value: Field): if name in self.fields_map: @@ -187,7 +187,7 @@ def finalise_fields(self) -> None: # Create lazy reverse FK fields on model. for key in self.backward_fk_fields: _key = f"_{key}" - field_object = self.fields_map[key] # type: fields.BackwardFKRelation # type: ignore + field_object: fields.BackwardFKRelation = self.fields_map[key] # type: ignore setattr( self._model, key, @@ -226,15 +226,15 @@ class ModelMeta(type): __slots__ = () def __new__(mcs, name: str, bases, attrs: dict, *args, **kwargs): - fields_db_projection = {} # type: Dict[str,str] - fields_map = {} # type: Dict[str, fields.Field] - filters = {} # type: Dict[str, Dict[str, dict]] - fk_fields = set() # type: Set[str] - m2m_fields = set() # type: Set[str] + fields_db_projection: Dict[str, str] = {} + fields_map: Dict[str, fields.Field] = {} + filters: Dict[str, Dict[str, dict]] = {} + fk_fields: Set[str] = set() + m2m_fields: Set[str] = set() meta_class = attrs.get("Meta", type("Meta", (), {})) - pk_attr = "id" + pk_attr: str = "id" - # Searching for Field attributes in the class hierarchie + # Searching for Field attributes in the class hierarchy def __search_for_field_attributes(base, attrs: dict): """ Searching for class attributes of type fields.Field @@ -336,7 +336,7 @@ def __search_for_field_attributes(base, attrs: dict): if not fields_map: meta.abstract = True - new_class = super().__new__(mcs, name, bases, attrs) # type: "Model" # type: ignore + new_class: "Model" = super().__new__(mcs, name, bases, attrs) # type: ignore for field in meta.fields_map.values(): field.model = new_class @@ -396,7 +396,7 @@ def _set_field_values(self, values_map: Dict[str, Any]) -> Set[str]: f"You should first call .save() on {value} before referring to it" ) field_object = meta.fields_map[key] - relation_field = field_object.source_field # type: str # type: ignore + relation_field: str = field_object.source_field # type: ignore setattr(self, key, value) passed_fields.add(relation_field) elif key in meta.fields_db_projection: diff --git a/tortoise/query_utils.py b/tortoise/query_utils.py index fb56397fe..be99b699e 100644 --- a/tortoise/query_utils.py +++ b/tortoise/query_utils.py @@ -1,5 +1,5 @@ from copy import copy -from typing import Any, List, Mapping, Optional, Tuple # noqa +from typing import Any, Dict, List, Optional, Tuple from pypika import Table from pypika.terms import Criterion @@ -173,14 +173,14 @@ def __init__(self, *args: "Q", join_type=AND, **kwargs) -> None: raise OperationalError("You can pass only Q nodes or filter kwargs in one Q node") if not all(isinstance(node, Q) for node in args): raise OperationalError("All ordered arguments must be Q nodes") - self.children = args # type: Tuple[Q, ...] - self.filters = kwargs # type: Mapping[str, Any] + self.children: Tuple[Q, ...] = args + self.filters: Dict[str, Any] = kwargs if join_type not in {self.AND, self.OR}: raise OperationalError("join_type must be AND or OR") self.join_type = join_type self._is_negated = False - self._annotations = {} # type: Mapping[str, Any] - self._custom_filters = {} # type: Mapping[str, Mapping[str, Any]] + self._annotations: Dict[str, Any] = {} + self._custom_filters: Dict[str, Dict[str, Any]] = {} def __and__(self, other) -> "Q": if not isinstance(other, Q): diff --git a/tortoise/queryset.py b/tortoise/queryset.py index d2ec2d278..f11e7f048 100644 --- a/tortoise/queryset.py +++ b/tortoise/queryset.py @@ -1,5 +1,5 @@ from copy import copy -from typing import Any, Dict, List, Optional, Set, Tuple # noqa +from typing import Any, Dict, List, Optional, Set, Tuple from pypika import JoinType, Order, Query, Table # noqa from pypika.functions import Count @@ -19,10 +19,10 @@ class AwaitableQuery: __slots__ = ("_joined_tables", "query", "model", "_db", "capabilities") def __init__(self, model) -> None: - self._joined_tables = [] # type: List[Table] + self._joined_tables: List[Table] = [] self.model = model - self.query = QUERY # type: Query - self._db = None # type: Optional[BaseDBAsyncClient] + self.query: Query = QUERY + self._db: Optional[BaseDBAsyncClient] = None self.capabilities = model._meta.db.capabilities def resolve_filters(self, model, q_objects, annotations, custom_filters) -> None: @@ -112,20 +112,20 @@ def __init__(self, model) -> None: super().__init__(model) self.fields = model._meta.db_fields - self._prefetch_map = {} # type: Dict[str, Set[str]] - self._prefetch_queries = {} # type: Dict[str, QuerySet] - self._single = False # type: bool - self._get = False # type: bool - self._count = False # type: bool - self._limit = None # type: Optional[int] - self._offset = None # type: Optional[int] - self._filter_kwargs = {} # type: Dict[str, Any] - self._orderings = [] # type: List[Tuple[str, Any]] - self._q_objects = [] # type: List[Q] - self._distinct = False # type: bool - self._annotations = {} # type: Dict[str, Aggregate] - self._having = {} # type: Dict[str, Any] - self._custom_filters = {} # type: Dict[str, dict] + self._prefetch_map: Dict[str, Set[str]] = {} + self._prefetch_queries: Dict[str, QuerySet] = {} + self._single: bool = False + self._get: bool = False + self._count: bool = False + self._limit: Optional[int] = None + self._offset: Optional[int] = None + self._filter_kwargs: Dict[str, Any] = {} + self._orderings: List[Tuple[str, Any]] = [] + self._q_objects: List[Q] = [] + self._distinct: bool = False + self._annotations: Dict[str, Aggregate] = {} + self._having: Dict[str, Any] = {} + self._custom_filters: Dict[str, dict] = {} def _clone(self) -> "QuerySet": queryset = QuerySet.__new__(QuerySet) @@ -298,7 +298,7 @@ def values(self, *args: str, **kwargs: str) -> "ValuesQuery": If no arguments are passed it will default to a dict containing all fields. """ if args or kwargs: - fields_for_select = {} # type: Dict[str, str] + fields_for_select: Dict[str, str] = {} for field in args: if field in fields_for_select: raise FieldError(f"Duplicate key {field}") diff --git a/tortoise/transactions.py b/tortoise/transactions.py index 6d5cfa1a5..bf1dce3cb 100644 --- a/tortoise/transactions.py +++ b/tortoise/transactions.py @@ -1,10 +1,10 @@ from functools import wraps -from typing import Callable, Dict, Optional # noqa +from typing import Callable, Optional from tortoise.backends.base.client import BaseDBAsyncClient, BaseTransactionWrapper from tortoise.exceptions import ParamsError -current_transaction_map = {} # type: Dict +current_transaction_map: dict = {} def _get_connection(connection_name: Optional[str]) -> BaseDBAsyncClient: diff --git a/tortoise/utils.py b/tortoise/utils.py index 8349c4286..e6fdfdbdf 100644 --- a/tortoise/utils.py +++ b/tortoise/utils.py @@ -9,7 +9,7 @@ class QueryAsyncIterator: def __init__(self, query: Awaitable[Iterator], callback: Optional[Callable] = None) -> None: self.query = query - self.sequence = None # type: Optional[Iterator] + self.sequence: Optional[Iterator] = None self._sequence_iterator = None self._callback = callback From 5f77d2fc204d30cde312d64c88f3be33526c6422 Mon Sep 17 00:00:00 2001 From: Grigi Date: Wed, 11 Sep 2019 15:42:57 +0200 Subject: [PATCH 03/10] Some more type annotations --- setup.cfg | 2 ++ tortoise/backends/asyncpg/client.py | 13 +++++---- tortoise/backends/base/client.py | 15 +++++----- tortoise/backends/base/executor.py | 32 ++++++++++++++-------- tortoise/backends/base/schema_generator.py | 2 +- tortoise/backends/mysql/client.py | 2 +- tortoise/contrib/quart/__init__.py | 6 ++-- tortoise/fields.py | 2 +- tortoise/models.py | 5 +--- tortoise/queryset.py | 29 ++++++++++---------- 10 files changed, 58 insertions(+), 50 deletions(-) diff --git a/setup.cfg b/setup.cfg index 0fb3a46bc..c699d2649 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ warn_unused_ignores = True warn_no_return = True warn_return_any = False warn_unused_configs = True +warn_unreachable = True allow_redefinition = True strict_equality = True show_error_context = True @@ -40,6 +41,7 @@ show_error_context = True check_untyped_defs = False disallow_untyped_defs = False disallow_incomplete_defs = False +warn_unreachable = False [mypy-examples.*] check_untyped_defs = False diff --git a/tortoise/backends/asyncpg/client.py b/tortoise/backends/asyncpg/client.py index 177f09591..34f35a906 100644 --- a/tortoise/backends/asyncpg/client.py +++ b/tortoise/backends/asyncpg/client.py @@ -4,6 +4,7 @@ from typing import List, Optional, SupportsInt import asyncpg +from asyncpg.transaction import Transaction from pypika import PostgreSQLQuery from tortoise.backends.asyncpg.executor import AsyncpgExecutor @@ -185,14 +186,14 @@ async def execute_script(self, query: str) -> None: class TransactionWrapper(AsyncpgDBClient, BaseTransactionWrapper): - def __init__(self, connection) -> None: - self._connection = connection._connection + def __init__(self, connection: AsyncpgDBClient) -> None: + self._connection: asyncpg.Connection = connection._connection self._lock = connection._lock self.log = logging.getLogger("db_client") self._transaction_class = self.__class__ self._old_context_value = None self.connection_name = connection.connection_name - self.transaction = None + self.transaction: Transaction = None self._finalized = False self._parent = connection @@ -205,7 +206,7 @@ async def _close(self) -> None: self._connection = self._parent._connection @retry_connection - async def start(self): + async def start(self) -> None: self.transaction = self._connection.transaction() await self.transaction.start() current_transaction = current_transaction_map[self.connection_name] @@ -216,13 +217,13 @@ def release(self) -> None: self._finalized = True current_transaction_map[self.connection_name].set(self._old_context_value) - async def commit(self): + async def commit(self) -> None: if self._finalized: raise TransactionManagementError("Transaction already finalised") await self.transaction.commit() self.release() - async def rollback(self): + async def rollback(self) -> None: if self._finalized: raise TransactionManagementError("Transaction already finalised") await self.transaction.rollback() diff --git a/tortoise/backends/base/client.py b/tortoise/backends/base/client.py index 52b2ad3b9..d13169f8f 100644 --- a/tortoise/backends/base/client.py +++ b/tortoise/backends/base/client.py @@ -1,5 +1,6 @@ +import asyncio import logging -from typing import Any, Optional, Sequence +from typing import Any, Optional, Sequence, Type from pypika import Query @@ -59,10 +60,10 @@ def __str__(self) -> str: class BaseDBAsyncClient: - query_class = Query - executor_class = BaseExecutor - schema_generator = BaseSchemaGenerator - capabilities = Capabilities("") + query_class: Type[Query] = Query + executor_class: Type[BaseExecutor] = BaseExecutor + schema_generator: Type[BaseSchemaGenerator] = BaseSchemaGenerator + capabilities: Capabilities = Capabilities("") def __init__(self, connection_name: str, fetch_inserted: bool = True, **kwargs) -> None: self.log = logging.getLogger("db_client") @@ -103,9 +104,9 @@ async def execute_script(self, query: str) -> None: class ConnectionWrapper: __slots__ = ("connection", "lock") - def __init__(self, connection, lock) -> None: + def __init__(self, connection, lock: asyncio.Lock) -> None: self.connection = connection - self.lock = lock + self.lock: asyncio.Lock = lock async def __aenter__(self): await self.lock.acquire() diff --git a/tortoise/backends/base/executor.py b/tortoise/backends/base/executor.py index 3631267f8..255c820df 100644 --- a/tortoise/backends/base/executor.py +++ b/tortoise/backends/base/executor.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: # pragma: nocoverage from tortoise.models import Model + from tortoise.backends.base.client import BaseDBAsyncClient EXECUTOR_CACHE: Dict[str, Tuple[list, str, Dict[str, Callable], str, Dict[str, str]]] = {} @@ -17,11 +18,17 @@ class BaseExecutor: TO_DB_OVERRIDE: Dict[Type[fields.Field], Callable] = {} FILTER_FUNC_OVERRIDE: Dict[Callable, Callable] = {} - EXPLAIN_PREFIX = "EXPLAIN" - - def __init__(self, model, db=None, prefetch_map=None, prefetch_queries=None): + EXPLAIN_PREFIX: str = "EXPLAIN" + + def __init__( + self, + model: "Type[Model]", + db: "BaseDBAsyncClient", + prefetch_map=None, + prefetch_queries=None, + ) -> None: self.model = model - self.db = db + self.db: "BaseDBAsyncClient" = db self.prefetch_map = prefetch_map if prefetch_map else {} self._prefetch_queries = prefetch_queries if prefetch_queries else {} @@ -30,14 +37,15 @@ def __init__(self, model, db=None, prefetch_map=None, prefetch_queries=None): self.regular_columns, columns = self._prepare_insert_columns() self.insert_query = self._prepare_insert_statement(columns) - self.column_map: Dict[str, Callable] = {} + self.column_map: Dict[str, Callable[[Any, Any], Any]] = {} for column in self.regular_columns: field_object = self.model._meta.fields_map[column] if field_object.__class__ in self.TO_DB_OVERRIDE: - func = partial(self.TO_DB_OVERRIDE[field_object.__class__], field_object) + self.column_map[column] = partial( + self.TO_DB_OVERRIDE[field_object.__class__], field_object + ) else: - func = field_object.to_db_value - self.column_map[column] = func + self.column_map[column] = field_object.to_db_value table = Table(self.model._meta.table) self.delete_query = str( @@ -71,7 +79,7 @@ async def execute_select(self, query, custom_fields: Optional[list] = None) -> l raw_results = await self.db.execute_query(query.get_sql()) instance_list = [] for row in raw_results: - instance = self.model._init_from_db(**row) + instance: "Model" = self.model._init_from_db(**row) if custom_fields: for field in custom_fields: setattr(instance, field, row[field]) @@ -174,7 +182,7 @@ async def _prefetch_reverse_relation( self._field_to_db(instance._meta.pk, instance.pk, instance) for instance in instance_list } - relation_field = self.model._meta.fields_map[field].relation_field + relation_field = self.model._meta.fields_map[field].relation_field # type: ignore related_object_list = await related_query.filter( **{f"{relation_field}__in": list(instance_id_set)} @@ -198,7 +206,7 @@ async def _prefetch_m2m_relation(self, instance_list: list, field: str, related_ for instance in instance_list } - field_object = self.model._meta.fields_map[field] + field_object: fields.ManyToManyField = self.model._meta.fields_map[field] # type: ignore through_table = Table(field_object.through) @@ -291,7 +299,7 @@ def _make_prefetch_queries(self) -> None: if field in self._prefetch_queries: related_query = self._prefetch_queries.get(field) else: - related_model_field = self.model._meta.fields_map.get(field) + related_model_field = self.model._meta.fields_map[field] related_model = related_model_field.type related_query = related_model.all().using_db(self.db) related_query.query = copy(related_query.model._meta.basequery) diff --git a/tortoise/backends/base/schema_generator.py b/tortoise/backends/base/schema_generator.py index 3825336c2..896385c58 100644 --- a/tortoise/backends/base/schema_generator.py +++ b/tortoise/backends/base/schema_generator.py @@ -199,7 +199,7 @@ def _get_table_sql(self, model, safe=True) -> dict: if field_object.index: fields_with_index.append(db_field) - if model._meta.unique_together is not None: + if model._meta.unique_together: unique_together_sqls = [] for unique_together_list in model._meta.unique_together: diff --git a/tortoise/backends/mysql/client.py b/tortoise/backends/mysql/client.py index 0cab9fa78..7320353d2 100644 --- a/tortoise/backends/mysql/client.py +++ b/tortoise/backends/mysql/client.py @@ -212,7 +212,7 @@ async def _close(self) -> None: self._connection = self._parent._connection @retry_connection - async def start(self): + async def start(self) -> None: await self._connection.begin() self._finalized = False current_transaction = current_transaction_map[self.connection_name] diff --git a/tortoise/contrib/quart/__init__.py b/tortoise/contrib/quart/__init__.py index d62de2f6f..b6d87c908 100644 --- a/tortoise/contrib/quart/__init__.py +++ b/tortoise/contrib/quart/__init__.py @@ -74,7 +74,7 @@ def register_tortoise( """ @app.before_serving - async def init_orm(): # pylint: disable=W0612 + async def init_orm() -> None: # pylint: disable=W0612 await Tortoise.init(config=config, config_file=config_file, db_url=db_url, modules=modules) logging.info("Tortoise-ORM started, %s, %s", Tortoise._connections, Tortoise.apps) if generate_schemas: @@ -82,12 +82,12 @@ async def init_orm(): # pylint: disable=W0612 await Tortoise.generate_schemas() @app.after_serving - async def close_orm(): # pylint: disable=W0612 + async def close_orm() -> None: # pylint: disable=W0612 await Tortoise.close_connections() logging.info("Tortoise-ORM shutdown") @app.cli.command() # type: ignore - def generate_schemas(): # pylint: disable=E0102 + def generate_schemas() -> None: # pylint: disable=E0102 """Populate DB with Tortoise-ORM schemas.""" async def inner() -> None: diff --git a/tortoise/fields.py b/tortoise/fields.py index d6604c009..049a284c9 100644 --- a/tortoise/fields.py +++ b/tortoise/fields.py @@ -382,7 +382,7 @@ def __init__( self, model_name: str, related_name: Optional[str] = None, on_delete=CASCADE, **kwargs ) -> None: super().__init__(**kwargs) - if isinstance(model_name, str) and len(model_name.split(".")) != 2: + if len(model_name.split(".")) != 2: raise ConfigurationError('Foreign key accepts model name in format "app.Model"') self.model_name = model_name self.related_name = related_name diff --git a/tortoise/models.py b/tortoise/models.py index 872be110c..4f5d0155b 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -22,7 +22,7 @@ def get_unique_together(meta) -> Tuple[Tuple[str, ...], ...]: - unique_together = getattr(meta, "unique_together", None) + unique_together = getattr(meta, "unique_together", ()) if isinstance(unique_together, (list, tuple)): if unique_together and isinstance(unique_together[0], str): @@ -618,9 +618,6 @@ def check(cls) -> None: @classmethod def _check_unique_together(cls) -> None: """Check the value of "unique_together" option.""" - if cls._meta.unique_together is None: - return - if not isinstance(cls._meta.unique_together, (tuple, list)): raise ConfigurationError(f"'{cls.__name__}.unique_together' must be a list or tuple.") diff --git a/tortoise/queryset.py b/tortoise/queryset.py index f11e7f048..e3405dacb 100644 --- a/tortoise/queryset.py +++ b/tortoise/queryset.py @@ -1,5 +1,5 @@ from copy import copy -from typing import Any, Dict, List, Optional, Set, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple, Union from pypika import JoinType, Order, Query, Table # noqa from pypika.functions import Count @@ -22,7 +22,7 @@ def __init__(self, model) -> None: self._joined_tables: List[Table] = [] self.model = model self.query: Query = QUERY - self._db: Optional[BaseDBAsyncClient] = None + self._db: BaseDBAsyncClient = None # type: ignore self.capabilities = model._meta.db.capabilities def resolve_filters(self, model, q_objects, annotations, custom_filters) -> None: @@ -72,7 +72,7 @@ def resolve_ordering(self, model, orderings, annotations) -> None: field_name = field_object.source_field or field_name self.query = self.query.orderby(getattr(table, field_name), order=ordering[1]) - def _make_query(self): + def _make_query(self) -> None: raise NotImplementedError() # pragma: nocoverage def __await__(self): @@ -390,7 +390,7 @@ def get(self, *args, **kwargs) -> "QuerySet": queryset._get = True return queryset - def prefetch_related(self, *args: str) -> "QuerySet": + def prefetch_related(self, *args: Union[str, Prefetch]) -> "QuerySet": """ Like ``.fetch_related()`` on instance, but works on all objects in QuerySet. """ @@ -435,10 +435,10 @@ async def explain(self) -> Any: **The output format may (and will) vary greatly depending on the database backend.** """ if self._db is None: - self._db = self.model._meta.db - + self._db = self.model._meta.db # type: ignore + self._make_query() return await self._db.executor_class(model=self.model, db=self._db).execute_explain( - self._make_query() + self.query ) def using_db(self, _db: BaseDBAsyncClient) -> "QuerySet": @@ -461,7 +461,7 @@ def _resolve_annotate(self) -> None: self._join_table_by_field(*join) self.query._select_other(aggregation_info["field"].as_(key)) - def _make_query(self) -> Query: + def _make_query(self) -> None: self.query = copy(self.model._meta.basequery_all_fields) self._resolve_annotate() self.resolve_filters( @@ -477,7 +477,6 @@ def _make_query(self) -> Query: if self._distinct: self.query._distinct = True self.resolve_ordering(self.model, self._orderings, self._annotations) - return self.query async def _execute(self): instance_list = await self._db.executor_class( @@ -510,7 +509,7 @@ def __init__(self, model, update_kwargs, db, q_objects, annotations, custom_filt self.custom_filters = custom_filters self._db = db - def _make_query(self): + def _make_query(self) -> None: table = Table(self.model._meta.table) self.query = self._db.query_class.update(table) self.resolve_filters( @@ -531,7 +530,7 @@ def _make_query(self): if isinstance(field_object, fields.ForeignKeyField): fk_field = field_object.source_field db_field = self.model._meta.fields_map[fk_field].source_field - value = executor.column_map[fk_field](value.id, None) + value = executor.column_map[fk_field](value.pk, None) # type: ignore else: try: db_field = self.model._meta.fields_db_projection[key] @@ -555,7 +554,7 @@ def __init__(self, model, db, q_objects, annotations, custom_filters) -> None: self.custom_filters = custom_filters self._db = db - def _make_query(self): + def _make_query(self) -> None: self.query = copy(self.model._meta.basequery) self.resolve_filters( model=self.model, @@ -579,7 +578,7 @@ def __init__(self, model, db, q_objects, annotations, custom_filters) -> None: self.custom_filters = custom_filters self._db = db - def _make_query(self): + def _make_query(self) -> None: self.query = copy(self.model._meta.basequery) self.resolve_filters( model=self.model, @@ -711,7 +710,7 @@ def __init__( self.flat = flat self._db = db - def _make_query(self): + def _make_query(self) -> None: self.query = copy(self.model._meta.basequery) for positional_number, field in self.fields.items(): self.add_field_to_select_query(field, positional_number) @@ -778,7 +777,7 @@ def __init__( self.q_objects = q_objects self._db = db - def _make_query(self): + def _make_query(self) -> None: self.query = copy(self.model._meta.basequery) for return_as, field in self.fields_for_select.items(): self.add_field_to_select_query(field, return_as) From e776bc4db7caeb844c9fd82e08e5152c0c2d6ccc Mon Sep 17 00:00:00 2001 From: Grigi Date: Sat, 14 Sep 2019 15:30:41 +0200 Subject: [PATCH 04/10] Lighter _init_from_db? --- examples/relations_recursive.py | 40 ++++++++++++++--------------- tortoise/models.py | 45 +++++++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/examples/relations_recursive.py b/examples/relations_recursive.py index f27406437..c5f23ea14 100644 --- a/examples/relations_recursive.py +++ b/examples/relations_recursive.py @@ -20,23 +20,23 @@ class Employee(Model): def __str__(self): return self.name - # async def full_hierarchy__async_for(self, level=0): - # """ - # Demonstrates ``async for` to fetch relations - # - # An async iterator will fetch the relationship on-demand. - # """ - # text = [ - # "{}{} (to: {}) (from: {})".format( - # level * " ", - # self, - # ", ".join([str(val) async for val in self.talks_to]), - # ", ".join([str(val) async for val in self.gets_talked_to]), - # ) - # ] - # async for member in self.team_members: - # text.append(await member.full_hierarchy__async_for(level + 1)) - # return "\n".join(text) + async def full_hierarchy__async_for(self, level=0): + """ + Demonstrates ``async for` to fetch relations + + An async iterator will fetch the relationship on-demand. + """ + text = [ + "{}{} (to: {}) (from: {})".format( + level * " ", + self, + ", ".join([str(val) async for val in self.talks_to]), + ", ".join([str(val) async for val in self.gets_talked_to]), + ) + ] + async for member in self.team_members: + text.append(await member.full_hierarchy__async_for(level + 1)) + return "\n".join(text) async def full_hierarchy__fetch_related(self, level=0): """ @@ -44,7 +44,7 @@ async def full_hierarchy__fetch_related(self, level=0): On prefetching the data, the relationship files will contain a regular list. - This is how one would get relations working on sync serialisation/templating frameworks. + This is how one would get relations working on sync serialization/templating frameworks. """ await self.fetch_related("team_members", "talks_to", "gets_talked_to") text = [ @@ -78,14 +78,14 @@ async def run(): # Evaluated off creation objects print(await loose.full_hierarchy__fetch_related()) - # print(await root.full_hierarchy__async_for()) + print(await root.full_hierarchy__async_for()) print(await root.full_hierarchy__fetch_related()) # Evaluated off new objects → Result is identical root2 = await Employee.get(name="Root") loose2 = await Employee.get(name="Loose") print(await loose2.full_hierarchy__fetch_related()) - # print(await root2.full_hierarchy__async_for()) + print(await root2.full_hierarchy__async_for()) print(await root2.full_hierarchy__fetch_related()) diff --git a/tortoise/models.py b/tortoise/models.py index 4f5d0155b..b9ce7fadc 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -241,14 +241,14 @@ def __search_for_field_attributes(base, attrs: dict): in the given class. If an attribute of the class is an instance of fields.Field, - then it will be added to the attrs dict. But only, if the + then it will be added to the fields dict. But only, if the key is not already in the dict. So derived classes have a higher - precedence. Multiple Inheritence is supported from left to right. + precedence. Multiple Inheritance is supported from left to right. After checking the given class, the function will look into - the classes according to the mro (method resolution order). + the classes according to the MRO (method resolution order). - The mro is 'natural' order, in wich python traverses methods and + The MRO is 'natural' order, in which python traverses methods and fields. For more information on the magic behind check out: `The Python 2.3 Method Resolution Order `_. @@ -346,7 +346,7 @@ def __search_for_field_attributes(base, attrs: dict): class Model(metaclass=ModelMeta): - # I don' like this here, but it makes autocompletion and static analysis much happier + # I don' like this here, but it makes auto completion and static analysis much happier _meta = MetaInfo(None) def __init__(self, *args, **kwargs) -> None: @@ -374,10 +374,33 @@ def _init_from_db(cls, **kwargs) -> MODEL_TYPE: meta = self._meta - for key, value in kwargs.items(): - model_field = meta.fields_db_projection_reverse.get(key) - if model_field: - setattr(self, model_field, meta.fields_map[model_field].to_python_value(value)) + # WIP: lighter _init_from_db? + # e.g. meta.db_native_fields, meta.db_default_fields and meta.db_complex_fields + # have it resolve key, model_field and field as tuples? + for key in meta.db_fields: + value = kwargs[key] + model_field = meta.fields_db_projection_reverse[key] + field = meta.fields_map[model_field] + default_converter = field.__class__.to_python_value is fields.Field.to_python_value + nullable = field.null + db_native = field.type in {str, int, bool, float} + if default_converter and db_native: + # print(1) + setattr(self, model_field, value) + elif default_converter and not nullable: + # print(2) + setattr(self, model_field, field.type(value)) + elif default_converter and nullable: + # print(3) + setattr(self, model_field, None if value is None else field.type(value)) + else: + # print(4) + setattr(self, model_field, field.to_python_value(value)) + + # for key, value in kwargs.items(): + # model_field = meta.fields_db_projection_reverse.get(key) + # if model_field: + # setattr(self, model_field, meta.fields_map[model_field].to_python_value(value)) return self @@ -538,7 +561,7 @@ async def bulk_create(cls: Type[MODEL_TYPE], objects: List[MODEL_TYPE], using_db created in the DB has all the defaults and generated fields set, but may be incomplete reference in Python. - e.g. ``IntField`` primary keys will not be poplulated. + e.g. ``IntField`` primary keys will not be populated. This is recommend only for throw away inserts where you want to ensure optimal insert performance. @@ -646,7 +669,7 @@ def _check_unique_together(cls) -> None: class Meta: """ - The ``Meta`` class is used to configure metadate for the Model. + The ``Meta`` class is used to configure metadata for the Model. Usage: From f0928a5046e34b1ca2ce4e68be5573922f1ffad3 Mon Sep 17 00:00:00 2001 From: Grigi Date: Sat, 14 Sep 2019 17:45:54 +0200 Subject: [PATCH 05/10] Test async comprehensions --- tortoise/tests/test_relations.py | 8 ++++---- tortoise/tests/testmodels.py | 34 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tortoise/tests/test_relations.py b/tortoise/tests/test_relations.py index 6206bb2e3..e39179a99 100644 --- a/tortoise/tests/test_relations.py +++ b/tortoise/tests/test_relations.py @@ -167,17 +167,17 @@ async def test_self_ref(self): 2.2. Third H2 (to: 2.1. Second H2) (from: )""" # Evaluated off creation objects - # self.assertEqual(await loose.full_hierarchy__async_for(), LOOSE_TEXT) + self.assertEqual(await loose.full_hierarchy__async_for(), LOOSE_TEXT) self.assertEqual(await loose.full_hierarchy__fetch_related(), LOOSE_TEXT) - # self.assertEqual(await root.full_hierarchy__async_for(), ROOT_TEXT) + self.assertEqual(await root.full_hierarchy__async_for(), ROOT_TEXT) self.assertEqual(await root.full_hierarchy__fetch_related(), ROOT_TEXT) # Evaluated off new objects → Result is identical root2 = await Employee.get(name="Root") loose2 = await Employee.get(name="Loose") - # self.assertEqual(await loose2.full_hierarchy__async_for(), LOOSE_TEXT) + self.assertEqual(await loose2.full_hierarchy__async_for(), LOOSE_TEXT) self.assertEqual(await loose2.full_hierarchy__fetch_related(), LOOSE_TEXT) - # self.assertEqual(await root2.full_hierarchy__async_for(), ROOT_TEXT) + self.assertEqual(await root2.full_hierarchy__async_for(), ROOT_TEXT) self.assertEqual(await root2.full_hierarchy__fetch_related(), ROOT_TEXT) async def test_prefetch_related_fk(self): diff --git a/tortoise/tests/testmodels.py b/tortoise/tests/testmodels.py index 7c501433f..f98346646 100644 --- a/tortoise/tests/testmodels.py +++ b/tortoise/tests/testmodels.py @@ -353,23 +353,23 @@ class Employee(Model): def __str__(self): return self.name - # async def full_hierarchy__async_for(self, level=0): - # """ - # Demonstrates ``async for` to fetch relations - # - # An async iterator will fetch the relationship on-demand. - # """ - # text = [ - # "{}{} (to: {}) (from: {})".format( - # level * " ", - # self, - # ", ".join(sorted([str(val) async for val in self.talks_to])), - # ", ".join(sorted([str(val) async for val in self.gets_talked_to])), - # ) - # ] - # async for member in self.team_members: - # text.append(await member.full_hierarchy__async_for(level + 1)) - # return "\n".join(text) + async def full_hierarchy__async_for(self, level=0): + """ + Demonstrates ``async for` to fetch relations + + An async iterator will fetch the relationship on-demand. + """ + text = [ + "{}{} (to: {}) (from: {})".format( + level * " ", + self, + ", ".join(sorted([str(val) async for val in self.talks_to])), + ", ".join(sorted([str(val) async for val in self.gets_talked_to])), + ) + ] + async for member in self.team_members: + text.append(await member.full_hierarchy__async_for(level + 1)) + return "\n".join(text) async def full_hierarchy__fetch_related(self, level=0): """ From 84b451f3a0fddee5c47869d0a7710a4c728b0a9c Mon Sep 17 00:00:00 2001 From: Grigi Date: Sun, 15 Sep 2019 05:20:04 +0200 Subject: [PATCH 06/10] Know about default converters & native DB types - 20-25% fetch speedup --- tortoise/models.py | 54 ++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/tortoise/models.py b/tortoise/models.py index b9ce7fadc..eb9c9d770 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -84,6 +84,9 @@ class MetaInfo: "table_description", "pk", "db_pk_field", + "db_native_fields", + "db_default_fields", + "db_complex_fields", ) def __init__(self, meta) -> None: @@ -112,6 +115,9 @@ def __init__(self, meta) -> None: self.table_description: str = getattr(meta, "table_description", "") self.pk: fields.Field = None # type: ignore self.db_pk_field: str = "" + self.db_native_fields: List[Tuple[str, str, fields.Field]] = [] + self.db_default_fields: List[Tuple[str, str, fields.Field]] = [] + self.db_complex_fields: List[Tuple[str, str, fields.Field]] = [] def add_field(self, name: str, value: Field): if name in self.fields_map: @@ -154,6 +160,7 @@ def finalise_model(self) -> None: """ self.finalise_fields() self._generate_filters() + self._generate_db_fields() def finalise_fields(self) -> None: self.db_fields = set(self.fields_db_projection.values()) @@ -210,6 +217,21 @@ def finalise_fields(self) -> None: property(partial(_m2m_getter, _key=_key, field_object=self.fields_map[key])), ) + def _generate_db_fields(self) -> None: + for key in self.db_fields: + model_field = self.fields_db_projection_reverse[key] + field = self.fields_map[model_field] + + default_converter = field.__class__.to_python_value is fields.Field.to_python_value + # TODO: Get this set from DB driver? + db_native = field.type in {str, int, bool, float} + if not default_converter: + self.db_complex_fields.append((key, model_field, field)) + elif db_native: + self.db_native_fields.append((key, model_field, field)) + else: + self.db_default_fields.append((key, model_field, field)) + def _generate_filters(self) -> None: get_overridden_filter_func = self.db.executor_class.get_overridden_filter_func for key, filter_info in self._filters.items(): @@ -374,33 +396,13 @@ def _init_from_db(cls, **kwargs) -> MODEL_TYPE: meta = self._meta - # WIP: lighter _init_from_db? - # e.g. meta.db_native_fields, meta.db_default_fields and meta.db_complex_fields - # have it resolve key, model_field and field as tuples? - for key in meta.db_fields: + for key, model_field, field in meta.db_native_fields: + setattr(self, model_field, kwargs[key]) + for key, model_field, field in meta.db_default_fields: value = kwargs[key] - model_field = meta.fields_db_projection_reverse[key] - field = meta.fields_map[model_field] - default_converter = field.__class__.to_python_value is fields.Field.to_python_value - nullable = field.null - db_native = field.type in {str, int, bool, float} - if default_converter and db_native: - # print(1) - setattr(self, model_field, value) - elif default_converter and not nullable: - # print(2) - setattr(self, model_field, field.type(value)) - elif default_converter and nullable: - # print(3) - setattr(self, model_field, None if value is None else field.type(value)) - else: - # print(4) - setattr(self, model_field, field.to_python_value(value)) - - # for key, value in kwargs.items(): - # model_field = meta.fields_db_projection_reverse.get(key) - # if model_field: - # setattr(self, model_field, meta.fields_map[model_field].to_python_value(value)) + setattr(self, model_field, None if value is None else field.type(value)) + for key, model_field, field in meta.db_complex_fields: + setattr(self, model_field, field.to_python_value(kwargs[key])) return self From 576b4a29461fd9b6feae5be4d4c41183cde565a1 Mon Sep 17 00:00:00 2001 From: Grigi Date: Sun, 15 Sep 2019 05:29:38 +0200 Subject: [PATCH 07/10] Update perf graph --- docs/ORM_Perf.png | Bin 35926 -> 36038 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/ORM_Perf.png b/docs/ORM_Perf.png index c7dd5c12dbf7fe2ba0acf6e6108813517f0798d7..8c3669e973a61acaba98a610cbdd9aff6888a1bd 100644 GIT binary patch literal 36038 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 Date: Sun, 15 Sep 2019 21:28:04 +0200 Subject: [PATCH 08/10] DB-specific types, clean class attrs to make slots possible --- tortoise/backends/asyncpg/executor.py | 2 ++ tortoise/backends/base/executor.py | 3 +++ tortoise/backends/sqlite/executor.py | 1 + tortoise/models.py | 25 +++++++++---------------- tortoise/tests/test_inheritence.py | 12 ++++++------ 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/tortoise/backends/asyncpg/executor.py b/tortoise/backends/asyncpg/executor.py index fea68e8ed..d2cb7405b 100644 --- a/tortoise/backends/asyncpg/executor.py +++ b/tortoise/backends/asyncpg/executor.py @@ -1,3 +1,4 @@ +import uuid from typing import List, Optional import asyncpg @@ -9,6 +10,7 @@ class AsyncpgExecutor(BaseExecutor): EXPLAIN_PREFIX = "EXPLAIN (FORMAT JSON, VERBOSE)" + DB_NATIVE = BaseExecutor.DB_NATIVE | {uuid.UUID} def Parameter(self, pos: int) -> Parameter: return Parameter("$%d" % (pos + 1,)) diff --git a/tortoise/backends/base/executor.py b/tortoise/backends/base/executor.py index 255c820df..3cf46d1a2 100644 --- a/tortoise/backends/base/executor.py +++ b/tortoise/backends/base/executor.py @@ -1,3 +1,5 @@ +import datetime +import decimal from copy import copy from functools import partial from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Type @@ -19,6 +21,7 @@ class BaseExecutor: TO_DB_OVERRIDE: Dict[Type[fields.Field], Callable] = {} FILTER_FUNC_OVERRIDE: Dict[Callable, Callable] = {} EXPLAIN_PREFIX: str = "EXPLAIN" + DB_NATIVE = {str, int, bool, float, decimal.Decimal, datetime.datetime, datetime.date} def __init__( self, diff --git a/tortoise/backends/sqlite/executor.py b/tortoise/backends/sqlite/executor.py index 0eecb99b5..fca1e882d 100644 --- a/tortoise/backends/sqlite/executor.py +++ b/tortoise/backends/sqlite/executor.py @@ -46,6 +46,7 @@ class SqliteExecutor(BaseExecutor): fields.DatetimeField: to_db_datetime, } EXPLAIN_PREFIX = "EXPLAIN QUERY PLAN" + DB_NATIVE = {str, int, bool, float} def Parameter(self, pos: int) -> Parameter: return Parameter("?") diff --git a/tortoise/models.py b/tortoise/models.py index eb9c9d770..0162def86 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -122,7 +122,6 @@ def __init__(self, meta) -> None: def add_field(self, name: str, value: Field): if name in self.fields_map: raise ConfigurationError(f"Field {name} already present in meta") - setattr(self._model, name, value) value.model = self._model self.fields_map[name] = value @@ -160,6 +159,7 @@ def finalise_model(self) -> None: """ self.finalise_fields() self._generate_filters() + self._generate_lazy_fk_m2m_fields() self._generate_db_fields() def finalise_fields(self) -> None: @@ -177,6 +177,7 @@ def finalise_fields(self) -> None: generated_fields.append(field.source_field or field.model_field_name) self.generated_db_fields = tuple(generated_fields) # type: ignore + def _generate_lazy_fk_m2m_fields(self) -> None: # Create lazy FK fields on model. for key in self.fk_fields: _key = f"_{key}" @@ -223,11 +224,9 @@ def _generate_db_fields(self) -> None: field = self.fields_map[model_field] default_converter = field.__class__.to_python_value is fields.Field.to_python_value - # TODO: Get this set from DB driver? - db_native = field.type in {str, int, bool, float} if not default_converter: self.db_complex_fields.append((key, model_field, field)) - elif db_native: + elif field.type in self.db.executor_class.DB_NATIVE: self.db_native_fields.append((key, model_field, field)) else: self.db_default_fields.append((key, model_field, field)) @@ -344,6 +343,9 @@ def __search_for_field_attributes(base, attrs: dict): ) ) + # Clean the class attributes + for slot in fields_map.keys(): + attrs.pop(slot, None) attrs["_meta"] = meta = MetaInfo(meta_class) meta.fields_map = fields_map @@ -377,9 +379,7 @@ def __init__(self, *args, **kwargs) -> None: self._saved_in_db = meta.pk_attr in kwargs and meta.pk.generated # Assign values and do type conversions - passed_fields = {*kwargs.keys()} - passed_fields.update(meta.fetch_fields) - passed_fields |= self._set_field_values(kwargs) + passed_fields = self._set_field_values(kwargs) | meta.fetch_fields | {*kwargs.keys()} # Assign defaults for missing fields for key in meta.fields.difference(passed_fields): @@ -420,20 +420,13 @@ def _set_field_values(self, values_map: Dict[str, Any]) -> Set[str]: raise OperationalError( f"You should first call .save() on {value} before referring to it" ) - field_object = meta.fields_map[key] - relation_field: str = field_object.source_field # type: ignore setattr(self, key, value) - passed_fields.add(relation_field) + passed_fields.add(meta.fields_map[key].source_field) elif key in meta.fields_db_projection: field_object = meta.fields_map[key] if value is None and not field_object.null: raise ValueError(f"{key} is non nullable field, but null was passed") setattr(self, key, field_object.to_python_value(value)) - elif key in meta.db_fields: - field_object = meta.fields_map[meta.fields_db_projection_reverse[key]] - if value is None and not field_object.null: - raise ValueError(f"{key} is non nullable field, but null was passed") - setattr(self, key, field_object.to_python_value(value)) elif key in meta.backward_fk_fields: raise ConfigurationError( "You can't set backward relations through init, change related model instead" @@ -443,7 +436,7 @@ def _set_field_values(self, values_map: Dict[str, Any]) -> Set[str]: "You can't set m2m relations through init, use m2m_manager instead" ) - return passed_fields + return passed_fields # type: ignore def __str__(self) -> str: return f"<{self.__class__.__name__}>" diff --git a/tortoise/tests/test_inheritence.py b/tortoise/tests/test_inheritence.py index 17fe33cdb..9c06d8365 100644 --- a/tortoise/tests/test_inheritence.py +++ b/tortoise/tests/test_inheritence.py @@ -2,10 +2,10 @@ from tortoise.tests.testmodels import MyAbstractBaseModel, MyDerivedModel -class TestBasic(test.SimpleTestCase): +class TestInheritance(test.SimpleTestCase): async def test_basic(self): - self.assertTrue(hasattr(MyAbstractBaseModel, "name")) - self.assertTrue(hasattr(MyDerivedModel, "created_at")) - self.assertTrue(hasattr(MyDerivedModel, "modified_at")) - self.assertTrue(hasattr(MyDerivedModel, "name")) - self.assertTrue(hasattr(MyDerivedModel, "first_name")) + self.assertTrue(hasattr(MyAbstractBaseModel(), "name")) + self.assertTrue(hasattr(MyDerivedModel(), "created_at")) + self.assertTrue(hasattr(MyDerivedModel(), "modified_at")) + self.assertTrue(hasattr(MyDerivedModel(), "name")) + self.assertTrue(hasattr(MyDerivedModel(), "first_name")) From 023d4b40b72d5fec8bbe584cab052f022d82c7d9 Mon Sep 17 00:00:00 2001 From: Grigi Date: Mon, 16 Sep 2019 12:16:16 +0200 Subject: [PATCH 09/10] f-string a few missed strings --- tortoise/__init__.py | 21 +++++++++------------ tortoise/backends/base/schema_generator.py | 13 +++++-------- tortoise/models.py | 12 ++++++------ tortoise/transactions.py | 4 ++-- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/tortoise/__init__.py b/tortoise/__init__.py index d71f37bb5..1c4562a5f 100644 --- a/tortoise/__init__.py +++ b/tortoise/__init__.py @@ -251,9 +251,8 @@ def get_related_model(related_app_name: str, related_model_name: str): if related_app_name not in cls.apps: raise ConfigurationError(f"No app with name '{related_app_name}' registered.") raise ConfigurationError( - "No model with name '{}' registered in app '{}'.".format( - related_model_name, related_app_name - ) + f"No model with name '{related_model_name}' registered in" + f" app '{related_app_name}'." ) def split_reference(reference: str) -> Tuple[str, str]: @@ -312,9 +311,8 @@ def split_reference(reference: str) -> Tuple[str, str]: backward_relation_name = f"{model._meta.table}s" if backward_relation_name in related_model._meta.fields: raise ConfigurationError( - 'backward relation "{}" duplicates in model {}'.format( - backward_relation_name, related_model_name - ) + f'backward relation "{backward_relation_name}" duplicates in' + f" model {related_model_name}" ) fk_relation = fields.BackwardFKRelation( model, f"{field}_id", fk_object.null, fk_object.description @@ -341,14 +339,13 @@ def split_reference(reference: str) -> Tuple[str, str]: backward_relation_name = m2m_object.related_name if not backward_relation_name: - backward_relation_name = m2m_object.related_name = "{}_through".format( - model._meta.table - ) + backward_relation_name = ( + m2m_object.related_name + ) = f"{model._meta.table}_through" if backward_relation_name in related_model._meta.fields: raise ConfigurationError( - 'backward relation "{}" duplicates in model {}'.format( - backward_relation_name, related_model_name - ) + f'backward relation "{backward_relation_name}" duplicates in' + f" model {related_model_name}" ) if not m2m_object.through: diff --git a/tortoise/backends/base/schema_generator.py b/tortoise/backends/base/schema_generator.py index 896385c58..b1d98331b 100644 --- a/tortoise/backends/base/schema_generator.py +++ b/tortoise/backends/base/schema_generator.py @@ -104,10 +104,8 @@ def _generate_index_name(self, model, field_names: List[str]) -> str: # characters (Oracle limit). # That's why we slice some of the strings here. table_name = model._meta.table - index_name = "{t}_{f}_{h}_idx".format( - t=table_name[:11], - f=field_names[0][:7], - h=self._make_hash(table_name, *field_names, length=6), + index_name = "{}_{}_{}_idx".format( + table_name[:11], field_names[0][:7], self._make_hash(table_name, *field_names, length=6) ) return index_name @@ -242,10 +240,9 @@ def _get_table_sql(self, model, safe=True) -> dict: ] if safe and not self.client.capabilities.safe_indexes: warnings.warn( - "Skipping creation of field indexes: safe index creation is not supported yet for " - "{dialect}. Please find the SQL queries to create the indexes in the logs.".format( - dialect=self.client.capabilities.dialect - ) + f"Skipping creation of field indexes: safe index creation is not supported" + f" yet for {self.client.capabilities.dialect}. Please find the SQL queries" + f" to create the indexes in the logs." ) for fis in field_indexes_sqls: logger.warning(fis) diff --git a/tortoise/models.py b/tortoise/models.py index 0162def86..762435b03 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -291,8 +291,8 @@ def __search_for_field_attributes(base, attrs: dict): if value.pk: if custom_pk_present: raise ConfigurationError( - "Can't create model {} with two primary keys, " - "only single pk are supported".format(name) + f"Can't create model {name} with two primary keys," + " only single pk are supported" ) if value.generated and not isinstance( value, (fields.SmallIntField, fields.IntField, fields.BigIntField) @@ -309,8 +309,8 @@ def __search_for_field_attributes(base, attrs: dict): if not isinstance(attrs["id"], fields.Field) or not attrs["id"].pk: raise ConfigurationError( - "Can't create model {} without explicit primary key " - "if field 'id' already present".format(name) + f"Can't create model {name} without explicit primary key if field 'id'" + " already present" ) for key, value in attrs.items(): @@ -658,8 +658,8 @@ def _check_unique_together(cls) -> None: if isinstance(field, ManyToManyField): raise ConfigurationError( - "'{}.unique_together' '{}' field refers " - "to ManyToMany field.".format(cls.__name__, field_name) + f"'{cls.__name__}.unique_together' '{field_name}' field refers" + " to ManyToMany field." ) class Meta: diff --git a/tortoise/transactions.py b/tortoise/transactions.py index bf1dce3cb..c750ef0d7 100644 --- a/tortoise/transactions.py +++ b/tortoise/transactions.py @@ -16,8 +16,8 @@ def _get_connection(connection_name: Optional[str]) -> BaseDBAsyncClient: connection = list(Tortoise._connections.values())[0] else: raise ParamsError( - "You are running with multiple databases, so you " - "should specify connection_name: {}".format(list(Tortoise._connections.keys())) + "You are running with multiple databases, so you should specify" + f" connection_name: {list(Tortoise._connections.keys())}" ) return connection From b1eb44120bf2611a289f6f56fcc48ef8bcd0c4ff Mon Sep 17 00:00:00 2001 From: Grigi Date: Fri, 20 Sep 2019 16:56:42 +0200 Subject: [PATCH 10/10] Use py36 async generators for simplicity --- tortoise/fields.py | 10 +++++----- tortoise/queryset.py | 6 +++--- tortoise/utils.py | 26 +------------------------- 3 files changed, 9 insertions(+), 33 deletions(-) diff --git a/tortoise/fields.py b/tortoise/fields.py index 049a284c9..a8c201e97 100644 --- a/tortoise/fields.py +++ b/tortoise/fields.py @@ -10,7 +10,6 @@ from pypika import Table from tortoise.exceptions import ConfigurationError, NoValuesFetched, OperationalError -from tortoise.utils import QueryAsyncIterator if TYPE_CHECKING: # pragma: nocoverage from tortoise.models import Model @@ -533,12 +532,13 @@ def __getitem__(self, item): def __await__(self): return self._query.__await__() - def __aiter__(self) -> QueryAsyncIterator: - async def fetched_callback(iterator_wrapper): + async def __aiter__(self): + if not self._fetched: + self.related_objects = await self self._fetched = True - self.related_objects = iterator_wrapper.sequence - return QueryAsyncIterator(self._query, callback=fetched_callback) + for val in self.related_objects: + yield val def filter(self, *args, **kwargs): """ diff --git a/tortoise/queryset.py b/tortoise/queryset.py index e3405dacb..3aa790627 100644 --- a/tortoise/queryset.py +++ b/tortoise/queryset.py @@ -9,7 +9,6 @@ from tortoise.backends.base.client import BaseDBAsyncClient from tortoise.exceptions import DoesNotExist, FieldError, IntegrityError, MultipleObjectsReturned from tortoise.query_utils import Prefetch, Q, QueryModifier, _get_joins_for_related_field -from tortoise.utils import QueryAsyncIterator # Empty placeholder - Should never be edited. QUERY = Query() @@ -81,8 +80,9 @@ def __await__(self): self._make_query() return self._execute().__await__() - def __aiter__(self) -> QueryAsyncIterator: - return QueryAsyncIterator(self) + async def __aiter__(self): + for val in await self: + yield val async def _execute(self): raise NotImplementedError() # pragma: nocoverage diff --git a/tortoise/utils.py b/tortoise/utils.py index e6fdfdbdf..b238c07e9 100644 --- a/tortoise/utils.py +++ b/tortoise/utils.py @@ -1,33 +1,9 @@ import logging -from typing import Awaitable, Callable, Iterator, List, Optional +from typing import List logger = logging.getLogger("tortoise") -class QueryAsyncIterator: - __slots__ = ("query", "sequence", "_sequence_iterator", "_callback") - - def __init__(self, query: Awaitable[Iterator], callback: Optional[Callable] = None) -> None: - self.query = query - self.sequence: Optional[Iterator] = None - self._sequence_iterator = None - self._callback = callback - - def __aiter__(self): - return self # pragma: nocoverage - - async def __anext__(self): - if self.sequence is None: - self.sequence = await self.query - self._sequence_iterator = self.sequence.__iter__() - if self._callback: # pragma: no branch - await self._callback(self) - try: - return next(self._sequence_iterator) - except StopIteration: - raise StopAsyncIteration - - def get_schema_sql(client, safe: bool) -> str: generator = client.schema_generator(client) return generator.get_create_schema_sql(safe)