Загрузить файлы в «venv/Lib/site-packages/sqlalchemy/testing»
This commit is contained in:
602
venv/Lib/site-packages/sqlalchemy/testing/provision.py
Normal file
602
venv/Lib/site-packages/sqlalchemy/testing/provision.py
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
# testing/provision.py
|
||||||
|
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||||
|
# <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||||
|
# mypy: ignore-errors
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
from . import engines
|
||||||
|
from . import util
|
||||||
|
from .. import exc
|
||||||
|
from .. import inspect
|
||||||
|
from ..engine import Connection
|
||||||
|
from ..engine import Engine
|
||||||
|
from ..engine import url as sa_url
|
||||||
|
from ..schema import sort_tables_and_constraints
|
||||||
|
from ..sql import ddl
|
||||||
|
from ..sql import schema
|
||||||
|
from ..util import decorator
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
FOLLOWER_IDENT = None
|
||||||
|
|
||||||
|
|
||||||
|
class register:
|
||||||
|
def __init__(self, decorator=None):
|
||||||
|
self.fns = {}
|
||||||
|
self.decorator = decorator
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init(cls, fn):
|
||||||
|
return register().for_db("*")(fn)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_decorator(cls, decorator):
|
||||||
|
return register(decorator).for_db("*")
|
||||||
|
|
||||||
|
def for_db(self, *dbnames):
|
||||||
|
def decorate(fn):
|
||||||
|
if self.decorator:
|
||||||
|
fn = self.decorator(fn)
|
||||||
|
for dbname in dbnames:
|
||||||
|
self.fns[dbname] = fn
|
||||||
|
return self
|
||||||
|
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
def call_original(self, cfg, *arg, **kw):
|
||||||
|
return self.fns["*"](cfg, *arg, **kw)
|
||||||
|
|
||||||
|
def __call__(self, cfg, *arg, **kw):
|
||||||
|
if isinstance(cfg, str):
|
||||||
|
url = sa_url.make_url(cfg)
|
||||||
|
elif isinstance(cfg, sa_url.URL):
|
||||||
|
url = cfg
|
||||||
|
elif isinstance(cfg, (Engine, Connection)):
|
||||||
|
url = cfg.engine.url
|
||||||
|
else:
|
||||||
|
url = cfg.db.url
|
||||||
|
backend = url.get_backend_name()
|
||||||
|
if backend in self.fns:
|
||||||
|
return self.fns[backend](cfg, *arg, **kw)
|
||||||
|
else:
|
||||||
|
return self.fns["*"](cfg, *arg, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
def create_follower_db(follower_ident):
|
||||||
|
for cfg in _configs_for_db_operation():
|
||||||
|
log.info("CREATE database %s, URI %r", follower_ident, cfg.db.url)
|
||||||
|
create_db(cfg, cfg.db, follower_ident)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_config(db_url, options, file_config, follower_ident):
|
||||||
|
# load the dialect, which should also have it set up its provision
|
||||||
|
# hooks
|
||||||
|
|
||||||
|
dialect = sa_url.make_url(db_url).get_dialect()
|
||||||
|
|
||||||
|
dialect.load_provisioning()
|
||||||
|
|
||||||
|
if follower_ident:
|
||||||
|
db_url = follower_url_from_main(db_url, follower_ident)
|
||||||
|
db_opts = {}
|
||||||
|
update_db_opts(db_url, db_opts, options)
|
||||||
|
db_opts["scope"] = "global"
|
||||||
|
eng = engines.testing_engine(db_url, db_opts)
|
||||||
|
|
||||||
|
post_configure_engine(db_url, eng, follower_ident)
|
||||||
|
|
||||||
|
eng.connect().close()
|
||||||
|
|
||||||
|
cfg = config.Config.register(eng, db_opts, options, file_config)
|
||||||
|
|
||||||
|
# a symbolic name that tests can use if they need to disambiguate
|
||||||
|
# names across databases
|
||||||
|
if follower_ident:
|
||||||
|
config.ident = follower_ident
|
||||||
|
|
||||||
|
if follower_ident:
|
||||||
|
configure_follower(cfg, follower_ident)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def drop_follower_db(follower_ident):
|
||||||
|
for cfg in _configs_for_db_operation():
|
||||||
|
log.info("DROP database %s, URI %r", follower_ident, cfg.db.url)
|
||||||
|
drop_db(cfg, cfg.db, follower_ident)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_db_urls(db_urls, extra_drivers):
|
||||||
|
"""Generate a set of URLs to test given configured URLs plus additional
|
||||||
|
driver names.
|
||||||
|
|
||||||
|
Given:
|
||||||
|
|
||||||
|
.. sourcecode:: text
|
||||||
|
|
||||||
|
--dburi postgresql://db1 \
|
||||||
|
--dburi postgresql://db2 \
|
||||||
|
--dburi postgresql://db2 \
|
||||||
|
--dbdriver=psycopg2 --dbdriver=asyncpg?async_fallback=true
|
||||||
|
|
||||||
|
Noting that the default postgresql driver is psycopg2, the output
|
||||||
|
would be:
|
||||||
|
|
||||||
|
.. sourcecode:: text
|
||||||
|
|
||||||
|
postgresql+psycopg2://db1
|
||||||
|
postgresql+asyncpg://db1
|
||||||
|
postgresql+psycopg2://db2
|
||||||
|
postgresql+psycopg2://db3
|
||||||
|
|
||||||
|
That is, for the driver in a --dburi, we want to keep that and use that
|
||||||
|
driver for each URL it's part of . For a driver that is only
|
||||||
|
in --dbdrivers, we want to use it just once for one of the URLs.
|
||||||
|
for a driver that is both coming from --dburi as well as --dbdrivers,
|
||||||
|
we want to keep it in that dburi.
|
||||||
|
|
||||||
|
Driver specific query options can be specified by added them to the
|
||||||
|
driver name. For example, to enable the async fallback option for
|
||||||
|
asyncpg::
|
||||||
|
|
||||||
|
.. sourcecode:: text
|
||||||
|
|
||||||
|
--dburi postgresql://db1 \
|
||||||
|
--dbdriver=asyncpg?async_fallback=true
|
||||||
|
|
||||||
|
"""
|
||||||
|
urls = set()
|
||||||
|
|
||||||
|
backend_to_driver_we_already_have = collections.defaultdict(set)
|
||||||
|
|
||||||
|
urls_plus_dialects = [
|
||||||
|
(url_obj, url_obj.get_dialect())
|
||||||
|
for url_obj in [sa_url.make_url(db_url) for db_url in db_urls]
|
||||||
|
]
|
||||||
|
|
||||||
|
for url_obj, dialect in urls_plus_dialects:
|
||||||
|
# use get_driver_name instead of dialect.driver to account for
|
||||||
|
# "_async" virtual drivers like oracledb and psycopg
|
||||||
|
driver_name = url_obj.get_driver_name()
|
||||||
|
backend_to_driver_we_already_have[dialect.name].add(driver_name)
|
||||||
|
|
||||||
|
backend_to_driver_we_need = {}
|
||||||
|
|
||||||
|
for url_obj, dialect in urls_plus_dialects:
|
||||||
|
backend = dialect.name
|
||||||
|
dialect.load_provisioning()
|
||||||
|
|
||||||
|
if backend not in backend_to_driver_we_need:
|
||||||
|
backend_to_driver_we_need[backend] = extra_per_backend = set(
|
||||||
|
extra_drivers
|
||||||
|
).difference(backend_to_driver_we_already_have[backend])
|
||||||
|
else:
|
||||||
|
extra_per_backend = backend_to_driver_we_need[backend]
|
||||||
|
|
||||||
|
for driver_url in _generate_driver_urls(url_obj, extra_per_backend):
|
||||||
|
if driver_url in urls:
|
||||||
|
continue
|
||||||
|
urls.add(driver_url)
|
||||||
|
yield driver_url
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_driver_urls(url, extra_drivers):
|
||||||
|
main_driver = url.get_driver_name()
|
||||||
|
extra_drivers.discard(main_driver)
|
||||||
|
|
||||||
|
url = generate_driver_url(url, main_driver, "")
|
||||||
|
yield url
|
||||||
|
|
||||||
|
for drv in list(extra_drivers):
|
||||||
|
if "?" in drv:
|
||||||
|
driver_only, query_str = drv.split("?", 1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
driver_only = drv
|
||||||
|
query_str = None
|
||||||
|
|
||||||
|
new_url = generate_driver_url(url, driver_only, query_str)
|
||||||
|
if new_url:
|
||||||
|
extra_drivers.remove(drv)
|
||||||
|
|
||||||
|
yield new_url
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def is_preferred_driver(cfg, engine):
|
||||||
|
"""Return True if the engine's URL is on the "default" driver, or
|
||||||
|
more generally the "preferred" driver to use for tests.
|
||||||
|
|
||||||
|
Backends can override this to make a different driver the "prefeferred"
|
||||||
|
driver that's not the default.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
engine.url._get_entrypoint()
|
||||||
|
is engine.url.set(
|
||||||
|
drivername=engine.url.get_backend_name()
|
||||||
|
)._get_entrypoint()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def generate_driver_url(url, driver, query_str):
|
||||||
|
backend = url.get_backend_name()
|
||||||
|
|
||||||
|
new_url = url.set(
|
||||||
|
drivername="%s+%s" % (backend, driver),
|
||||||
|
)
|
||||||
|
if query_str:
|
||||||
|
new_url = new_url.update_query_string(query_str)
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_url.get_dialect()
|
||||||
|
except exc.NoSuchModuleError:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return new_url
|
||||||
|
|
||||||
|
|
||||||
|
def _configs_for_db_operation():
|
||||||
|
hosts = set()
|
||||||
|
|
||||||
|
for cfg in config.Config.all_configs():
|
||||||
|
cfg.db.dispose()
|
||||||
|
|
||||||
|
for cfg in config.Config.all_configs():
|
||||||
|
url = cfg.db.url
|
||||||
|
backend = url.get_backend_name()
|
||||||
|
host_conf = (backend, url.username, url.host, url.database)
|
||||||
|
|
||||||
|
if host_conf not in hosts:
|
||||||
|
yield cfg
|
||||||
|
hosts.add(host_conf)
|
||||||
|
|
||||||
|
for cfg in config.Config.all_configs():
|
||||||
|
cfg.db.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def drop_all_schema_objects_pre_tables(cfg, eng):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def drop_all_schema_objects_post_tables(cfg, eng):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def drop_all_schema_objects(cfg, eng):
|
||||||
|
drop_all_schema_objects_pre_tables(cfg, eng)
|
||||||
|
|
||||||
|
drop_views(cfg, eng)
|
||||||
|
|
||||||
|
if config.requirements.materialized_views.enabled:
|
||||||
|
drop_materialized_views(cfg, eng)
|
||||||
|
|
||||||
|
inspector = inspect(eng)
|
||||||
|
|
||||||
|
consider_schemas = (None,)
|
||||||
|
if config.requirements.schemas.enabled_for_config(cfg):
|
||||||
|
consider_schemas += (cfg.test_schema, cfg.test_schema_2)
|
||||||
|
util.drop_all_tables(eng, inspector, consider_schemas=consider_schemas)
|
||||||
|
|
||||||
|
drop_all_schema_objects_post_tables(cfg, eng)
|
||||||
|
|
||||||
|
if config.requirements.sequences.enabled_for_config(cfg):
|
||||||
|
with eng.begin() as conn:
|
||||||
|
for seq in inspector.get_sequence_names():
|
||||||
|
conn.execute(ddl.DropSequence(schema.Sequence(seq)))
|
||||||
|
if config.requirements.schemas.enabled_for_config(cfg):
|
||||||
|
for schema_name in [cfg.test_schema, cfg.test_schema_2]:
|
||||||
|
for seq in inspector.get_sequence_names(
|
||||||
|
schema=schema_name
|
||||||
|
):
|
||||||
|
conn.execute(
|
||||||
|
ddl.DropSequence(
|
||||||
|
schema.Sequence(seq, schema=schema_name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def drop_views(cfg, eng):
|
||||||
|
inspector = inspect(eng)
|
||||||
|
|
||||||
|
try:
|
||||||
|
view_names = inspector.get_view_names()
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
with eng.begin() as conn:
|
||||||
|
for vname in view_names:
|
||||||
|
conn.execute(
|
||||||
|
ddl._DropView(schema.Table(vname, schema.MetaData()))
|
||||||
|
)
|
||||||
|
|
||||||
|
if config.requirements.schemas.enabled_for_config(cfg):
|
||||||
|
try:
|
||||||
|
view_names = inspector.get_view_names(schema=cfg.test_schema)
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
with eng.begin() as conn:
|
||||||
|
for vname in view_names:
|
||||||
|
conn.execute(
|
||||||
|
ddl._DropView(
|
||||||
|
schema.Table(
|
||||||
|
vname,
|
||||||
|
schema.MetaData(),
|
||||||
|
schema=cfg.test_schema,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def drop_materialized_views(cfg, eng):
|
||||||
|
inspector = inspect(eng)
|
||||||
|
|
||||||
|
mview_names = inspector.get_materialized_view_names()
|
||||||
|
|
||||||
|
with eng.begin() as conn:
|
||||||
|
for vname in mview_names:
|
||||||
|
conn.exec_driver_sql(f"DROP MATERIALIZED VIEW {vname}")
|
||||||
|
|
||||||
|
if config.requirements.schemas.enabled_for_config(cfg):
|
||||||
|
mview_names = inspector.get_materialized_view_names(
|
||||||
|
schema=cfg.test_schema
|
||||||
|
)
|
||||||
|
with eng.begin() as conn:
|
||||||
|
for vname in mview_names:
|
||||||
|
conn.exec_driver_sql(
|
||||||
|
f"DROP MATERIALIZED VIEW {cfg.test_schema}.{vname}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def create_db(cfg, eng, ident):
|
||||||
|
"""Dynamically create a database for testing.
|
||||||
|
|
||||||
|
Used when a test run will employ multiple processes, e.g., when run
|
||||||
|
via `tox` or `pytest -n4`.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"no DB creation routine for cfg: %s" % (eng.url,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def drop_db(cfg, eng, ident):
|
||||||
|
"""Drop a database that we dynamically created for testing."""
|
||||||
|
raise NotImplementedError("no DB drop routine for cfg: %s" % (eng.url,))
|
||||||
|
|
||||||
|
|
||||||
|
def _adapt_update_db_opts(fn):
|
||||||
|
insp = util.inspect_getfullargspec(fn)
|
||||||
|
if len(insp.args) == 3:
|
||||||
|
return fn
|
||||||
|
else:
|
||||||
|
return lambda db_url, db_opts, _options: fn(db_url, db_opts)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init_decorator(_adapt_update_db_opts)
|
||||||
|
def update_db_opts(db_url, db_opts, options):
|
||||||
|
"""Set database options (db_opts) for a test database that we created."""
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def post_configure_engine(url, engine, follower_ident):
|
||||||
|
"""Perform extra steps after configuring the main engine for testing.
|
||||||
|
|
||||||
|
(For the internal dialects, currently only used by sqlite, oracle, mssql)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def post_configure_testing_engine(url, engine, options, scope):
|
||||||
|
"""perform extra steps after configuring any engine within the
|
||||||
|
testing_engine() function.
|
||||||
|
|
||||||
|
this includes the main engine as well as most ad-hoc testing engines.
|
||||||
|
|
||||||
|
steps here should not get in the way of test cases that are looking
|
||||||
|
for events, etc.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def follower_url_from_main(url, ident):
|
||||||
|
"""Create a connection URL for a dynamically-created test database.
|
||||||
|
|
||||||
|
:param url: the connection URL specified when the test run was invoked
|
||||||
|
:param ident: the pytest-xdist "worker identifier" to be used as the
|
||||||
|
database name
|
||||||
|
"""
|
||||||
|
url = sa_url.make_url(url)
|
||||||
|
return url.set(database=ident)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def configure_follower(cfg, ident):
|
||||||
|
"""Create dialect-specific config settings for a follower database."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def run_reap_dbs(url, ident):
|
||||||
|
"""Remove databases that were created during the test process, after the
|
||||||
|
process has ended.
|
||||||
|
|
||||||
|
This is an optional step that is invoked for certain backends that do not
|
||||||
|
reliably release locks on the database as long as a process is still in
|
||||||
|
use. For the internal dialects, this is currently only necessary for
|
||||||
|
mssql and oracle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def reap_dbs(idents_file):
|
||||||
|
log.info("Reaping databases...")
|
||||||
|
|
||||||
|
urls = collections.defaultdict(set)
|
||||||
|
idents = collections.defaultdict(set)
|
||||||
|
dialects = {}
|
||||||
|
|
||||||
|
with open(idents_file) as file_:
|
||||||
|
for line in file_:
|
||||||
|
line = line.strip()
|
||||||
|
db_name, db_url = line.split(" ")
|
||||||
|
url_obj = sa_url.make_url(db_url)
|
||||||
|
if db_name not in dialects:
|
||||||
|
dialects[db_name] = url_obj.get_dialect()
|
||||||
|
dialects[db_name].load_provisioning()
|
||||||
|
url_key = (url_obj.get_backend_name(), url_obj.host)
|
||||||
|
urls[url_key].add(db_url)
|
||||||
|
idents[url_key].add(db_name)
|
||||||
|
|
||||||
|
for url_key in urls:
|
||||||
|
url = list(urls[url_key])[0]
|
||||||
|
ident = idents[url_key]
|
||||||
|
run_reap_dbs(url, ident)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def temp_table_keyword_args(cfg, eng):
|
||||||
|
"""Specify keyword arguments for creating a temporary Table.
|
||||||
|
|
||||||
|
Dialect-specific implementations of this method will return the
|
||||||
|
kwargs that are passed to the Table method when creating a temporary
|
||||||
|
table for testing, e.g., in the define_temp_tables method of the
|
||||||
|
ComponentReflectionTest class in suite/test_reflection.py
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"no temp table keyword args routine for cfg: %s" % (eng.url,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def prepare_for_drop_tables(config, connection):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def stop_test_class_outside_fixtures(config, db, testcls):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def get_temp_table_name(cfg, eng, base_name):
|
||||||
|
"""Specify table name for creating a temporary Table.
|
||||||
|
|
||||||
|
Dialect-specific implementations of this method will return the
|
||||||
|
name to use when creating a temporary table for testing,
|
||||||
|
e.g., in the define_temp_tables method of the
|
||||||
|
ComponentReflectionTest class in suite/test_reflection.py
|
||||||
|
|
||||||
|
Default to just the base name since that's what most dialects will
|
||||||
|
use. The mssql dialect's implementation will need a "#" prepended.
|
||||||
|
"""
|
||||||
|
return base_name
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def set_default_schema_on_connection(cfg, dbapi_connection, schema_name):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"backend does not implement a schema name set function: %s"
|
||||||
|
% (cfg.db.url,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def upsert(
|
||||||
|
cfg,
|
||||||
|
table,
|
||||||
|
returning,
|
||||||
|
*,
|
||||||
|
set_lambda=None,
|
||||||
|
sort_by_parameter_order=False,
|
||||||
|
index_elements=None,
|
||||||
|
):
|
||||||
|
"""return the backends insert..on conflict / on dupe etc. construct.
|
||||||
|
|
||||||
|
while we should add a backend-neutral upsert construct as well, such as
|
||||||
|
insert().upsert(), it's important that we continue to test the
|
||||||
|
backend-specific insert() constructs since if we do implement
|
||||||
|
insert().upsert(), that would be using a different codepath for the things
|
||||||
|
we need to test like insertmanyvalues, etc.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"backend does not include an upsert implementation: {cfg.db.url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def normalize_sequence(cfg, sequence):
|
||||||
|
"""Normalize sequence parameters for dialect that don't start with 1
|
||||||
|
by default.
|
||||||
|
|
||||||
|
The default implementation does nothing
|
||||||
|
"""
|
||||||
|
return sequence
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def allow_stale_update_impl(cfg):
|
||||||
|
return contextlib.nullcontext()
|
||||||
|
|
||||||
|
|
||||||
|
@decorator
|
||||||
|
def allow_stale_updates(fn, *arg, **kw):
|
||||||
|
"""decorator around a test function that indicates the test will
|
||||||
|
be UPDATING rows that have been read and are now stale.
|
||||||
|
|
||||||
|
This normally doesn't require intervention except for mariadb 12
|
||||||
|
which now raises its own error for that, and we want to turn off
|
||||||
|
that setting just within the scope of the test that needs it
|
||||||
|
to be turned off (i.e. ORM stale version tests)
|
||||||
|
|
||||||
|
"""
|
||||||
|
with allow_stale_update_impl(config._current):
|
||||||
|
return fn(*arg, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
@register.init
|
||||||
|
def delete_from_all_tables(connection, cfg, metadata):
|
||||||
|
"""an absolutely foolproof delete from all tables routine.
|
||||||
|
|
||||||
|
dialects should override this to add special instructions like
|
||||||
|
disable constraints etc.
|
||||||
|
|
||||||
|
"""
|
||||||
|
savepoints = getattr(cfg.requirements, "savepoints", False)
|
||||||
|
if savepoints:
|
||||||
|
savepoints = savepoints.enabled
|
||||||
|
|
||||||
|
inspector = inspect(connection)
|
||||||
|
|
||||||
|
for table in reversed(
|
||||||
|
[
|
||||||
|
t
|
||||||
|
for (t, fks) in sort_tables_and_constraints(
|
||||||
|
metadata.tables.values()
|
||||||
|
)
|
||||||
|
if t is not None
|
||||||
|
# remember that inspector.get_table_names() is cached,
|
||||||
|
# so this emits SQL once per unique schema name
|
||||||
|
and t.name in inspector.get_table_names(schema=t.schema)
|
||||||
|
]
|
||||||
|
):
|
||||||
|
if savepoints:
|
||||||
|
with connection.begin_nested():
|
||||||
|
connection.execute(table.delete())
|
||||||
|
else:
|
||||||
|
connection.execute(table.delete())
|
||||||
1945
venv/Lib/site-packages/sqlalchemy/testing/requirements.py
Normal file
1945
venv/Lib/site-packages/sqlalchemy/testing/requirements.py
Normal file
File diff suppressed because it is too large
Load Diff
198
venv/Lib/site-packages/sqlalchemy/testing/schema.py
Normal file
198
venv/Lib/site-packages/sqlalchemy/testing/schema.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# testing/schema.py
|
||||||
|
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||||
|
# <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||||
|
# mypy: ignore-errors
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
from . import exclusions
|
||||||
|
from .. import event
|
||||||
|
from .. import schema
|
||||||
|
from .. import types as sqltypes
|
||||||
|
from ..orm import mapped_column as _orm_mapped_column
|
||||||
|
from ..util import OrderedDict
|
||||||
|
|
||||||
|
__all__ = ["Table", "Column"]
|
||||||
|
|
||||||
|
table_options = {}
|
||||||
|
|
||||||
|
|
||||||
|
def Table(*args, **kw) -> schema.Table:
|
||||||
|
"""A schema.Table wrapper/hook for dialect-specific tweaks."""
|
||||||
|
|
||||||
|
# pop out local options; these are not used at the moment
|
||||||
|
_ = {k: kw.pop(k) for k in list(kw) if k.startswith("test_")}
|
||||||
|
|
||||||
|
kw.update(table_options)
|
||||||
|
|
||||||
|
return schema.Table(*args, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
def mapped_column(*args, **kw):
|
||||||
|
"""An orm.mapped_column wrapper/hook for dialect-specific tweaks."""
|
||||||
|
|
||||||
|
return _schema_column(_orm_mapped_column, args, kw)
|
||||||
|
|
||||||
|
|
||||||
|
def Column(*args, **kw):
|
||||||
|
"""A schema.Column wrapper/hook for dialect-specific tweaks."""
|
||||||
|
|
||||||
|
return _schema_column(schema.Column, args, kw)
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_column(factory, args, kw):
|
||||||
|
test_opts = {k: kw.pop(k) for k in list(kw) if k.startswith("test_")}
|
||||||
|
|
||||||
|
if not config.requirements.foreign_key_ddl.enabled_for_config(config):
|
||||||
|
args = [arg for arg in args if not isinstance(arg, schema.ForeignKey)]
|
||||||
|
|
||||||
|
construct = factory(*args, **kw)
|
||||||
|
|
||||||
|
if factory is schema.Column:
|
||||||
|
col = construct
|
||||||
|
else:
|
||||||
|
col = construct.column
|
||||||
|
|
||||||
|
if test_opts.get("test_needs_autoincrement", False) and kw.get(
|
||||||
|
"primary_key", False
|
||||||
|
):
|
||||||
|
if col.default is None and col.server_default is None:
|
||||||
|
col.autoincrement = True
|
||||||
|
|
||||||
|
# allow any test suite to pick up on this
|
||||||
|
col.info["test_needs_autoincrement"] = True
|
||||||
|
|
||||||
|
# hardcoded rule for oracle; this should
|
||||||
|
# be moved out
|
||||||
|
if exclusions.against(config._current, "oracle"):
|
||||||
|
|
||||||
|
def add_seq(c, tbl):
|
||||||
|
c._init_items(
|
||||||
|
schema.Sequence(
|
||||||
|
_truncate_name(
|
||||||
|
config.db.dialect, tbl.name + "_" + c.name + "_seq"
|
||||||
|
),
|
||||||
|
optional=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
event.listen(col, "after_parent_attach", add_seq, propagate=True)
|
||||||
|
return construct
|
||||||
|
|
||||||
|
|
||||||
|
class eq_type_affinity:
|
||||||
|
"""Helper to compare types inside of datastructures based on affinity.
|
||||||
|
|
||||||
|
E.g.::
|
||||||
|
|
||||||
|
eq_(
|
||||||
|
inspect(connection).get_columns("foo"),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": testing.eq_type_affinity(sqltypes.INTEGER),
|
||||||
|
"nullable": False,
|
||||||
|
"default": None,
|
||||||
|
"autoincrement": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data",
|
||||||
|
"type": testing.eq_type_affinity(sqltypes.NullType),
|
||||||
|
"nullable": True,
|
||||||
|
"default": None,
|
||||||
|
"autoincrement": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
self.target = sqltypes.to_instance(target)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.target._type_affinity is other._type_affinity
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return self.target._type_affinity is not other._type_affinity
|
||||||
|
|
||||||
|
|
||||||
|
class eq_compile_type:
|
||||||
|
"""similar to eq_type_affinity but uses compile"""
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.target == other.compile()
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return self.target != other.compile()
|
||||||
|
|
||||||
|
|
||||||
|
class eq_clause_element:
|
||||||
|
"""Helper to compare SQL structures based on compare()"""
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.target.compare(other)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.target.compare(other)
|
||||||
|
|
||||||
|
|
||||||
|
def _truncate_name(dialect, name):
|
||||||
|
if len(name) > dialect.max_identifier_length:
|
||||||
|
return (
|
||||||
|
name[0 : max(dialect.max_identifier_length - 6, 0)]
|
||||||
|
+ "_"
|
||||||
|
+ hex(hash(name) % 64)[2:]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def pep435_enum(name):
|
||||||
|
# Implements PEP 435 in the minimal fashion needed by SQLAlchemy
|
||||||
|
__members__ = OrderedDict()
|
||||||
|
|
||||||
|
def __init__(self, name, value, alias=None):
|
||||||
|
self.name = name
|
||||||
|
self.value = value
|
||||||
|
self.__members__[name] = self
|
||||||
|
value_to_member[value] = self
|
||||||
|
setattr(self.__class__, name, self)
|
||||||
|
if alias:
|
||||||
|
self.__members__[alias] = self
|
||||||
|
setattr(self.__class__, alias, self)
|
||||||
|
|
||||||
|
value_to_member = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, value):
|
||||||
|
return value_to_member[value]
|
||||||
|
|
||||||
|
someenum = type(
|
||||||
|
name,
|
||||||
|
(object,),
|
||||||
|
{"__members__": __members__, "__init__": __init__, "get": get},
|
||||||
|
)
|
||||||
|
|
||||||
|
# getframe() trick for pickling I don't understand courtesy
|
||||||
|
# Python namedtuple()
|
||||||
|
try:
|
||||||
|
module = sys._getframe(1).f_globals.get("__name__", "__main__")
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
pass
|
||||||
|
if module is not None:
|
||||||
|
someenum.__module__ = module
|
||||||
|
|
||||||
|
return someenum
|
||||||
534
venv/Lib/site-packages/sqlalchemy/testing/util.py
Normal file
534
venv/Lib/site-packages/sqlalchemy/testing/util.py
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
# testing/util.py
|
||||||
|
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||||
|
# <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||||
|
# mypy: ignore-errors
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
from collections import namedtuple
|
||||||
|
import contextlib
|
||||||
|
import decimal
|
||||||
|
import gc
|
||||||
|
from itertools import chain
|
||||||
|
import pickle
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
from sys import getsizeof
|
||||||
|
import time
|
||||||
|
import types
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
from . import mock
|
||||||
|
from .. import inspect
|
||||||
|
from ..engine import Connection
|
||||||
|
from ..schema import Column
|
||||||
|
from ..schema import DropConstraint
|
||||||
|
from ..schema import DropTable
|
||||||
|
from ..schema import ForeignKeyConstraint
|
||||||
|
from ..schema import MetaData
|
||||||
|
from ..schema import Table
|
||||||
|
from ..sql import schema
|
||||||
|
from ..sql.sqltypes import Integer
|
||||||
|
from ..util import decorator
|
||||||
|
from ..util import defaultdict
|
||||||
|
from ..util import has_refcount_gc
|
||||||
|
from ..util import inspect_getfullargspec
|
||||||
|
|
||||||
|
if not has_refcount_gc:
|
||||||
|
|
||||||
|
def non_refcount_gc_collect(*args):
|
||||||
|
gc.collect()
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
gc_collect = lazy_gc = non_refcount_gc_collect
|
||||||
|
else:
|
||||||
|
# assume CPython - straight gc.collect, lazy_gc() is a pass
|
||||||
|
gc_collect = gc.collect
|
||||||
|
|
||||||
|
def lazy_gc():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def picklers():
|
||||||
|
nt = namedtuple("picklers", ["loads", "dumps"])
|
||||||
|
|
||||||
|
for protocol in range(-2, pickle.HIGHEST_PROTOCOL + 1):
|
||||||
|
yield nt(pickle.loads, lambda d: pickle.dumps(d, protocol))
|
||||||
|
|
||||||
|
|
||||||
|
def random_choices(population, k=1):
|
||||||
|
return random.choices(population, k=k)
|
||||||
|
|
||||||
|
|
||||||
|
def round_decimal(value, prec):
|
||||||
|
if isinstance(value, float):
|
||||||
|
return round(value, prec)
|
||||||
|
|
||||||
|
# can also use shift() here but that is 2.6 only
|
||||||
|
return (value * decimal.Decimal("1" + "0" * prec)).to_integral(
|
||||||
|
decimal.ROUND_FLOOR
|
||||||
|
) / pow(10, prec)
|
||||||
|
|
||||||
|
|
||||||
|
class RandomSet(set):
|
||||||
|
def __iter__(self):
|
||||||
|
l = list(set.__iter__(self))
|
||||||
|
random.shuffle(l)
|
||||||
|
return iter(l)
|
||||||
|
|
||||||
|
def pop(self):
|
||||||
|
index = random.randint(0, len(self) - 1)
|
||||||
|
item = list(set.__iter__(self))[index]
|
||||||
|
self.remove(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def union(self, other):
|
||||||
|
return RandomSet(set.union(self, other))
|
||||||
|
|
||||||
|
def difference(self, other):
|
||||||
|
return RandomSet(set.difference(self, other))
|
||||||
|
|
||||||
|
def intersection(self, other):
|
||||||
|
return RandomSet(set.intersection(self, other))
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
return RandomSet(self)
|
||||||
|
|
||||||
|
|
||||||
|
def conforms_partial_ordering(tuples, sorted_elements):
|
||||||
|
"""True if the given sorting conforms to the given partial ordering."""
|
||||||
|
|
||||||
|
deps = defaultdict(set)
|
||||||
|
for parent, child in tuples:
|
||||||
|
deps[parent].add(child)
|
||||||
|
for i, node in enumerate(sorted_elements):
|
||||||
|
for n in sorted_elements[i:]:
|
||||||
|
if node in deps[n]:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def all_partial_orderings(tuples, elements):
|
||||||
|
edges = defaultdict(set)
|
||||||
|
for parent, child in tuples:
|
||||||
|
edges[child].add(parent)
|
||||||
|
|
||||||
|
def _all_orderings(elements):
|
||||||
|
if len(elements) == 1:
|
||||||
|
yield list(elements)
|
||||||
|
else:
|
||||||
|
for elem in elements:
|
||||||
|
subset = set(elements).difference([elem])
|
||||||
|
if not subset.intersection(edges[elem]):
|
||||||
|
for sub_ordering in _all_orderings(subset):
|
||||||
|
yield [elem] + sub_ordering
|
||||||
|
|
||||||
|
return iter(_all_orderings(elements))
|
||||||
|
|
||||||
|
|
||||||
|
def function_named(fn, name):
|
||||||
|
"""Return a function with a given __name__.
|
||||||
|
|
||||||
|
Will assign to __name__ and return the original function if possible on
|
||||||
|
the Python implementation, otherwise a new function will be constructed.
|
||||||
|
|
||||||
|
This function should be phased out as much as possible
|
||||||
|
in favor of @decorator. Tests that "generate" many named tests
|
||||||
|
should be modernized.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
fn.__name__ = name
|
||||||
|
except TypeError:
|
||||||
|
fn = types.FunctionType(
|
||||||
|
fn.__code__, fn.__globals__, name, fn.__defaults__, fn.__closure__
|
||||||
|
)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
def run_as_contextmanager(ctx, fn, *arg, **kw):
|
||||||
|
"""Run the given function under the given contextmanager,
|
||||||
|
simulating the behavior of 'with' to support older
|
||||||
|
Python versions.
|
||||||
|
|
||||||
|
This is not necessary anymore as we have placed 2.6
|
||||||
|
as minimum Python version, however some tests are still using
|
||||||
|
this structure.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
obj = ctx.__enter__()
|
||||||
|
try:
|
||||||
|
result = fn(obj, *arg, **kw)
|
||||||
|
ctx.__exit__(None, None, None)
|
||||||
|
return result
|
||||||
|
except:
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
raise_ = ctx.__exit__(*exc_info)
|
||||||
|
if not raise_:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
return raise_
|
||||||
|
|
||||||
|
|
||||||
|
def rowset(results):
|
||||||
|
"""Converts the results of sql execution into a plain set of column tuples.
|
||||||
|
|
||||||
|
Useful for asserting the results of an unordered query.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {tuple(row) for row in results}
|
||||||
|
|
||||||
|
|
||||||
|
def fail(msg):
|
||||||
|
assert False, msg
|
||||||
|
|
||||||
|
|
||||||
|
@decorator
|
||||||
|
def provide_metadata(fn, *args, **kw):
|
||||||
|
"""Provide bound MetaData for a single test, dropping afterwards.
|
||||||
|
|
||||||
|
Legacy; use the "metadata" pytest fixture.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import fixtures
|
||||||
|
|
||||||
|
metadata = schema.MetaData()
|
||||||
|
self = args[0]
|
||||||
|
prev_meta = getattr(self, "metadata", None)
|
||||||
|
self.metadata = metadata
|
||||||
|
try:
|
||||||
|
return fn(*args, **kw)
|
||||||
|
finally:
|
||||||
|
# close out some things that get in the way of dropping tables.
|
||||||
|
# when using the "metadata" fixture, there is a set ordering
|
||||||
|
# of things that makes sure things are cleaned up in order, however
|
||||||
|
# the simple "decorator" nature of this legacy function means
|
||||||
|
# we have to hardcode some of that cleanup ahead of time.
|
||||||
|
|
||||||
|
# close ORM sessions
|
||||||
|
fixtures.close_all_sessions()
|
||||||
|
|
||||||
|
# integrate with the "connection" fixture as there are many
|
||||||
|
# tests where it is used along with provide_metadata
|
||||||
|
cfc = fixtures.base._connection_fixture_connection
|
||||||
|
if cfc:
|
||||||
|
# TODO: this warning can be used to find all the places
|
||||||
|
# this is used with connection fixture
|
||||||
|
# warn("mixing legacy provide metadata with connection fixture")
|
||||||
|
drop_all_tables_from_metadata(metadata, cfc)
|
||||||
|
# as the provide_metadata fixture is often used with "testing.db",
|
||||||
|
# when we do the drop we have to commit the transaction so that
|
||||||
|
# the DB is actually updated as the CREATE would have been
|
||||||
|
# committed
|
||||||
|
cfc.get_transaction().commit()
|
||||||
|
else:
|
||||||
|
drop_all_tables_from_metadata(metadata, config.db)
|
||||||
|
self.metadata = prev_meta
|
||||||
|
|
||||||
|
|
||||||
|
def flag_combinations(*combinations):
|
||||||
|
"""A facade around @testing.combinations() oriented towards boolean
|
||||||
|
keyword-based arguments.
|
||||||
|
|
||||||
|
Basically generates a nice looking identifier based on the keywords
|
||||||
|
and also sets up the argument names.
|
||||||
|
|
||||||
|
E.g.::
|
||||||
|
|
||||||
|
@testing.flag_combinations(
|
||||||
|
dict(lazy=False, passive=False),
|
||||||
|
dict(lazy=True, passive=False),
|
||||||
|
dict(lazy=False, passive=True),
|
||||||
|
dict(lazy=False, passive=True, raiseload=True),
|
||||||
|
)
|
||||||
|
def test_fn(lazy, passive, raiseload): ...
|
||||||
|
|
||||||
|
would result in::
|
||||||
|
|
||||||
|
@testing.combinations(
|
||||||
|
("", False, False, False),
|
||||||
|
("lazy", True, False, False),
|
||||||
|
("lazy_passive", True, True, False),
|
||||||
|
("lazy_passive", True, True, True),
|
||||||
|
id_="iaaa",
|
||||||
|
argnames="lazy,passive,raiseload",
|
||||||
|
)
|
||||||
|
def test_fn(lazy, passive, raiseload): ...
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
keys = set()
|
||||||
|
|
||||||
|
for d in combinations:
|
||||||
|
keys.update(d)
|
||||||
|
|
||||||
|
keys = sorted(keys)
|
||||||
|
|
||||||
|
return config.combinations(
|
||||||
|
*[
|
||||||
|
("_".join(k for k in keys if d.get(k, False)),)
|
||||||
|
+ tuple(d.get(k, False) for k in keys)
|
||||||
|
for d in combinations
|
||||||
|
],
|
||||||
|
id_="i" + ("a" * len(keys)),
|
||||||
|
argnames=",".join(keys),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def lambda_combinations(lambda_arg_sets, **kw):
|
||||||
|
args = inspect_getfullargspec(lambda_arg_sets)
|
||||||
|
|
||||||
|
arg_sets = lambda_arg_sets(*[mock.Mock() for arg in args[0]])
|
||||||
|
|
||||||
|
def create_fixture(pos):
|
||||||
|
def fixture(**kw):
|
||||||
|
return lambda_arg_sets(**kw)[pos]
|
||||||
|
|
||||||
|
fixture.__name__ = "fixture_%3.3d" % pos
|
||||||
|
return fixture
|
||||||
|
|
||||||
|
return config.combinations(
|
||||||
|
*[(create_fixture(i),) for i in range(len(arg_sets))], **kw
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_lambda(__fn, **kw):
|
||||||
|
"""Given a no-arg lambda and a namespace, return a new lambda that
|
||||||
|
has all the values filled in.
|
||||||
|
|
||||||
|
This is used so that we can have module-level fixtures that
|
||||||
|
refer to instance-level variables using lambdas.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
pos_args = inspect_getfullargspec(__fn)[0]
|
||||||
|
pass_pos_args = {arg: kw.pop(arg) for arg in pos_args}
|
||||||
|
glb = dict(__fn.__globals__)
|
||||||
|
glb.update(kw)
|
||||||
|
new_fn = types.FunctionType(__fn.__code__, glb)
|
||||||
|
return new_fn(**pass_pos_args)
|
||||||
|
|
||||||
|
|
||||||
|
def metadata_fixture(ddl="function"):
|
||||||
|
"""Provide MetaData for a pytest fixture."""
|
||||||
|
|
||||||
|
def decorate(fn):
|
||||||
|
def run_ddl(self):
|
||||||
|
metadata = self.metadata = schema.MetaData()
|
||||||
|
try:
|
||||||
|
result = fn(self, metadata)
|
||||||
|
metadata.create_all(config.db)
|
||||||
|
# TODO:
|
||||||
|
# somehow get a per-function dml erase fixture here
|
||||||
|
yield result
|
||||||
|
finally:
|
||||||
|
metadata.drop_all(config.db)
|
||||||
|
|
||||||
|
return config.fixture(scope=ddl)(run_ddl)
|
||||||
|
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
def force_drop_names(*names):
|
||||||
|
"""Force the given table names to be dropped after test complete,
|
||||||
|
isolating for foreign key cycles
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@decorator
|
||||||
|
def go(fn, *args, **kw):
|
||||||
|
try:
|
||||||
|
return fn(*args, **kw)
|
||||||
|
finally:
|
||||||
|
drop_all_tables(config.db, inspect(config.db), include_names=names)
|
||||||
|
|
||||||
|
return go
|
||||||
|
|
||||||
|
|
||||||
|
class adict(dict):
|
||||||
|
"""Dict keys available as attributes. Shadows."""
|
||||||
|
|
||||||
|
def __getattribute__(self, key):
|
||||||
|
try:
|
||||||
|
return self[key]
|
||||||
|
except KeyError:
|
||||||
|
return dict.__getattribute__(self, key)
|
||||||
|
|
||||||
|
def __call__(self, *keys):
|
||||||
|
return tuple([self[key] for key in keys])
|
||||||
|
|
||||||
|
get_all = __call__
|
||||||
|
|
||||||
|
|
||||||
|
def drop_all_tables_from_metadata(metadata, engine_or_connection):
|
||||||
|
from . import engines
|
||||||
|
|
||||||
|
def go(connection):
|
||||||
|
engines.testing_reaper.prepare_for_drop_tables(connection)
|
||||||
|
|
||||||
|
if not connection.dialect.supports_alter:
|
||||||
|
from . import assertions
|
||||||
|
|
||||||
|
with assertions.expect_warnings(
|
||||||
|
"Can't sort tables", assert_=False
|
||||||
|
):
|
||||||
|
metadata.drop_all(connection)
|
||||||
|
else:
|
||||||
|
metadata.drop_all(connection)
|
||||||
|
|
||||||
|
if not isinstance(engine_or_connection, Connection):
|
||||||
|
with engine_or_connection.begin() as connection:
|
||||||
|
go(connection)
|
||||||
|
else:
|
||||||
|
go(engine_or_connection)
|
||||||
|
|
||||||
|
|
||||||
|
def drop_all_tables(
|
||||||
|
engine,
|
||||||
|
inspector,
|
||||||
|
schema=None,
|
||||||
|
consider_schemas=(None,),
|
||||||
|
include_names=None,
|
||||||
|
):
|
||||||
|
if include_names is not None:
|
||||||
|
include_names = set(include_names)
|
||||||
|
|
||||||
|
if schema is not None:
|
||||||
|
assert consider_schemas == (
|
||||||
|
None,
|
||||||
|
), "consider_schemas and schema are mutually exclusive"
|
||||||
|
consider_schemas = (schema,)
|
||||||
|
|
||||||
|
with engine.begin() as conn:
|
||||||
|
for table_key, fkcs in reversed(
|
||||||
|
inspector.sort_tables_on_foreign_key_dependency(
|
||||||
|
consider_schemas=consider_schemas
|
||||||
|
)
|
||||||
|
):
|
||||||
|
if table_key:
|
||||||
|
if (
|
||||||
|
include_names is not None
|
||||||
|
and table_key[1] not in include_names
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
conn.execute(
|
||||||
|
DropTable(
|
||||||
|
Table(table_key[1], MetaData(), schema=table_key[0])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif fkcs:
|
||||||
|
if not engine.dialect.supports_alter:
|
||||||
|
continue
|
||||||
|
for t_key, fkc in fkcs:
|
||||||
|
if (
|
||||||
|
include_names is not None
|
||||||
|
and t_key[1] not in include_names
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
tb = Table(
|
||||||
|
t_key[1],
|
||||||
|
MetaData(),
|
||||||
|
Column("x", Integer),
|
||||||
|
Column("y", Integer),
|
||||||
|
schema=t_key[0],
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
DropConstraint(
|
||||||
|
ForeignKeyConstraint([tb.c.x], [tb.c.y], name=fkc)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_events(event_cls):
|
||||||
|
@decorator
|
||||||
|
def decorate(fn, *arg, **kw):
|
||||||
|
try:
|
||||||
|
return fn(*arg, **kw)
|
||||||
|
finally:
|
||||||
|
event_cls._clear()
|
||||||
|
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
def total_size(o):
|
||||||
|
"""Returns the approximate memory footprint an object and all of its
|
||||||
|
contents.
|
||||||
|
|
||||||
|
source: https://code.activestate.com/recipes/577504/
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def dict_handler(d):
|
||||||
|
return chain.from_iterable(d.items())
|
||||||
|
|
||||||
|
all_handlers = {
|
||||||
|
tuple: iter,
|
||||||
|
list: iter,
|
||||||
|
deque: iter,
|
||||||
|
dict: dict_handler,
|
||||||
|
set: iter,
|
||||||
|
frozenset: iter,
|
||||||
|
}
|
||||||
|
seen = set() # track which object id's have already been seen
|
||||||
|
default_size = getsizeof(0) # estimate sizeof object without __sizeof__
|
||||||
|
|
||||||
|
def sizeof(o):
|
||||||
|
if id(o) in seen: # do not double count the same object
|
||||||
|
return 0
|
||||||
|
seen.add(id(o))
|
||||||
|
s = getsizeof(o, default_size)
|
||||||
|
|
||||||
|
for typ, handler in all_handlers.items():
|
||||||
|
if isinstance(o, typ):
|
||||||
|
s += sum(map(sizeof, handler(o)))
|
||||||
|
break
|
||||||
|
return s
|
||||||
|
|
||||||
|
return sizeof(o)
|
||||||
|
|
||||||
|
|
||||||
|
def count_cache_key_tuples(tup):
|
||||||
|
"""given a cache key tuple, counts how many instances of actual
|
||||||
|
tuples are found.
|
||||||
|
|
||||||
|
used to alert large jumps in cache key complexity.
|
||||||
|
|
||||||
|
"""
|
||||||
|
stack = [tup]
|
||||||
|
|
||||||
|
sentinel = object()
|
||||||
|
num_elements = 0
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
elem = stack.pop(0)
|
||||||
|
if elem is sentinel:
|
||||||
|
num_elements += 1
|
||||||
|
elif isinstance(elem, tuple):
|
||||||
|
if elem:
|
||||||
|
stack = list(elem) + [sentinel] + stack
|
||||||
|
return num_elements
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def skip_if_timeout(seconds: float, cleanup: Any = None):
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
yield
|
||||||
|
sec = time.time() - now
|
||||||
|
if sec > seconds:
|
||||||
|
try:
|
||||||
|
cleanup()
|
||||||
|
finally:
|
||||||
|
config.skip_test(
|
||||||
|
f"test took too long ({sec:.4f} seconds > {seconds})"
|
||||||
|
)
|
||||||
52
venv/Lib/site-packages/sqlalchemy/testing/warnings.py
Normal file
52
venv/Lib/site-packages/sqlalchemy/testing/warnings.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# testing/warnings.py
|
||||||
|
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||||
|
# <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||||
|
# mypy: ignore-errors
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from . import assertions
|
||||||
|
from .. import exc
|
||||||
|
from .. import exc as sa_exc
|
||||||
|
from ..exc import SATestSuiteWarning
|
||||||
|
from ..util.langhelpers import _warnings_warn
|
||||||
|
|
||||||
|
|
||||||
|
def warn_test_suite(message):
|
||||||
|
_warnings_warn(message, category=SATestSuiteWarning)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_filters():
|
||||||
|
"""hook for setting up warnings filters.
|
||||||
|
|
||||||
|
SQLAlchemy-specific classes must only be here and not in pytest config,
|
||||||
|
as we need to delay importing SQLAlchemy until conftest.py has been
|
||||||
|
processed.
|
||||||
|
|
||||||
|
NOTE: filters on subclasses of DeprecationWarning or
|
||||||
|
PendingDeprecationWarning have no effect if added here, since pytest
|
||||||
|
will add at each test the following filters
|
||||||
|
``always::PendingDeprecationWarning`` and ``always::DeprecationWarning``
|
||||||
|
that will take precedence over any added here.
|
||||||
|
|
||||||
|
"""
|
||||||
|
warnings.filterwarnings("error", category=exc.SAWarning)
|
||||||
|
warnings.filterwarnings("always", category=exc.SATestSuiteWarning)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_warnings(fn, warning_msgs, regex=False):
|
||||||
|
"""Assert that each of the given warnings are emitted by fn.
|
||||||
|
|
||||||
|
Deprecated. Please use assertions.expect_warnings().
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
with assertions._expect_warnings(
|
||||||
|
sa_exc.SAWarning, warning_msgs, regex=regex
|
||||||
|
):
|
||||||
|
return fn()
|
||||||
Reference in New Issue
Block a user