From f016c1ac7f93d2f759aff53ec1494658efd7b496 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 2 Nov 2020 17:37:12 -0500 Subject: [PATCH] Reduce import time overhead * Fix subclass traversals to not run classes multiple times * switch compiler visitor to use an attrgetter, to avoid an eval() at startup time * don't pre-generate traversal functions, there's lots of these which are expensive to generate at once and most applications won't use them all; have it generate them on first use instead * Some ideas about removing asyncio imports, they don't seem to be too signficant, apply some more simplicity to the overall "greenlet fallback" situation Fixes: #5681 Change-Id: Ib564ddaddb374787ce3e11ff48026e99ed570933 --- lib/sqlalchemy/engine/create.py | 2 +- lib/sqlalchemy/event/attr.py | 16 +- lib/sqlalchemy/sql/__init__.py | 11 +- lib/sqlalchemy/sql/annotation.py | 6 +- lib/sqlalchemy/sql/traversals.py | 7 +- lib/sqlalchemy/sql/visitors.py | 35 +++-- lib/sqlalchemy/util/__init__.py | 1 + lib/sqlalchemy/util/_concurrency_py3k.py | 181 +++++++++++------------ lib/sqlalchemy/util/_preloaded.py | 12 +- lib/sqlalchemy/util/concurrency.py | 47 ++++-- lib/sqlalchemy/util/langhelpers.py | 43 ++++-- test/aaa_profiling/test_orm.py | 2 +- test/profiles.txt | 36 +---- 13 files changed, 195 insertions(+), 204 deletions(-) diff --git a/lib/sqlalchemy/engine/create.py b/lib/sqlalchemy/engine/create.py index 365a72a961..4d84687ea3 100644 --- a/lib/sqlalchemy/engine/create.py +++ b/lib/sqlalchemy/engine/create.py @@ -92,7 +92,7 @@ def create_engine(url, **kwargs): :ref:`connections_toplevel` - :param case_sensitive=True: if False, result column names + :param case_sensitive: if False, result column names will match in a case-insensitive fashion, that is, ``row['SomeColumn']``. diff --git a/lib/sqlalchemy/event/attr.py b/lib/sqlalchemy/event/attr.py index baa3cd28a6..122221d407 100644 --- a/lib/sqlalchemy/event/attr.py +++ b/lib/sqlalchemy/event/attr.py @@ -127,10 +127,8 @@ class _ClsLevelDispatch(RefCollection): raise exc.InvalidRequestError( "Can't assign an event directly to the %s class" % target ) - stack = [target] - while stack: - cls = stack.pop(0) - stack.extend(cls.__subclasses__()) + + for cls in util.walk_subclasses(target): if cls is not target and cls not in self._clslevel: self.update_subclass(cls) else: @@ -148,10 +146,7 @@ class _ClsLevelDispatch(RefCollection): raise exc.InvalidRequestError( "Can't assign an event directly to the %s class" % target ) - stack = [target] - while stack: - cls = stack.pop(0) - stack.extend(cls.__subclasses__()) + for cls in util.walk_subclasses(target): if cls is not target and cls not in self._clslevel: self.update_subclass(cls) else: @@ -178,10 +173,7 @@ class _ClsLevelDispatch(RefCollection): def remove(self, event_key): target = event_key.dispatch_target - stack = [target] - while stack: - cls = stack.pop(0) - stack.extend(cls.__subclasses__()) + for cls in util.walk_subclasses(target): if cls in self._clslevel: self._clslevel[cls].remove(event_key._listen_fn) registry._removed_from_collection(event_key, self) diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py index 8cfd20054e..645189e76e 100644 --- a/lib/sqlalchemy/sql/__init__.py +++ b/lib/sqlalchemy/sql/__init__.py @@ -55,10 +55,10 @@ from .expression import literal_column # noqa from .expression import modifier # noqa from .expression import not_ # noqa from .expression import null # noqa -from .expression import nullsfirst # noqa; deprecated 1.4; see #5435 -from .expression import nullslast # noqa; deprecated 1.4; see #5435 from .expression import nulls_first # noqa from .expression import nulls_last # noqa +from .expression import nullsfirst # noqa +from .expression import nullslast # noqa from .expression import or_ # noqa from .expression import outerjoin # noqa from .expression import outparam # noqa @@ -106,7 +106,8 @@ def __go(lcls): from .elements import AnnotatedColumnElement from .elements import ClauseList # noqa from .selectable import AnnotatedFromClause # noqa - from .traversals import _preconfigure_traversals + + # from .traversals import _preconfigure_traversals from . import base from . import coercions @@ -133,7 +134,9 @@ def __go(lcls): _prepare_annotations(FromClause, AnnotatedFromClause) _prepare_annotations(ClauseList, Annotated) - _preconfigure_traversals(ClauseElement) + # this is expensive at import time; elements that are used can create + # their traversals on demand + # _preconfigure_traversals(ClauseElement) _sa_util.preloaded.import_prefix("sqlalchemy.sql") diff --git a/lib/sqlalchemy/sql/annotation.py b/lib/sqlalchemy/sql/annotation.py index 8a0d6ec280..94d37573ce 100644 --- a/lib/sqlalchemy/sql/annotation.py +++ b/lib/sqlalchemy/sql/annotation.py @@ -354,9 +354,5 @@ def _new_annotation_type(cls, base_cls): def _prepare_annotations(target_hierarchy, base_cls): - stack = [target_hierarchy] - while stack: - cls = stack.pop() - stack.extend(cls.__subclasses__()) - + for cls in util.walk_subclasses(target_hierarchy): _new_annotation_type(cls, base_cls) diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py index cb38df6afa..a24d010cdd 100644 --- a/lib/sqlalchemy/sql/traversals.py +++ b/lib/sqlalchemy/sql/traversals.py @@ -33,12 +33,7 @@ def compare(obj1, obj2, **kw): def _preconfigure_traversals(target_hierarchy): - - stack = [target_hierarchy] - while stack: - cls = stack.pop() - stack.extend(cls.__subclasses__()) - + for cls in util.walk_subclasses(target_hierarchy): if hasattr(cls, "_traverse_internals"): cls._generate_cache_attrs() _copy_internals.generate_dispatch( diff --git a/lib/sqlalchemy/sql/visitors.py b/lib/sqlalchemy/sql/visitors.py index 27499b5f71..43b7cb4afe 100644 --- a/lib/sqlalchemy/sql/visitors.py +++ b/lib/sqlalchemy/sql/visitors.py @@ -24,6 +24,7 @@ http://techspot.zzzeek.org/2008/01/23/expression-transformations/ . """ from collections import deque +import operator from .. import exc from .. import util @@ -63,26 +64,24 @@ def _generate_compiler_dispatch(cls): % cls.__name__ ) - code = ( - "def _compiler_dispatch(self, visitor, **kw):\n" - " try:\n" - " meth = visitor.visit_%(name)s\n" - " except AttributeError as err:\n" - " util.raise_(\n" - " exc.UnsupportedCompilationError(visitor, cls), \n" - " replace_context=err)\n" - " else:\n" - " return meth(self, **kw)\n" - ) % {"name": visit_name} - - _compiler_dispatch = langhelpers._exec_code_in_env( - code, {"exc": exc, "cls": cls, "util": util}, "_compiler_dispatch" - ) + name = "visit_%s" % visit_name + getter = operator.attrgetter(name) + + def _compiler_dispatch(self, visitor, **kw): + """Look for an attribute named "visit_" on the + visitor, and call it with the same kw params. - _compiler_dispatch.__doc__ = """Look for an attribute named "visit_" - + self.__visit_name__ on the visitor, and call it with the same - kw params. """ + try: + meth = getter(visitor) + except AttributeError as err: + util.raise_( + exc.UnsupportedCompilationError(visitor, cls), + replace_context=err, + ) + else: + return meth(self, **kw) + cls._compiler_dispatch = ( cls._original_compiler_dispatch ) = _compiler_dispatch diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index 00066cbed8..7c5257b875 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -157,6 +157,7 @@ from .langhelpers import set_creation_order # noqa from .langhelpers import string_or_unprintable # noqa from .langhelpers import symbol # noqa from .langhelpers import unbound_method_to_callable # noqa +from .langhelpers import walk_subclasses # noqa from .langhelpers import warn # noqa from .langhelpers import warn_exception # noqa from .langhelpers import warn_limited # noqa diff --git a/lib/sqlalchemy/util/_concurrency_py3k.py b/lib/sqlalchemy/util/_concurrency_py3k.py index ee3abe5fa9..5d11bf92c1 100644 --- a/lib/sqlalchemy/util/_concurrency_py3k.py +++ b/lib/sqlalchemy/util/_concurrency_py3k.py @@ -4,110 +4,103 @@ from typing import Any from typing import Callable from typing import Coroutine +import greenlet + from .. import exc -try: - import greenlet - # implementation based on snaury gist at - # https://gist.github.com/snaury/202bf4f22c41ca34e56297bae5f33fef - # Issue for context: https://github.com/python-greenlet/greenlet/issues/173 +# implementation based on snaury gist at +# https://gist.github.com/snaury/202bf4f22c41ca34e56297bae5f33fef +# Issue for context: https://github.com/python-greenlet/greenlet/issues/173 + + +class _AsyncIoGreenlet(greenlet.greenlet): + def __init__(self, fn, driver): + greenlet.greenlet.__init__(self, fn, driver) + self.driver = driver + + +def await_only(awaitable: Coroutine) -> Any: + """Awaits an async function in a sync method. + + The sync method must be insice a :func:`greenlet_spawn` context. + :func:`await_` calls cannot be nested. - class _AsyncIoGreenlet(greenlet.greenlet): - def __init__(self, fn, driver): - greenlet.greenlet.__init__(self, fn, driver) - self.driver = driver + :param awaitable: The coroutine to call. - def await_only(awaitable: Coroutine) -> Any: - """Awaits an async function in a sync method. + """ + # this is called in the context greenlet while running fn + current = greenlet.getcurrent() + if not isinstance(current, _AsyncIoGreenlet): + raise exc.InvalidRequestError( + "greenlet_spawn has not been called; can't call await_() here." + ) - The sync method must be insice a :func:`greenlet_spawn` context. - :func:`await_` calls cannot be nested. + # returns the control to the driver greenlet passing it + # a coroutine to run. Once the awaitable is done, the driver greenlet + # switches back to this greenlet with the result of awaitable that is + # then returned to the caller (or raised as error) + return current.driver.switch(awaitable) - :param awaitable: The coroutine to call. - """ - # this is called in the context greenlet while running fn - current = greenlet.getcurrent() - if not isinstance(current, _AsyncIoGreenlet): +def await_fallback(awaitable: Coroutine) -> Any: + """Awaits an async function in a sync method. + + The sync method must be insice a :func:`greenlet_spawn` context. + :func:`await_` calls cannot be nested. + + :param awaitable: The coroutine to call. + + """ + + # this is called in the context greenlet while running fn + current = greenlet.getcurrent() + if not isinstance(current, _AsyncIoGreenlet): + loop = asyncio.get_event_loop() + if loop.is_running(): raise exc.InvalidRequestError( - "greenlet_spawn has not been called; can't call await_() here." + "greenlet_spawn has not been called and asyncio event " + "loop is already running; can't call await_() here." ) - - # returns the control to the driver greenlet passing it - # a coroutine to run. Once the awaitable is done, the driver greenlet - # switches back to this greenlet with the result of awaitable that is - # then returned to the caller (or raised as error) - return current.driver.switch(awaitable) - - def await_fallback(awaitable: Coroutine) -> Any: - """Awaits an async function in a sync method. - - The sync method must be insice a :func:`greenlet_spawn` context. - :func:`await_` calls cannot be nested. - - :param awaitable: The coroutine to call. - - """ - # this is called in the context greenlet while running fn - current = greenlet.getcurrent() - if not isinstance(current, _AsyncIoGreenlet): - loop = asyncio.get_event_loop() - if loop.is_running(): - raise exc.InvalidRequestError( - "greenlet_spawn has not been called and asyncio event " - "loop is already running; can't call await_() here." - ) - return loop.run_until_complete(awaitable) - - return current.driver.switch(awaitable) - - async def greenlet_spawn(fn: Callable, *args, **kwargs) -> Any: - """Runs a sync function ``fn`` in a new greenlet. - - The sync function can then use :func:`await_` to wait for async - functions. - - :param fn: The sync callable to call. - :param \\*args: Positional arguments to pass to the ``fn`` callable. - :param \\*\\*kwargs: Keyword arguments to pass to the ``fn`` callable. - """ - context = _AsyncIoGreenlet(fn, greenlet.getcurrent()) - # runs the function synchronously in gl greenlet. If the execution - # is interrupted by await_, context is not dead and result is a - # coroutine to wait. If the context is dead the function has - # returned, and its result can be returned. - try: - result = context.switch(*args, **kwargs) - while not context.dead: - try: - # wait for a coroutine from await_ and then return its - # result back to it. - value = await result - except Exception: - # this allows an exception to be raised within - # the moderated greenlet so that it can continue - # its expected flow. - result = context.throw(*sys.exc_info()) - else: - result = context.switch(value) - finally: - # clean up to avoid cycle resolution by gc - del context.driver - return result - - -except ImportError: # pragma: no cover - greenlet = None - - def await_fallback(awaitable): - return asyncio.get_event_loop().run_until_complete(awaitable) - - def await_only(awaitable): - raise ValueError("Greenlet is required to use this function") - - async def greenlet_spawn(fn, *args, **kw): - raise ValueError("Greenlet is required to use this function") + return loop.run_until_complete(awaitable) + + return current.driver.switch(awaitable) + + +async def greenlet_spawn(fn: Callable, *args, **kwargs) -> Any: + """Runs a sync function ``fn`` in a new greenlet. + + The sync function can then use :func:`await_` to wait for async + functions. + + :param fn: The sync callable to call. + :param \\*args: Positional arguments to pass to the ``fn`` callable. + :param \\*\\*kwargs: Keyword arguments to pass to the ``fn`` callable. + """ + + context = _AsyncIoGreenlet(fn, greenlet.getcurrent()) + # runs the function synchronously in gl greenlet. If the execution + # is interrupted by await_, context is not dead and result is a + # coroutine to wait. If the context is dead the function has + # returned, and its result can be returned. + try: + result = context.switch(*args, **kwargs) + while not context.dead: + try: + # wait for a coroutine from await_ and then return its + # result back to it. + value = await result + except Exception: + # this allows an exception to be raised within + # the moderated greenlet so that it can continue + # its expected flow. + result = context.throw(*sys.exc_info()) + else: + result = context.switch(value) + finally: + # clean up to avoid cycle resolution by gc + del context.driver + return result class AsyncAdaptedLock: diff --git a/lib/sqlalchemy/util/_preloaded.py b/lib/sqlalchemy/util/_preloaded.py index 1a833a963c..e267c008c1 100644 --- a/lib/sqlalchemy/util/_preloaded.py +++ b/lib/sqlalchemy/util/_preloaded.py @@ -35,8 +35,9 @@ class _ModuleRegistry: name. Example: ``sqlalchemy.sql.util`` becomes ``preloaded.sql_util``. """ - def __init__(self, prefix="sqlalchemy"): + def __init__(self, prefix="sqlalchemy."): self.module_registry = set() + self.prefix = prefix def preload_module(self, *deps): """Adds the specified modules to the list to load. @@ -52,8 +53,13 @@ class _ModuleRegistry: specified path. """ for module in self.module_registry: - key = module.split("sqlalchemy.")[-1].replace(".", "_") - if module.startswith(path) and key not in self.__dict__: + if self.prefix: + key = module.split(self.prefix)[-1].replace(".", "_") + else: + key = module + if ( + not path or module.startswith(path) + ) and key not in self.__dict__: compat.import_(module, globals(), locals()) self.__dict__[key] = sys.modules[module] diff --git a/lib/sqlalchemy/util/concurrency.py b/lib/sqlalchemy/util/concurrency.py index e0883aa683..f78c0971c7 100644 --- a/lib/sqlalchemy/util/concurrency.py +++ b/lib/sqlalchemy/util/concurrency.py @@ -1,25 +1,40 @@ from . import compat +have_greenlet = False if compat.py3k: - import asyncio - from ._concurrency_py3k import await_only - from ._concurrency_py3k import await_fallback - from ._concurrency_py3k import greenlet - from ._concurrency_py3k import greenlet_spawn - from ._concurrency_py3k import AsyncAdaptedLock -else: - asyncio = None - greenlet = None - - def await_only(thing): + try: + import greenlet # noqa F401 + except ImportError: + pass + else: + have_greenlet = True + from ._concurrency_py3k import await_only + from ._concurrency_py3k import await_fallback + from ._concurrency_py3k import greenlet_spawn + from ._concurrency_py3k import AsyncAdaptedLock + from ._concurrency_py3k import asyncio # noqa F401 + +if not have_greenlet: + + asyncio = None # noqa F811 + + def _not_implemented(): + if not compat.py3k: + raise ValueError("Cannot use this function in py2.") + else: + raise ValueError( + "the greenlet library is required to use this function." + ) + + def await_only(thing): # noqa F811 return thing - def await_fallback(thing): + def await_fallback(thing): # noqa F81 return thing - def greenlet_spawn(fn, *args, **kw): - raise ValueError("Cannot use this function in py2.") + def greenlet_spawn(fn, *args, **kw): # noqa F81 + _not_implemented() - def AsyncAdaptedLock(*args, **kw): - raise ValueError("Cannot use this function in py2.") + def AsyncAdaptedLock(*args, **kw): # noqa F81 + _not_implemented() diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 4289db8129..ce3a284993 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -10,6 +10,7 @@ modules, classes, hierarchies, attributes, functions, and methods. """ +import collections from functools import update_wrapper import hashlib import inspect @@ -83,6 +84,20 @@ class safe_reraise(object): compat.raise_(value, with_traceback=traceback) +def walk_subclasses(cls): + seen = set() + + stack = [cls] + while stack: + cls = stack.pop() + if cls in seen: + continue + else: + seen.add(cls) + stack.extend(cls.__subclasses__()) + yield cls + + def string_or_unprintable(element): if isinstance(element, compat.string_types): return element @@ -1782,15 +1797,22 @@ def inject_docstring_text(doctext, injecttext, pos): return "\n".join(lines) +_param_reg = re.compile(r"(\s+):param (.+?):") + + def inject_param_text(doctext, inject_params): - doclines = doctext.splitlines() + doclines = collections.deque(doctext.splitlines()) lines = [] + # TODO: this is not working for params like ":param case_sensitive=True:" + to_inject = None while doclines: - line = doclines.pop(0) + line = doclines.popleft() + + m = _param_reg.match(line) + if to_inject is None: - m = re.match(r"(\s+):param (.+?):", line) if m: param = m.group(2).lstrip("*") if param in inject_params: @@ -1805,23 +1827,16 @@ def inject_param_text(doctext, inject_params): indent = " " * len(m2.group(1)) to_inject = indent + inject_params[param] - elif line.lstrip().startswith(":param "): - lines.append("\n") - lines.append(to_inject) - lines.append("\n") + elif m: + lines.extend(["\n", to_inject, "\n"]) to_inject = None elif not line.rstrip(): - lines.append(line) - lines.append(to_inject) - lines.append("\n") + lines.extend([line, to_inject, "\n"]) to_inject = None elif line.endswith("::"): # TODO: this still wont cover if the code example itself has blank # lines in it, need to detect those via indentation. - lines.append(line) - lines.append( - doclines.pop(0) - ) # the blank line following a code example + lines.extend([line, doclines.popleft()]) continue lines.append(line) diff --git a/test/aaa_profiling/test_orm.py b/test/aaa_profiling/test_orm.py index 4bc2af93d3..1bbfd36586 100644 --- a/test/aaa_profiling/test_orm.py +++ b/test/aaa_profiling/test_orm.py @@ -864,7 +864,7 @@ class JoinedEagerLoadTest(NoCache, fixtures.MappedTest): from sqlalchemy.orm.context import ORMCompileState - @profiling.function_call_count() + @profiling.function_call_count(warmup=1) def go(): for i in range(100): # NOTE: this test was broken in diff --git a/test/profiles.txt b/test/profiles.txt index 4a21de4273..08cdf911b9 100644 --- a/test/profiles.txt +++ b/test/profiles.txt @@ -1,15 +1,15 @@ # /home/classic/dev/sqlalchemy/test/profiles.txt # This file is written out on a per-environment basis. -# For each test in aaa_profiling, the corresponding function and +# For each test in aaa_profiling, the corresponding function and # environment is located within this file. If it doesn't exist, # the test is skipped. -# If a callcount does exist, it is compared to what we received. +# If a callcount does exist, it is compared to what we received. # assertions are raised if the counts do not match. -# -# To add a new callcount test, apply the function_call_count -# decorator and re-run the tests using the --write-profiles +# +# To add a new callcount test, apply the function_call_count +# decorator and re-run the tests using the --write-profiles # option - this file will be rewritten including the new count. -# +# # TEST: test.aaa_profiling.test_compiler.CompileTest.test_insert @@ -153,34 +153,10 @@ test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3. # TEST: test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mariadb_mysqldb_dbapiunicode_cextensions 159 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mariadb_mysqldb_dbapiunicode_nocextensions 159 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mariadb_pymysql_dbapiunicode_cextensions 159 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mariadb_pymysql_dbapiunicode_nocextensions 159 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mssql_pyodbc_dbapiunicode_cextensions 159 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mssql_pyodbc_dbapiunicode_nocextensions 159 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mysql_mysqldb_dbapiunicode_cextensions 152 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mysql_mysqldb_dbapiunicode_nocextensions 152 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mysql_pymysql_dbapiunicode_cextensions 152 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_mysql_pymysql_dbapiunicode_nocextensions 152 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_cextensions 157 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_oracle_cx_oracle_dbapiunicode_nocextensions 159 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_cextensions 159 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_postgresql_psycopg2_dbapiunicode_nocextensions 159 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_cextensions 159 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_nocextensions 159 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mariadb_mysqldb_dbapiunicode_cextensions 165 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mariadb_mysqldb_dbapiunicode_nocextensions 165 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mariadb_pymysql_dbapiunicode_cextensions 165 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mariadb_pymysql_dbapiunicode_nocextensions 165 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mssql_pyodbc_dbapiunicode_cextensions 165 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mssql_pyodbc_dbapiunicode_nocextensions 165 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mysql_mysqldb_dbapiunicode_cextensions 158 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mysql_mysqldb_dbapiunicode_nocextensions 158 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mysql_pymysql_dbapiunicode_cextensions 158 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_mysql_pymysql_dbapiunicode_nocextensions 158 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_oracle_cx_oracle_dbapiunicode_cextensions 163 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_oracle_cx_oracle_dbapiunicode_nocextensions 163 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_cextensions 165 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_postgresql_psycopg2_dbapiunicode_nocextensions 165 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.8_sqlite_pysqlite_dbapiunicode_cextensions 165 -- 2.39.5