SQLAlchemy 2.0.47__cp313-cp313t-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy/__init__.py +283 -0
- sqlalchemy/connectors/__init__.py +18 -0
- sqlalchemy/connectors/aioodbc.py +184 -0
- sqlalchemy/connectors/asyncio.py +429 -0
- sqlalchemy/connectors/pyodbc.py +250 -0
- sqlalchemy/cyextension/__init__.py +6 -0
- sqlalchemy/cyextension/collections.cp313t-win_amd64.pyd +0 -0
- sqlalchemy/cyextension/collections.pyx +409 -0
- sqlalchemy/cyextension/immutabledict.cp313t-win_amd64.pyd +0 -0
- sqlalchemy/cyextension/immutabledict.pxd +8 -0
- sqlalchemy/cyextension/immutabledict.pyx +133 -0
- sqlalchemy/cyextension/processors.cp313t-win_amd64.pyd +0 -0
- sqlalchemy/cyextension/processors.pyx +68 -0
- sqlalchemy/cyextension/resultproxy.cp313t-win_amd64.pyd +0 -0
- sqlalchemy/cyextension/resultproxy.pyx +102 -0
- sqlalchemy/cyextension/util.cp313t-win_amd64.pyd +0 -0
- sqlalchemy/cyextension/util.pyx +90 -0
- sqlalchemy/dialects/__init__.py +62 -0
- sqlalchemy/dialects/_typing.py +30 -0
- sqlalchemy/dialects/mssql/__init__.py +88 -0
- sqlalchemy/dialects/mssql/aioodbc.py +63 -0
- sqlalchemy/dialects/mssql/base.py +4093 -0
- sqlalchemy/dialects/mssql/information_schema.py +285 -0
- sqlalchemy/dialects/mssql/json.py +129 -0
- sqlalchemy/dialects/mssql/provision.py +185 -0
- sqlalchemy/dialects/mssql/pymssql.py +126 -0
- sqlalchemy/dialects/mssql/pyodbc.py +760 -0
- sqlalchemy/dialects/mysql/__init__.py +104 -0
- sqlalchemy/dialects/mysql/aiomysql.py +250 -0
- sqlalchemy/dialects/mysql/asyncmy.py +231 -0
- sqlalchemy/dialects/mysql/base.py +3949 -0
- sqlalchemy/dialects/mysql/cymysql.py +106 -0
- sqlalchemy/dialects/mysql/dml.py +225 -0
- sqlalchemy/dialects/mysql/enumerated.py +282 -0
- sqlalchemy/dialects/mysql/expression.py +146 -0
- sqlalchemy/dialects/mysql/json.py +91 -0
- sqlalchemy/dialects/mysql/mariadb.py +72 -0
- sqlalchemy/dialects/mysql/mariadbconnector.py +322 -0
- sqlalchemy/dialects/mysql/mysqlconnector.py +302 -0
- sqlalchemy/dialects/mysql/mysqldb.py +314 -0
- sqlalchemy/dialects/mysql/provision.py +153 -0
- sqlalchemy/dialects/mysql/pymysql.py +158 -0
- sqlalchemy/dialects/mysql/pyodbc.py +157 -0
- sqlalchemy/dialects/mysql/reflection.py +727 -0
- sqlalchemy/dialects/mysql/reserved_words.py +570 -0
- sqlalchemy/dialects/mysql/types.py +835 -0
- sqlalchemy/dialects/oracle/__init__.py +81 -0
- sqlalchemy/dialects/oracle/base.py +3802 -0
- sqlalchemy/dialects/oracle/cx_oracle.py +1555 -0
- sqlalchemy/dialects/oracle/dictionary.py +507 -0
- sqlalchemy/dialects/oracle/oracledb.py +941 -0
- sqlalchemy/dialects/oracle/provision.py +297 -0
- sqlalchemy/dialects/oracle/types.py +316 -0
- sqlalchemy/dialects/oracle/vector.py +365 -0
- sqlalchemy/dialects/postgresql/__init__.py +167 -0
- sqlalchemy/dialects/postgresql/_psycopg_common.py +189 -0
- sqlalchemy/dialects/postgresql/array.py +519 -0
- sqlalchemy/dialects/postgresql/asyncpg.py +1284 -0
- sqlalchemy/dialects/postgresql/base.py +5378 -0
- sqlalchemy/dialects/postgresql/dml.py +339 -0
- sqlalchemy/dialects/postgresql/ext.py +540 -0
- sqlalchemy/dialects/postgresql/hstore.py +406 -0
- sqlalchemy/dialects/postgresql/json.py +404 -0
- sqlalchemy/dialects/postgresql/named_types.py +524 -0
- sqlalchemy/dialects/postgresql/operators.py +129 -0
- sqlalchemy/dialects/postgresql/pg8000.py +669 -0
- sqlalchemy/dialects/postgresql/pg_catalog.py +326 -0
- sqlalchemy/dialects/postgresql/provision.py +183 -0
- sqlalchemy/dialects/postgresql/psycopg.py +862 -0
- sqlalchemy/dialects/postgresql/psycopg2.py +892 -0
- sqlalchemy/dialects/postgresql/psycopg2cffi.py +61 -0
- sqlalchemy/dialects/postgresql/ranges.py +1031 -0
- sqlalchemy/dialects/postgresql/types.py +313 -0
- sqlalchemy/dialects/sqlite/__init__.py +57 -0
- sqlalchemy/dialects/sqlite/aiosqlite.py +482 -0
- sqlalchemy/dialects/sqlite/base.py +3056 -0
- sqlalchemy/dialects/sqlite/dml.py +263 -0
- sqlalchemy/dialects/sqlite/json.py +92 -0
- sqlalchemy/dialects/sqlite/provision.py +229 -0
- sqlalchemy/dialects/sqlite/pysqlcipher.py +157 -0
- sqlalchemy/dialects/sqlite/pysqlite.py +756 -0
- sqlalchemy/dialects/type_migration_guidelines.txt +145 -0
- sqlalchemy/engine/__init__.py +62 -0
- sqlalchemy/engine/_py_processors.py +136 -0
- sqlalchemy/engine/_py_row.py +128 -0
- sqlalchemy/engine/_py_util.py +74 -0
- sqlalchemy/engine/base.py +3390 -0
- sqlalchemy/engine/characteristics.py +155 -0
- sqlalchemy/engine/create.py +893 -0
- sqlalchemy/engine/cursor.py +2298 -0
- sqlalchemy/engine/default.py +2394 -0
- sqlalchemy/engine/events.py +965 -0
- sqlalchemy/engine/interfaces.py +3471 -0
- sqlalchemy/engine/mock.py +134 -0
- sqlalchemy/engine/processors.py +61 -0
- sqlalchemy/engine/reflection.py +2102 -0
- sqlalchemy/engine/result.py +2399 -0
- sqlalchemy/engine/row.py +400 -0
- sqlalchemy/engine/strategies.py +16 -0
- sqlalchemy/engine/url.py +924 -0
- sqlalchemy/engine/util.py +167 -0
- sqlalchemy/event/__init__.py +26 -0
- sqlalchemy/event/api.py +220 -0
- sqlalchemy/event/attr.py +676 -0
- sqlalchemy/event/base.py +472 -0
- sqlalchemy/event/legacy.py +258 -0
- sqlalchemy/event/registry.py +390 -0
- sqlalchemy/events.py +17 -0
- sqlalchemy/exc.py +832 -0
- sqlalchemy/ext/__init__.py +11 -0
- sqlalchemy/ext/associationproxy.py +2027 -0
- sqlalchemy/ext/asyncio/__init__.py +25 -0
- sqlalchemy/ext/asyncio/base.py +281 -0
- sqlalchemy/ext/asyncio/engine.py +1471 -0
- sqlalchemy/ext/asyncio/exc.py +21 -0
- sqlalchemy/ext/asyncio/result.py +965 -0
- sqlalchemy/ext/asyncio/scoping.py +1599 -0
- sqlalchemy/ext/asyncio/session.py +1947 -0
- sqlalchemy/ext/automap.py +1701 -0
- sqlalchemy/ext/baked.py +570 -0
- sqlalchemy/ext/compiler.py +600 -0
- sqlalchemy/ext/declarative/__init__.py +65 -0
- sqlalchemy/ext/declarative/extensions.py +564 -0
- sqlalchemy/ext/horizontal_shard.py +478 -0
- sqlalchemy/ext/hybrid.py +1535 -0
- sqlalchemy/ext/indexable.py +364 -0
- sqlalchemy/ext/instrumentation.py +450 -0
- sqlalchemy/ext/mutable.py +1085 -0
- sqlalchemy/ext/mypy/__init__.py +6 -0
- sqlalchemy/ext/mypy/apply.py +324 -0
- sqlalchemy/ext/mypy/decl_class.py +515 -0
- sqlalchemy/ext/mypy/infer.py +590 -0
- sqlalchemy/ext/mypy/names.py +335 -0
- sqlalchemy/ext/mypy/plugin.py +303 -0
- sqlalchemy/ext/mypy/util.py +357 -0
- sqlalchemy/ext/orderinglist.py +439 -0
- sqlalchemy/ext/serializer.py +185 -0
- sqlalchemy/future/__init__.py +16 -0
- sqlalchemy/future/engine.py +15 -0
- sqlalchemy/inspection.py +174 -0
- sqlalchemy/log.py +288 -0
- sqlalchemy/orm/__init__.py +171 -0
- sqlalchemy/orm/_orm_constructors.py +2661 -0
- sqlalchemy/orm/_typing.py +179 -0
- sqlalchemy/orm/attributes.py +2845 -0
- sqlalchemy/orm/base.py +971 -0
- sqlalchemy/orm/bulk_persistence.py +2135 -0
- sqlalchemy/orm/clsregistry.py +571 -0
- sqlalchemy/orm/collections.py +1627 -0
- sqlalchemy/orm/context.py +3334 -0
- sqlalchemy/orm/decl_api.py +2004 -0
- sqlalchemy/orm/decl_base.py +2192 -0
- sqlalchemy/orm/dependency.py +1302 -0
- sqlalchemy/orm/descriptor_props.py +1092 -0
- sqlalchemy/orm/dynamic.py +300 -0
- sqlalchemy/orm/evaluator.py +379 -0
- sqlalchemy/orm/events.py +3252 -0
- sqlalchemy/orm/exc.py +237 -0
- sqlalchemy/orm/identity.py +302 -0
- sqlalchemy/orm/instrumentation.py +754 -0
- sqlalchemy/orm/interfaces.py +1496 -0
- sqlalchemy/orm/loading.py +1686 -0
- sqlalchemy/orm/mapped_collection.py +557 -0
- sqlalchemy/orm/mapper.py +4444 -0
- sqlalchemy/orm/path_registry.py +809 -0
- sqlalchemy/orm/persistence.py +1788 -0
- sqlalchemy/orm/properties.py +935 -0
- sqlalchemy/orm/query.py +3459 -0
- sqlalchemy/orm/relationships.py +3508 -0
- sqlalchemy/orm/scoping.py +2148 -0
- sqlalchemy/orm/session.py +5280 -0
- sqlalchemy/orm/state.py +1168 -0
- sqlalchemy/orm/state_changes.py +196 -0
- sqlalchemy/orm/strategies.py +3470 -0
- sqlalchemy/orm/strategy_options.py +2568 -0
- sqlalchemy/orm/sync.py +164 -0
- sqlalchemy/orm/unitofwork.py +796 -0
- sqlalchemy/orm/util.py +2403 -0
- sqlalchemy/orm/writeonly.py +674 -0
- sqlalchemy/pool/__init__.py +44 -0
- sqlalchemy/pool/base.py +1524 -0
- sqlalchemy/pool/events.py +375 -0
- sqlalchemy/pool/impl.py +588 -0
- sqlalchemy/py.typed +0 -0
- sqlalchemy/schema.py +69 -0
- sqlalchemy/sql/__init__.py +145 -0
- sqlalchemy/sql/_dml_constructors.py +132 -0
- sqlalchemy/sql/_elements_constructors.py +1872 -0
- sqlalchemy/sql/_orm_types.py +20 -0
- sqlalchemy/sql/_py_util.py +75 -0
- sqlalchemy/sql/_selectable_constructors.py +763 -0
- sqlalchemy/sql/_typing.py +482 -0
- sqlalchemy/sql/annotation.py +587 -0
- sqlalchemy/sql/base.py +2293 -0
- sqlalchemy/sql/cache_key.py +1057 -0
- sqlalchemy/sql/coercions.py +1404 -0
- sqlalchemy/sql/compiler.py +8081 -0
- sqlalchemy/sql/crud.py +1752 -0
- sqlalchemy/sql/ddl.py +1444 -0
- sqlalchemy/sql/default_comparator.py +551 -0
- sqlalchemy/sql/dml.py +1850 -0
- sqlalchemy/sql/elements.py +5589 -0
- sqlalchemy/sql/events.py +458 -0
- sqlalchemy/sql/expression.py +159 -0
- sqlalchemy/sql/functions.py +2158 -0
- sqlalchemy/sql/lambdas.py +1442 -0
- sqlalchemy/sql/naming.py +209 -0
- sqlalchemy/sql/operators.py +2623 -0
- sqlalchemy/sql/roles.py +323 -0
- sqlalchemy/sql/schema.py +6222 -0
- sqlalchemy/sql/selectable.py +7265 -0
- sqlalchemy/sql/sqltypes.py +3930 -0
- sqlalchemy/sql/traversals.py +1024 -0
- sqlalchemy/sql/type_api.py +2368 -0
- sqlalchemy/sql/util.py +1485 -0
- sqlalchemy/sql/visitors.py +1164 -0
- sqlalchemy/testing/__init__.py +96 -0
- sqlalchemy/testing/assertions.py +994 -0
- sqlalchemy/testing/assertsql.py +520 -0
- sqlalchemy/testing/asyncio.py +135 -0
- sqlalchemy/testing/config.py +434 -0
- sqlalchemy/testing/engines.py +483 -0
- sqlalchemy/testing/entities.py +117 -0
- sqlalchemy/testing/exclusions.py +476 -0
- sqlalchemy/testing/fixtures/__init__.py +28 -0
- sqlalchemy/testing/fixtures/base.py +384 -0
- sqlalchemy/testing/fixtures/mypy.py +332 -0
- sqlalchemy/testing/fixtures/orm.py +227 -0
- sqlalchemy/testing/fixtures/sql.py +482 -0
- sqlalchemy/testing/pickleable.py +155 -0
- sqlalchemy/testing/plugin/__init__.py +6 -0
- sqlalchemy/testing/plugin/bootstrap.py +51 -0
- sqlalchemy/testing/plugin/plugin_base.py +828 -0
- sqlalchemy/testing/plugin/pytestplugin.py +892 -0
- sqlalchemy/testing/profiling.py +329 -0
- sqlalchemy/testing/provision.py +603 -0
- sqlalchemy/testing/requirements.py +1945 -0
- sqlalchemy/testing/schema.py +198 -0
- sqlalchemy/testing/suite/__init__.py +19 -0
- sqlalchemy/testing/suite/test_cte.py +237 -0
- sqlalchemy/testing/suite/test_ddl.py +389 -0
- sqlalchemy/testing/suite/test_deprecations.py +153 -0
- sqlalchemy/testing/suite/test_dialect.py +776 -0
- sqlalchemy/testing/suite/test_insert.py +630 -0
- sqlalchemy/testing/suite/test_reflection.py +3557 -0
- sqlalchemy/testing/suite/test_results.py +504 -0
- sqlalchemy/testing/suite/test_rowcount.py +258 -0
- sqlalchemy/testing/suite/test_select.py +2010 -0
- sqlalchemy/testing/suite/test_sequence.py +317 -0
- sqlalchemy/testing/suite/test_types.py +2147 -0
- sqlalchemy/testing/suite/test_unicode_ddl.py +189 -0
- sqlalchemy/testing/suite/test_update_delete.py +139 -0
- sqlalchemy/testing/util.py +535 -0
- sqlalchemy/testing/warnings.py +52 -0
- sqlalchemy/types.py +74 -0
- sqlalchemy/util/__init__.py +162 -0
- sqlalchemy/util/_collections.py +712 -0
- sqlalchemy/util/_concurrency_py3k.py +288 -0
- sqlalchemy/util/_has_cy.py +40 -0
- sqlalchemy/util/_py_collections.py +541 -0
- sqlalchemy/util/compat.py +421 -0
- sqlalchemy/util/concurrency.py +110 -0
- sqlalchemy/util/deprecations.py +401 -0
- sqlalchemy/util/langhelpers.py +2203 -0
- sqlalchemy/util/preloaded.py +150 -0
- sqlalchemy/util/queue.py +322 -0
- sqlalchemy/util/tool_support.py +201 -0
- sqlalchemy/util/topological.py +120 -0
- sqlalchemy/util/typing.py +734 -0
- sqlalchemy-2.0.47.dist-info/METADATA +243 -0
- sqlalchemy-2.0.47.dist-info/RECORD +274 -0
- sqlalchemy-2.0.47.dist-info/WHEEL +5 -0
- sqlalchemy-2.0.47.dist-info/licenses/LICENSE +19 -0
- sqlalchemy-2.0.47.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
# testing/plugin/pytestplugin.py
|
|
2
|
+
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
|
3
|
+
# <see AUTHORS file>
|
|
4
|
+
#
|
|
5
|
+
# This module is part of SQLAlchemy and is released under
|
|
6
|
+
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
|
+
# mypy: ignore-errors
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import collections
|
|
13
|
+
from functools import update_wrapper
|
|
14
|
+
import inspect
|
|
15
|
+
import itertools
|
|
16
|
+
import operator
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# installed by bootstrap.py
|
|
27
|
+
if not TYPE_CHECKING:
|
|
28
|
+
import sqla_plugin_base as plugin_base
|
|
29
|
+
except ImportError:
|
|
30
|
+
# assume we're a package, use traditional import
|
|
31
|
+
from . import plugin_base
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pytest_addoption(parser):
|
|
35
|
+
group = parser.getgroup("sqlalchemy")
|
|
36
|
+
|
|
37
|
+
def make_option(name, **kw):
|
|
38
|
+
callback_ = kw.pop("callback", None)
|
|
39
|
+
if callback_:
|
|
40
|
+
|
|
41
|
+
class CallableAction(argparse.Action):
|
|
42
|
+
def __call__(
|
|
43
|
+
self, parser, namespace, values, option_string=None
|
|
44
|
+
):
|
|
45
|
+
callback_(option_string, values, parser)
|
|
46
|
+
|
|
47
|
+
kw["action"] = CallableAction
|
|
48
|
+
|
|
49
|
+
zeroarg_callback = kw.pop("zeroarg_callback", None)
|
|
50
|
+
if zeroarg_callback:
|
|
51
|
+
|
|
52
|
+
class CallableAction(argparse.Action):
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
option_strings,
|
|
56
|
+
dest,
|
|
57
|
+
default=False,
|
|
58
|
+
required=False,
|
|
59
|
+
help=None, # noqa
|
|
60
|
+
):
|
|
61
|
+
super().__init__(
|
|
62
|
+
option_strings=option_strings,
|
|
63
|
+
dest=dest,
|
|
64
|
+
nargs=0,
|
|
65
|
+
const=True,
|
|
66
|
+
default=default,
|
|
67
|
+
required=required,
|
|
68
|
+
help=help,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def __call__(
|
|
72
|
+
self, parser, namespace, values, option_string=None
|
|
73
|
+
):
|
|
74
|
+
zeroarg_callback(option_string, values, parser)
|
|
75
|
+
|
|
76
|
+
kw["action"] = CallableAction
|
|
77
|
+
|
|
78
|
+
group.addoption(name, **kw)
|
|
79
|
+
|
|
80
|
+
plugin_base.setup_options(make_option)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def pytest_configure(config: pytest.Config):
|
|
84
|
+
plugin_base.read_config(config.rootpath)
|
|
85
|
+
if plugin_base.exclude_tags or plugin_base.include_tags:
|
|
86
|
+
new_expr = " and ".join(
|
|
87
|
+
list(plugin_base.include_tags)
|
|
88
|
+
+ [f"not {tag}" for tag in plugin_base.exclude_tags]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if config.option.markexpr:
|
|
92
|
+
config.option.markexpr += f" and {new_expr}"
|
|
93
|
+
else:
|
|
94
|
+
config.option.markexpr = new_expr
|
|
95
|
+
|
|
96
|
+
if config.pluginmanager.hasplugin("xdist"):
|
|
97
|
+
config.pluginmanager.register(XDistHooks())
|
|
98
|
+
|
|
99
|
+
if hasattr(config, "workerinput"):
|
|
100
|
+
plugin_base.restore_important_follower_config(config.workerinput)
|
|
101
|
+
plugin_base.configure_follower(config.workerinput["follower_ident"])
|
|
102
|
+
else:
|
|
103
|
+
if config.option.write_idents and os.path.exists(
|
|
104
|
+
config.option.write_idents
|
|
105
|
+
):
|
|
106
|
+
os.remove(config.option.write_idents)
|
|
107
|
+
|
|
108
|
+
plugin_base.pre_begin(config.option)
|
|
109
|
+
|
|
110
|
+
plugin_base.set_coverage_flag(
|
|
111
|
+
bool(getattr(config.option, "cov_source", False))
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
plugin_base.set_fixture_functions(PytestFixtureFunctions)
|
|
115
|
+
|
|
116
|
+
if config.option.dump_pyannotate:
|
|
117
|
+
global DUMP_PYANNOTATE
|
|
118
|
+
DUMP_PYANNOTATE = True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
DUMP_PYANNOTATE = False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.fixture(autouse=True)
|
|
125
|
+
def collect_types_fixture():
|
|
126
|
+
if DUMP_PYANNOTATE:
|
|
127
|
+
from pyannotate_runtime import collect_types
|
|
128
|
+
|
|
129
|
+
collect_types.start()
|
|
130
|
+
yield
|
|
131
|
+
if DUMP_PYANNOTATE:
|
|
132
|
+
collect_types.stop()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _log_sqlalchemy_info(session):
|
|
136
|
+
import sqlalchemy
|
|
137
|
+
from sqlalchemy import __version__
|
|
138
|
+
from sqlalchemy.util import has_compiled_ext
|
|
139
|
+
from sqlalchemy.util._has_cy import _CYEXTENSION_MSG
|
|
140
|
+
|
|
141
|
+
greet = "sqlalchemy installation"
|
|
142
|
+
site = "no user site" if sys.flags.no_user_site else "user site loaded"
|
|
143
|
+
msgs = [
|
|
144
|
+
f"SQLAlchemy {__version__} ({site})",
|
|
145
|
+
f"Path: {sqlalchemy.__file__}",
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if has_compiled_ext():
|
|
149
|
+
from sqlalchemy.cyextension import util
|
|
150
|
+
|
|
151
|
+
msgs.append(f"compiled extension enabled, e.g. {util.__file__} ")
|
|
152
|
+
else:
|
|
153
|
+
msgs.append(f"compiled extension not enabled; {_CYEXTENSION_MSG}")
|
|
154
|
+
|
|
155
|
+
pm = session.config.pluginmanager.get_plugin("terminalreporter")
|
|
156
|
+
if pm:
|
|
157
|
+
pm.write_sep("=", greet)
|
|
158
|
+
for m in msgs:
|
|
159
|
+
pm.write_line(m)
|
|
160
|
+
else:
|
|
161
|
+
# fancy pants reporter not found, fallback to plain print
|
|
162
|
+
print("=" * 25, greet, "=" * 25)
|
|
163
|
+
for m in msgs:
|
|
164
|
+
print(m)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def pytest_sessionstart(session):
|
|
168
|
+
from sqlalchemy.testing import asyncio
|
|
169
|
+
|
|
170
|
+
_log_sqlalchemy_info(session)
|
|
171
|
+
asyncio._assume_async(plugin_base.post_begin)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def pytest_sessionfinish(session):
|
|
175
|
+
from sqlalchemy.testing import asyncio
|
|
176
|
+
|
|
177
|
+
asyncio._maybe_async_provisioning(plugin_base.final_process_cleanup)
|
|
178
|
+
|
|
179
|
+
if session.config.option.dump_pyannotate:
|
|
180
|
+
from pyannotate_runtime import collect_types
|
|
181
|
+
|
|
182
|
+
collect_types.dump_stats(session.config.option.dump_pyannotate)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def pytest_unconfigure(config):
|
|
186
|
+
from sqlalchemy.testing import asyncio
|
|
187
|
+
|
|
188
|
+
asyncio._shutdown()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def pytest_collection_finish(session):
|
|
192
|
+
if session.config.option.dump_pyannotate:
|
|
193
|
+
from pyannotate_runtime import collect_types
|
|
194
|
+
|
|
195
|
+
lib_sqlalchemy = os.path.abspath("lib/sqlalchemy")
|
|
196
|
+
|
|
197
|
+
def _filter(filename):
|
|
198
|
+
filename = os.path.normpath(os.path.abspath(filename))
|
|
199
|
+
if "lib/sqlalchemy" not in os.path.commonpath(
|
|
200
|
+
[filename, lib_sqlalchemy]
|
|
201
|
+
):
|
|
202
|
+
return None
|
|
203
|
+
if "testing" in filename:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
return filename
|
|
207
|
+
|
|
208
|
+
collect_types.init_types_collection(filter_filename=_filter)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class XDistHooks:
|
|
212
|
+
def pytest_configure_node(self, node):
|
|
213
|
+
from sqlalchemy.testing import provision
|
|
214
|
+
from sqlalchemy.testing import asyncio
|
|
215
|
+
|
|
216
|
+
# the master for each node fills workerinput dictionary
|
|
217
|
+
# which pytest-xdist will transfer to the subprocess
|
|
218
|
+
|
|
219
|
+
plugin_base.memoize_important_follower_config(node.workerinput)
|
|
220
|
+
|
|
221
|
+
node.workerinput["follower_ident"] = "test_%s" % uuid.uuid4().hex[0:12]
|
|
222
|
+
|
|
223
|
+
asyncio._maybe_async_provisioning(
|
|
224
|
+
provision.create_follower_db, node.workerinput["follower_ident"]
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def pytest_testnodedown(self, node, error):
|
|
228
|
+
from sqlalchemy.testing import provision
|
|
229
|
+
from sqlalchemy.testing import asyncio
|
|
230
|
+
|
|
231
|
+
asyncio._maybe_async_provisioning(
|
|
232
|
+
provision.drop_follower_db, node.workerinput["follower_ident"]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def pytest_collection_modifyitems(session, config, items):
|
|
237
|
+
# look for all those classes that specify __backend__ and
|
|
238
|
+
# expand them out into per-database test cases.
|
|
239
|
+
|
|
240
|
+
# this is much easier to do within pytest_pycollect_makeitem, however
|
|
241
|
+
# pytest is iterating through cls.__dict__ as makeitem is
|
|
242
|
+
# called which causes a "dictionary changed size" error on py3k.
|
|
243
|
+
# I'd submit a pullreq for them to turn it into a list first, but
|
|
244
|
+
# it's to suit the rather odd use case here which is that we are adding
|
|
245
|
+
# new classes to a module on the fly.
|
|
246
|
+
|
|
247
|
+
from sqlalchemy.testing import asyncio
|
|
248
|
+
|
|
249
|
+
rebuilt_items = collections.defaultdict(
|
|
250
|
+
lambda: collections.defaultdict(list)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
items[:] = [
|
|
254
|
+
item
|
|
255
|
+
for item in items
|
|
256
|
+
if item.getparent(pytest.Class) is not None
|
|
257
|
+
and not item.getparent(pytest.Class).name.startswith("_")
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
test_classes = {item.getparent(pytest.Class) for item in items}
|
|
261
|
+
|
|
262
|
+
def collect(element):
|
|
263
|
+
for inst_or_fn in element.collect():
|
|
264
|
+
if isinstance(inst_or_fn, pytest.Collector):
|
|
265
|
+
yield from collect(inst_or_fn)
|
|
266
|
+
else:
|
|
267
|
+
yield inst_or_fn
|
|
268
|
+
|
|
269
|
+
def setup_test_classes():
|
|
270
|
+
for test_class in test_classes:
|
|
271
|
+
# transfer legacy __backend__ and __sparse_backend__ symbols
|
|
272
|
+
# to be markers
|
|
273
|
+
if getattr(test_class.cls, "__backend__", False) or getattr(
|
|
274
|
+
test_class.cls, "__only_on__", False
|
|
275
|
+
):
|
|
276
|
+
add_markers = {"backend"}
|
|
277
|
+
elif getattr(test_class.cls, "__sparse_backend__", False):
|
|
278
|
+
add_markers = {"sparse_backend", "backend"}
|
|
279
|
+
elif getattr(test_class.cls, "__sparse_driver_backend__", False):
|
|
280
|
+
add_markers = {"sparse_driver_backend", "backend"}
|
|
281
|
+
else:
|
|
282
|
+
add_markers = frozenset()
|
|
283
|
+
|
|
284
|
+
existing_markers = {
|
|
285
|
+
mark.name for mark in test_class.iter_markers()
|
|
286
|
+
}
|
|
287
|
+
add_markers = add_markers - existing_markers
|
|
288
|
+
all_markers = existing_markers.union(add_markers)
|
|
289
|
+
|
|
290
|
+
for marker in add_markers:
|
|
291
|
+
test_class.add_marker(marker)
|
|
292
|
+
|
|
293
|
+
sub_tests = list(
|
|
294
|
+
plugin_base.generate_sub_tests(
|
|
295
|
+
test_class.cls, test_class.module, all_markers
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
if not sub_tests:
|
|
299
|
+
rebuilt_items[test_class.cls]
|
|
300
|
+
|
|
301
|
+
for sub_cls in sub_tests:
|
|
302
|
+
if sub_cls is not test_class.cls:
|
|
303
|
+
per_cls_dict = rebuilt_items[test_class.cls]
|
|
304
|
+
|
|
305
|
+
module = test_class.getparent(pytest.Module)
|
|
306
|
+
|
|
307
|
+
new_cls = pytest.Class.from_parent(
|
|
308
|
+
name=sub_cls.__name__, parent=module
|
|
309
|
+
)
|
|
310
|
+
for marker in add_markers:
|
|
311
|
+
new_cls.add_marker(marker)
|
|
312
|
+
|
|
313
|
+
for fn in collect(new_cls):
|
|
314
|
+
per_cls_dict[fn.name].append(fn)
|
|
315
|
+
|
|
316
|
+
# class requirements will sometimes need to access the DB to check
|
|
317
|
+
# capabilities, so need to do this for async
|
|
318
|
+
asyncio._maybe_async_provisioning(setup_test_classes)
|
|
319
|
+
|
|
320
|
+
newitems = []
|
|
321
|
+
for item in items:
|
|
322
|
+
cls_ = item.cls
|
|
323
|
+
if cls_ in rebuilt_items:
|
|
324
|
+
newitems.extend(rebuilt_items[cls_][item.name])
|
|
325
|
+
else:
|
|
326
|
+
newitems.append(item)
|
|
327
|
+
|
|
328
|
+
# seems like the functions attached to a test class aren't sorted already?
|
|
329
|
+
# is that true and why's that? (when using unittest, they're sorted)
|
|
330
|
+
items[:] = sorted(
|
|
331
|
+
newitems,
|
|
332
|
+
key=lambda item: (
|
|
333
|
+
item.getparent(pytest.Module).name,
|
|
334
|
+
item.getparent(pytest.Class).name,
|
|
335
|
+
item.name,
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def pytest_pycollect_makeitem(collector, name, obj):
|
|
341
|
+
if inspect.isclass(obj) and plugin_base.want_class(name, obj):
|
|
342
|
+
from sqlalchemy.testing import config
|
|
343
|
+
|
|
344
|
+
if config.any_async:
|
|
345
|
+
obj = _apply_maybe_async(obj)
|
|
346
|
+
|
|
347
|
+
return [
|
|
348
|
+
pytest.Class.from_parent(
|
|
349
|
+
name=parametrize_cls.__name__, parent=collector
|
|
350
|
+
)
|
|
351
|
+
for parametrize_cls in _parametrize_cls(collector.module, obj)
|
|
352
|
+
]
|
|
353
|
+
elif (
|
|
354
|
+
inspect.isfunction(obj)
|
|
355
|
+
and collector.cls is not None
|
|
356
|
+
and plugin_base.want_method(collector.cls, obj)
|
|
357
|
+
):
|
|
358
|
+
# None means, fall back to default logic, which includes
|
|
359
|
+
# method-level parametrize
|
|
360
|
+
return None
|
|
361
|
+
else:
|
|
362
|
+
# empty list means skip this item
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _is_wrapped_coroutine_function(fn):
|
|
367
|
+
while hasattr(fn, "__wrapped__"):
|
|
368
|
+
fn = fn.__wrapped__
|
|
369
|
+
|
|
370
|
+
return inspect.iscoroutinefunction(fn)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _apply_maybe_async(obj, recurse=True):
|
|
374
|
+
from sqlalchemy.testing import asyncio
|
|
375
|
+
|
|
376
|
+
for name, value in vars(obj).items():
|
|
377
|
+
if (
|
|
378
|
+
(callable(value) or isinstance(value, classmethod))
|
|
379
|
+
and not getattr(value, "_maybe_async_applied", False)
|
|
380
|
+
and (name.startswith("test_"))
|
|
381
|
+
and not _is_wrapped_coroutine_function(value)
|
|
382
|
+
):
|
|
383
|
+
is_classmethod = False
|
|
384
|
+
if isinstance(value, classmethod):
|
|
385
|
+
value = value.__func__
|
|
386
|
+
is_classmethod = True
|
|
387
|
+
|
|
388
|
+
@_pytest_fn_decorator
|
|
389
|
+
def make_async(fn, *args, **kwargs):
|
|
390
|
+
return asyncio._maybe_async(fn, *args, **kwargs)
|
|
391
|
+
|
|
392
|
+
do_async = make_async(value)
|
|
393
|
+
if is_classmethod:
|
|
394
|
+
do_async = classmethod(do_async)
|
|
395
|
+
do_async._maybe_async_applied = True
|
|
396
|
+
|
|
397
|
+
setattr(obj, name, do_async)
|
|
398
|
+
if recurse:
|
|
399
|
+
for cls in obj.mro()[1:]:
|
|
400
|
+
if cls != object:
|
|
401
|
+
_apply_maybe_async(cls, False)
|
|
402
|
+
return obj
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _parametrize_cls(module, cls):
|
|
406
|
+
"""implement a class-based version of pytest parametrize."""
|
|
407
|
+
|
|
408
|
+
if "_sa_parametrize" not in cls.__dict__:
|
|
409
|
+
return [cls]
|
|
410
|
+
|
|
411
|
+
_sa_parametrize = cls._sa_parametrize
|
|
412
|
+
classes = []
|
|
413
|
+
for full_param_set in itertools.product(
|
|
414
|
+
*[params for argname, params in _sa_parametrize]
|
|
415
|
+
):
|
|
416
|
+
cls_variables = {}
|
|
417
|
+
|
|
418
|
+
for argname, param in zip(
|
|
419
|
+
[_sa_param[0] for _sa_param in _sa_parametrize], full_param_set
|
|
420
|
+
):
|
|
421
|
+
if not argname:
|
|
422
|
+
raise TypeError("need argnames for class-based combinations")
|
|
423
|
+
argname_split = re.split(r",\s*", argname)
|
|
424
|
+
for arg, val in zip(argname_split, param.values):
|
|
425
|
+
cls_variables[arg] = val
|
|
426
|
+
parametrized_name = "_".join(
|
|
427
|
+
re.sub(r"\W", "", token)
|
|
428
|
+
for param in full_param_set
|
|
429
|
+
for token in param.id.split("-")
|
|
430
|
+
)
|
|
431
|
+
name = "%s_%s" % (cls.__name__, parametrized_name)
|
|
432
|
+
newcls = type.__new__(type, name, (cls,), cls_variables)
|
|
433
|
+
setattr(module, name, newcls)
|
|
434
|
+
classes.append(newcls)
|
|
435
|
+
return classes
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
_current_class = None
|
|
439
|
+
|
|
440
|
+
_current_warning_context = None
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def pytest_runtest_setup(item):
|
|
444
|
+
from sqlalchemy.testing import asyncio
|
|
445
|
+
|
|
446
|
+
# pytest_runtest_setup runs *before* pytest fixtures with scope="class".
|
|
447
|
+
# plugin_base.start_test_class_outside_fixtures may opt to raise SkipTest
|
|
448
|
+
# for the whole class and has to run things that are across all current
|
|
449
|
+
# databases, so we run this outside of the pytest fixture system altogether
|
|
450
|
+
# and ensure asyncio greenlet if any engines are async
|
|
451
|
+
|
|
452
|
+
global _current_class, _current_warning_context
|
|
453
|
+
|
|
454
|
+
if isinstance(item, pytest.Function) and _current_class is None:
|
|
455
|
+
asyncio._maybe_async_provisioning(
|
|
456
|
+
plugin_base.start_test_class_outside_fixtures,
|
|
457
|
+
item.cls,
|
|
458
|
+
)
|
|
459
|
+
_current_class = item.getparent(pytest.Class)
|
|
460
|
+
|
|
461
|
+
if hasattr(_current_class.cls, "__warnings__"):
|
|
462
|
+
import warnings
|
|
463
|
+
|
|
464
|
+
_current_warning_context = warnings.catch_warnings()
|
|
465
|
+
_current_warning_context.__enter__()
|
|
466
|
+
for warning_message in _current_class.cls.__warnings__:
|
|
467
|
+
warnings.filterwarnings("ignore", warning_message)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
471
|
+
def pytest_runtest_teardown(item, nextitem):
|
|
472
|
+
# runs inside of pytest function fixture scope
|
|
473
|
+
# after test function runs
|
|
474
|
+
|
|
475
|
+
from sqlalchemy.testing import asyncio
|
|
476
|
+
|
|
477
|
+
asyncio._maybe_async(plugin_base.after_test, item)
|
|
478
|
+
|
|
479
|
+
yield
|
|
480
|
+
# this is now after all the fixture teardown have run, the class can be
|
|
481
|
+
# finalized. Since pytest v7 this finalizer can no longer be added in
|
|
482
|
+
# pytest_runtest_setup since the class has not yet been setup at that
|
|
483
|
+
# time.
|
|
484
|
+
# See https://github.com/pytest-dev/pytest/issues/9343
|
|
485
|
+
|
|
486
|
+
global _current_class, _current_report, _current_warning_context
|
|
487
|
+
|
|
488
|
+
if _current_class is not None and (
|
|
489
|
+
# last test or a new class
|
|
490
|
+
nextitem is None
|
|
491
|
+
or nextitem.getparent(pytest.Class) is not _current_class
|
|
492
|
+
):
|
|
493
|
+
|
|
494
|
+
if _current_warning_context is not None:
|
|
495
|
+
_current_warning_context.__exit__(None, None, None)
|
|
496
|
+
_current_warning_context = None
|
|
497
|
+
|
|
498
|
+
_current_class = None
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
asyncio._maybe_async_provisioning(
|
|
502
|
+
plugin_base.stop_test_class_outside_fixtures, item.cls
|
|
503
|
+
)
|
|
504
|
+
except Exception as e:
|
|
505
|
+
# in case of an exception during teardown attach the original
|
|
506
|
+
# error to the exception message, otherwise it will get lost
|
|
507
|
+
if _current_report.failed:
|
|
508
|
+
if not e.args:
|
|
509
|
+
e.args = (
|
|
510
|
+
"__Original test failure__:\n"
|
|
511
|
+
+ _current_report.longreprtext,
|
|
512
|
+
)
|
|
513
|
+
elif e.args[-1] and isinstance(e.args[-1], str):
|
|
514
|
+
args = list(e.args)
|
|
515
|
+
args[-1] += (
|
|
516
|
+
"\n__Original test failure__:\n"
|
|
517
|
+
+ _current_report.longreprtext
|
|
518
|
+
)
|
|
519
|
+
e.args = tuple(args)
|
|
520
|
+
else:
|
|
521
|
+
e.args += (
|
|
522
|
+
"__Original test failure__",
|
|
523
|
+
_current_report.longreprtext,
|
|
524
|
+
)
|
|
525
|
+
raise
|
|
526
|
+
finally:
|
|
527
|
+
_current_report = None
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def pytest_runtest_call(item):
|
|
531
|
+
# runs inside of pytest function fixture scope
|
|
532
|
+
# before test function runs
|
|
533
|
+
|
|
534
|
+
from sqlalchemy.testing import asyncio
|
|
535
|
+
|
|
536
|
+
asyncio._maybe_async(
|
|
537
|
+
plugin_base.before_test,
|
|
538
|
+
item,
|
|
539
|
+
item.module.__name__,
|
|
540
|
+
item.cls,
|
|
541
|
+
item.name,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
_current_report = None
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def pytest_runtest_logreport(report):
|
|
549
|
+
global _current_report
|
|
550
|
+
if report.when == "call":
|
|
551
|
+
_current_report = report
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@pytest.fixture(scope="class")
|
|
555
|
+
def setup_class_methods(request):
|
|
556
|
+
from sqlalchemy.testing import asyncio
|
|
557
|
+
|
|
558
|
+
cls = request.cls
|
|
559
|
+
|
|
560
|
+
if hasattr(cls, "setup_test_class"):
|
|
561
|
+
asyncio._maybe_async(cls.setup_test_class)
|
|
562
|
+
|
|
563
|
+
yield
|
|
564
|
+
|
|
565
|
+
if hasattr(cls, "teardown_test_class"):
|
|
566
|
+
asyncio._maybe_async(cls.teardown_test_class)
|
|
567
|
+
|
|
568
|
+
asyncio._maybe_async(plugin_base.stop_test_class, cls)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@pytest.fixture(scope="function")
|
|
572
|
+
def setup_test_methods(request):
|
|
573
|
+
from sqlalchemy.testing import asyncio
|
|
574
|
+
|
|
575
|
+
# called for each test
|
|
576
|
+
|
|
577
|
+
self = request.instance
|
|
578
|
+
|
|
579
|
+
# before this fixture runs:
|
|
580
|
+
|
|
581
|
+
# 1. function level "autouse" fixtures under py3k (examples: TablesTest
|
|
582
|
+
# define tables / data, MappedTest define tables / mappers / data)
|
|
583
|
+
|
|
584
|
+
# 2. was for p2k. no longer applies
|
|
585
|
+
|
|
586
|
+
# 3. run outer xdist-style setup
|
|
587
|
+
if hasattr(self, "setup_test"):
|
|
588
|
+
asyncio._maybe_async(self.setup_test)
|
|
589
|
+
|
|
590
|
+
# alembic test suite is using setUp and tearDown
|
|
591
|
+
# xdist methods; support these in the test suite
|
|
592
|
+
# for the near term
|
|
593
|
+
if hasattr(self, "setUp"):
|
|
594
|
+
asyncio._maybe_async(self.setUp)
|
|
595
|
+
|
|
596
|
+
# inside the yield:
|
|
597
|
+
# 4. function level fixtures defined on test functions themselves,
|
|
598
|
+
# e.g. "connection", "metadata" run next
|
|
599
|
+
|
|
600
|
+
# 5. pytest hook pytest_runtest_call then runs
|
|
601
|
+
|
|
602
|
+
# 6. test itself runs
|
|
603
|
+
|
|
604
|
+
yield
|
|
605
|
+
|
|
606
|
+
# yield finishes:
|
|
607
|
+
|
|
608
|
+
# 7. function level fixtures defined on test functions
|
|
609
|
+
# themselves, e.g. "connection" rolls back the transaction, "metadata"
|
|
610
|
+
# emits drop all
|
|
611
|
+
|
|
612
|
+
# 8. pytest hook pytest_runtest_teardown hook runs, this is associated
|
|
613
|
+
# with fixtures close all sessions, provisioning.stop_test_class(),
|
|
614
|
+
# engines.testing_reaper -> ensure all connection pool connections
|
|
615
|
+
# are returned, engines created by testing_engine that aren't the
|
|
616
|
+
# config engine are disposed
|
|
617
|
+
|
|
618
|
+
asyncio._maybe_async(plugin_base.after_test_fixtures, self)
|
|
619
|
+
|
|
620
|
+
# 10. run xdist-style teardown
|
|
621
|
+
if hasattr(self, "tearDown"):
|
|
622
|
+
asyncio._maybe_async(self.tearDown)
|
|
623
|
+
|
|
624
|
+
if hasattr(self, "teardown_test"):
|
|
625
|
+
asyncio._maybe_async(self.teardown_test)
|
|
626
|
+
|
|
627
|
+
# 11. was for p2k. no longer applies
|
|
628
|
+
|
|
629
|
+
# 12. function level "autouse" fixtures under py3k (examples: TablesTest /
|
|
630
|
+
# MappedTest delete table data, possibly drop tables and clear mappers
|
|
631
|
+
# depending on the flags defined by the test class)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _pytest_fn_decorator(target):
|
|
635
|
+
"""Port of langhelpers.decorator with pytest-specific tricks."""
|
|
636
|
+
|
|
637
|
+
from sqlalchemy.util.langhelpers import format_argspec_plus
|
|
638
|
+
from sqlalchemy.util.compat import inspect_getfullargspec
|
|
639
|
+
|
|
640
|
+
def _exec_code_in_env(code, env, fn_name):
|
|
641
|
+
# note this is affected by "from __future__ import annotations" at
|
|
642
|
+
# the top; exec'ed code will use non-evaluated annotations
|
|
643
|
+
# which allows us to be more flexible with code rendering
|
|
644
|
+
# in format_argpsec_plus()
|
|
645
|
+
exec(code, env)
|
|
646
|
+
return env[fn_name]
|
|
647
|
+
|
|
648
|
+
def decorate(fn, add_positional_parameters=()):
|
|
649
|
+
spec = inspect_getfullargspec(fn)
|
|
650
|
+
if add_positional_parameters:
|
|
651
|
+
spec.args.extend(add_positional_parameters)
|
|
652
|
+
|
|
653
|
+
metadata = dict(
|
|
654
|
+
__target_fn="__target_fn", __orig_fn="__orig_fn", name=fn.__name__
|
|
655
|
+
)
|
|
656
|
+
metadata.update(format_argspec_plus(spec, grouped=False))
|
|
657
|
+
code = (
|
|
658
|
+
"""\
|
|
659
|
+
def %(name)s%(grouped_args)s:
|
|
660
|
+
return %(__target_fn)s(%(__orig_fn)s, %(apply_kw)s)
|
|
661
|
+
"""
|
|
662
|
+
% metadata
|
|
663
|
+
)
|
|
664
|
+
decorated = _exec_code_in_env(
|
|
665
|
+
code, {"__target_fn": target, "__orig_fn": fn}, fn.__name__
|
|
666
|
+
)
|
|
667
|
+
if not add_positional_parameters:
|
|
668
|
+
decorated.__defaults__ = getattr(fn, "__func__", fn).__defaults__
|
|
669
|
+
decorated.__wrapped__ = fn
|
|
670
|
+
return update_wrapper(decorated, fn)
|
|
671
|
+
else:
|
|
672
|
+
# this is the pytest hacky part. don't do a full update wrapper
|
|
673
|
+
# because pytest is really being sneaky about finding the args
|
|
674
|
+
# for the wrapped function
|
|
675
|
+
decorated.__module__ = fn.__module__
|
|
676
|
+
decorated.__name__ = fn.__name__
|
|
677
|
+
if hasattr(fn, "pytestmark"):
|
|
678
|
+
decorated.pytestmark = fn.pytestmark
|
|
679
|
+
return decorated
|
|
680
|
+
|
|
681
|
+
return decorate
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class PytestFixtureFunctions(plugin_base.FixtureFunctions):
|
|
685
|
+
def skip_test_exception(self, *arg, **kw):
|
|
686
|
+
return pytest.skip.Exception(*arg, **kw)
|
|
687
|
+
|
|
688
|
+
@property
|
|
689
|
+
def add_to_marker(self):
|
|
690
|
+
return pytest.mark
|
|
691
|
+
|
|
692
|
+
def mark_base_test_class(self):
|
|
693
|
+
return pytest.mark.usefixtures(
|
|
694
|
+
"setup_class_methods",
|
|
695
|
+
"setup_test_methods",
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
_combination_id_fns = {
|
|
699
|
+
"i": lambda obj: obj,
|
|
700
|
+
"r": repr,
|
|
701
|
+
"s": str,
|
|
702
|
+
"n": lambda obj: (
|
|
703
|
+
obj.__name__ if hasattr(obj, "__name__") else type(obj).__name__
|
|
704
|
+
),
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
def combinations(self, *arg_sets, **kw):
|
|
708
|
+
"""Facade for pytest.mark.parametrize.
|
|
709
|
+
|
|
710
|
+
Automatically derives argument names from the callable which in our
|
|
711
|
+
case is always a method on a class with positional arguments.
|
|
712
|
+
|
|
713
|
+
ids for parameter sets are derived using an optional template.
|
|
714
|
+
|
|
715
|
+
"""
|
|
716
|
+
from sqlalchemy.testing import exclusions
|
|
717
|
+
|
|
718
|
+
if len(arg_sets) == 1 and hasattr(arg_sets[0], "__next__"):
|
|
719
|
+
arg_sets = list(arg_sets[0])
|
|
720
|
+
|
|
721
|
+
argnames = kw.pop("argnames", None)
|
|
722
|
+
|
|
723
|
+
def _filter_exclusions(args):
|
|
724
|
+
result = []
|
|
725
|
+
gathered_exclusions = []
|
|
726
|
+
for a in args:
|
|
727
|
+
if isinstance(a, exclusions.compound):
|
|
728
|
+
gathered_exclusions.append(a)
|
|
729
|
+
else:
|
|
730
|
+
result.append(a)
|
|
731
|
+
|
|
732
|
+
return result, gathered_exclusions
|
|
733
|
+
|
|
734
|
+
id_ = kw.pop("id_", None)
|
|
735
|
+
|
|
736
|
+
tobuild_pytest_params = []
|
|
737
|
+
has_exclusions = False
|
|
738
|
+
if id_:
|
|
739
|
+
_combination_id_fns = self._combination_id_fns
|
|
740
|
+
|
|
741
|
+
# because itemgetter is not consistent for one argument vs.
|
|
742
|
+
# multiple, make it multiple in all cases and use a slice
|
|
743
|
+
# to omit the first argument
|
|
744
|
+
_arg_getter = operator.itemgetter(
|
|
745
|
+
0,
|
|
746
|
+
*[
|
|
747
|
+
idx
|
|
748
|
+
for idx, char in enumerate(id_)
|
|
749
|
+
if char in ("n", "r", "s", "a")
|
|
750
|
+
],
|
|
751
|
+
)
|
|
752
|
+
fns = [
|
|
753
|
+
(operator.itemgetter(idx), _combination_id_fns[char])
|
|
754
|
+
for idx, char in enumerate(id_)
|
|
755
|
+
if char in _combination_id_fns
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
for arg in arg_sets:
|
|
759
|
+
if not isinstance(arg, tuple):
|
|
760
|
+
arg = (arg,)
|
|
761
|
+
|
|
762
|
+
fn_params, param_exclusions = _filter_exclusions(arg)
|
|
763
|
+
|
|
764
|
+
parameters = _arg_getter(fn_params)[1:]
|
|
765
|
+
|
|
766
|
+
if param_exclusions:
|
|
767
|
+
has_exclusions = True
|
|
768
|
+
|
|
769
|
+
tobuild_pytest_params.append(
|
|
770
|
+
(
|
|
771
|
+
parameters,
|
|
772
|
+
param_exclusions,
|
|
773
|
+
"-".join(
|
|
774
|
+
comb_fn(getter(arg)) for getter, comb_fn in fns
|
|
775
|
+
),
|
|
776
|
+
)
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
else:
|
|
780
|
+
for arg in arg_sets:
|
|
781
|
+
if not isinstance(arg, tuple):
|
|
782
|
+
arg = (arg,)
|
|
783
|
+
|
|
784
|
+
fn_params, param_exclusions = _filter_exclusions(arg)
|
|
785
|
+
|
|
786
|
+
if param_exclusions:
|
|
787
|
+
has_exclusions = True
|
|
788
|
+
|
|
789
|
+
tobuild_pytest_params.append(
|
|
790
|
+
(fn_params, param_exclusions, None)
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
pytest_params = []
|
|
794
|
+
for parameters, param_exclusions, id_ in tobuild_pytest_params:
|
|
795
|
+
if has_exclusions:
|
|
796
|
+
parameters += (param_exclusions,)
|
|
797
|
+
|
|
798
|
+
param = pytest.param(*parameters, id=id_)
|
|
799
|
+
pytest_params.append(param)
|
|
800
|
+
|
|
801
|
+
def decorate(fn):
|
|
802
|
+
if inspect.isclass(fn):
|
|
803
|
+
if has_exclusions:
|
|
804
|
+
raise NotImplementedError(
|
|
805
|
+
"exclusions not supported for class level combinations"
|
|
806
|
+
)
|
|
807
|
+
if "_sa_parametrize" not in fn.__dict__:
|
|
808
|
+
fn._sa_parametrize = []
|
|
809
|
+
fn._sa_parametrize.append((argnames, pytest_params))
|
|
810
|
+
return fn
|
|
811
|
+
else:
|
|
812
|
+
_fn_argnames = inspect.getfullargspec(fn).args[1:]
|
|
813
|
+
if argnames is None:
|
|
814
|
+
_argnames = _fn_argnames
|
|
815
|
+
else:
|
|
816
|
+
_argnames = re.split(r", *", argnames)
|
|
817
|
+
|
|
818
|
+
if has_exclusions:
|
|
819
|
+
existing_exl = sum(
|
|
820
|
+
1 for n in _fn_argnames if n.startswith("_exclusions")
|
|
821
|
+
)
|
|
822
|
+
current_exclusion_name = f"_exclusions_{existing_exl}"
|
|
823
|
+
_argnames += [current_exclusion_name]
|
|
824
|
+
|
|
825
|
+
@_pytest_fn_decorator
|
|
826
|
+
def check_exclusions(fn, *args, **kw):
|
|
827
|
+
_exclusions = args[-1]
|
|
828
|
+
if _exclusions:
|
|
829
|
+
exlu = exclusions.compound().add(*_exclusions)
|
|
830
|
+
fn = exlu(fn)
|
|
831
|
+
return fn(*args[:-1], **kw)
|
|
832
|
+
|
|
833
|
+
fn = check_exclusions(
|
|
834
|
+
fn, add_positional_parameters=(current_exclusion_name,)
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
return pytest.mark.parametrize(_argnames, pytest_params)(fn)
|
|
838
|
+
|
|
839
|
+
return decorate
|
|
840
|
+
|
|
841
|
+
def param_ident(self, *parameters):
|
|
842
|
+
ident = parameters[0]
|
|
843
|
+
return pytest.param(*parameters[1:], id=ident)
|
|
844
|
+
|
|
845
|
+
def fixture(self, *arg, **kw):
|
|
846
|
+
from sqlalchemy.testing import config
|
|
847
|
+
from sqlalchemy.testing import asyncio
|
|
848
|
+
|
|
849
|
+
# wrapping pytest.fixture function. determine if
|
|
850
|
+
# decorator was called as @fixture or @fixture().
|
|
851
|
+
if len(arg) > 0 and callable(arg[0]):
|
|
852
|
+
# was called as @fixture(), we have the function to wrap.
|
|
853
|
+
fn = arg[0]
|
|
854
|
+
arg = arg[1:]
|
|
855
|
+
else:
|
|
856
|
+
# was called as @fixture, don't have the function yet.
|
|
857
|
+
fn = None
|
|
858
|
+
|
|
859
|
+
# create a pytest.fixture marker. because the fn is not being
|
|
860
|
+
# passed, this is always a pytest.FixtureFunctionMarker()
|
|
861
|
+
# object (or whatever pytest is calling it when you read this)
|
|
862
|
+
# that is waiting for a function.
|
|
863
|
+
fixture = pytest.fixture(*arg, **kw)
|
|
864
|
+
|
|
865
|
+
# now apply wrappers to the function, including fixture itself
|
|
866
|
+
|
|
867
|
+
def wrap(fn):
|
|
868
|
+
if config.any_async:
|
|
869
|
+
fn = asyncio._maybe_async_wrapper(fn)
|
|
870
|
+
# other wrappers may be added here
|
|
871
|
+
|
|
872
|
+
# now apply FixtureFunctionMarker
|
|
873
|
+
fn = fixture(fn)
|
|
874
|
+
|
|
875
|
+
return fn
|
|
876
|
+
|
|
877
|
+
if fn:
|
|
878
|
+
return wrap(fn)
|
|
879
|
+
else:
|
|
880
|
+
return wrap
|
|
881
|
+
|
|
882
|
+
def get_current_test_name(self):
|
|
883
|
+
return os.environ.get("PYTEST_CURRENT_TEST")
|
|
884
|
+
|
|
885
|
+
def async_test(self, fn):
|
|
886
|
+
from sqlalchemy.testing import asyncio
|
|
887
|
+
|
|
888
|
+
@_pytest_fn_decorator
|
|
889
|
+
def decorate(fn, *args, **kwargs):
|
|
890
|
+
asyncio._run_coroutine_function(fn, *args, **kwargs)
|
|
891
|
+
|
|
892
|
+
return decorate(fn)
|