From ff1ab665cb1694b85085680d1a02c7c11fa2a6d4 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 9 Jan 2022 11:49:02 -0500 Subject: mypy: sqlalchemy.util Starting to set up practices and conventions to get the library typed. Key goals for typing are: 1. whole library can pass mypy without any strict turned on. 2. we can incrementally turn on some strict flags on a per-package/ module basis, as here we turn on more strictness for sqlalchemy.util, exc, and log 3. mypy ORM plugin tests work fully without sqlalchemy2-stubs installed 4. public facing methods all have return types, major parameter signatures filled in also 5. Foundational elements like util etc. are typed enough so that we can use them in fully typed internals higher up the stack. Conventions set up here: 1. we can use lots of config in setup.cfg to limit where mypy is throwing errors and how detailed it should be in different packages / modules. We can use this to push up gerrits that will pass tests fully without everything being typed. 2. a new tox target pep484 is added. this links to a new jenkins pep484 job that works across all projects (alembic, dogpile, etc.) We've worked around some mypy bugs that will likely be around for awhile, and also set up some core practices for how to deal with certain things such as public_factory modules (mypy won't accept a module from a callable at all, so need to use simple type checking conditionals). References: #6810 Change-Id: I80be58029896a29fd9f491aa3215422a8b705e12 --- lib/sqlalchemy/sql/_py_util.py | 6 ++-- lib/sqlalchemy/sql/_typing.py | 9 ++++++ lib/sqlalchemy/sql/util.py | 73 +++++++++++++++++++++++++++--------------- 3 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 lib/sqlalchemy/sql/_typing.py (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/_py_util.py b/lib/sqlalchemy/sql/_py_util.py index e9357bf7d..594967a40 100644 --- a/lib/sqlalchemy/sql/_py_util.py +++ b/lib/sqlalchemy/sql/_py_util.py @@ -5,8 +5,10 @@ # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php +from typing import Dict -class prefix_anon_map(dict): + +class prefix_anon_map(Dict[str, str]): """A map that creates new keys for missing key access. Considers keys of the form " " to produce @@ -27,7 +29,7 @@ class prefix_anon_map(dict): return value -class cache_anon_map(dict): +class cache_anon_map(Dict[int, str]): """A map that creates new keys for missing key access. Produces an incrementing sequence given a series of unique keys. diff --git a/lib/sqlalchemy/sql/_typing.py b/lib/sqlalchemy/sql/_typing.py new file mode 100644 index 000000000..b5b0efb21 --- /dev/null +++ b/lib/sqlalchemy/sql/_typing.py @@ -0,0 +1,9 @@ +from typing import Any +from typing import Mapping +from typing import Sequence +from typing import Union + +_SingleExecuteParams = Mapping[str, Any] +_MultiExecuteParams = Sequence[_SingleExecuteParams] +_ExecuteParams = Union[_SingleExecuteParams, _MultiExecuteParams] +_ExecuteOptions = Mapping[str, Any] diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index fa3bae835..c0de1902f 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -8,14 +8,20 @@ """High level utilities which build upon other modules here. """ - from collections import deque from itertools import chain +import typing +from typing import Any +from typing import cast +from typing import Optional from . import coercions from . import operators from . import roles from . import visitors +from ._typing import _ExecuteParams +from ._typing import _MultiExecuteParams +from ._typing import _SingleExecuteParams from .annotation import _deep_annotate # noqa from .annotation import _deep_deannotate # noqa from .annotation import _shallow_annotate # noqa @@ -45,6 +51,9 @@ from .selectable import TableClause from .. import exc from .. import util +if typing.TYPE_CHECKING: + from ..engine.row import Row + def join_condition(a, b, a_subset=None, consider_as_foreign_keys=None): """Create a join condition between two tables or selectables. @@ -488,13 +497,13 @@ def _quote_ddl_expr(element): class _repr_base: - _LIST = 0 - _TUPLE = 1 - _DICT = 2 + _LIST: int = 0 + _TUPLE: int = 1 + _DICT: int = 2 __slots__ = ("max_chars",) - def trunc(self, value): + def trunc(self, value: Any) -> str: rep = repr(value) lenrep = len(rep) if lenrep > self.max_chars: @@ -515,11 +524,11 @@ class _repr_row(_repr_base): __slots__ = ("row",) - def __init__(self, row, max_chars=300): + def __init__(self, row: "Row", max_chars: int = 300): self.row = row self.max_chars = max_chars - def __repr__(self): + def __repr__(self) -> str: trunc = self.trunc return "(%s%s)" % ( ", ".join(trunc(value) for value in self.row), @@ -537,13 +546,19 @@ class _repr_params(_repr_base): __slots__ = "params", "batches", "ismulti" - def __init__(self, params, batches, max_chars=300, ismulti=None): - self.params = params + def __init__( + self, + params: _ExecuteParams, + batches: int, + max_chars: int = 300, + ismulti: Optional[bool] = None, + ): + self.params: _ExecuteParams = params self.ismulti = ismulti self.batches = batches self.max_chars = max_chars - def __repr__(self): + def __repr__(self) -> str: if self.ismulti is None: return self.trunc(self.params) @@ -557,23 +572,31 @@ class _repr_params(_repr_base): else: return self.trunc(self.params) - if self.ismulti and len(self.params) > self.batches: - msg = " ... displaying %i of %i total bound parameter sets ... " - return " ".join( - ( - self._repr_multi(self.params[: self.batches - 2], typ)[ - 0:-1 - ], - msg % (self.batches, len(self.params)), - self._repr_multi(self.params[-2:], typ)[1:], + if self.ismulti: + multi_params = cast(_MultiExecuteParams, self.params) + + if len(self.params) > self.batches: + msg = ( + " ... displaying %i of %i total bound parameter sets ... " ) - ) - elif self.ismulti: - return self._repr_multi(self.params, typ) + return " ".join( + ( + self._repr_multi( + multi_params[: self.batches - 2], + typ, + )[0:-1], + msg % (self.batches, len(self.params)), + self._repr_multi(multi_params[-2:], typ)[1:], + ) + ) + else: + return self._repr_multi(multi_params, typ) else: - return self._repr_params(self.params, typ) + return self._repr_params( + cast(_SingleExecuteParams, self.params), typ + ) - def _repr_multi(self, multi_params, typ): + def _repr_multi(self, multi_params: _MultiExecuteParams, typ) -> str: if multi_params: if isinstance(multi_params[0], list): elem_type = self._LIST @@ -597,7 +620,7 @@ class _repr_params(_repr_base): else: return "(%s)" % elements - def _repr_params(self, params, typ): + def _repr_params(self, params: _SingleExecuteParams, typ: int) -> str: trunc = self.trunc if typ is self._DICT: return "{%s}" % ( -- cgit v1.2.1