From 98ce470685f41d07ba691e4bfed2c1eb5bf96d6a Mon Sep 17 00:00:00 2001 From: Polina Date: Thu, 2 Jul 2026 20:39:14 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?venv/Lib/site-packages/sqlalchemy/testing=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sqlalchemy/testing/engines.py | 482 ++++++++++++++++++ .../sqlalchemy/testing/entities.py | 117 +++++ .../sqlalchemy/testing/exclusions.py | 476 +++++++++++++++++ .../sqlalchemy/testing/pickleable.py | 155 ++++++ .../sqlalchemy/testing/profiling.py | 328 ++++++++++++ 5 files changed, 1558 insertions(+) create mode 100644 venv/Lib/site-packages/sqlalchemy/testing/engines.py create mode 100644 venv/Lib/site-packages/sqlalchemy/testing/entities.py create mode 100644 venv/Lib/site-packages/sqlalchemy/testing/exclusions.py create mode 100644 venv/Lib/site-packages/sqlalchemy/testing/pickleable.py create mode 100644 venv/Lib/site-packages/sqlalchemy/testing/profiling.py diff --git a/venv/Lib/site-packages/sqlalchemy/testing/engines.py b/venv/Lib/site-packages/sqlalchemy/testing/engines.py new file mode 100644 index 0000000..4327f76 --- /dev/null +++ b/venv/Lib/site-packages/sqlalchemy/testing/engines.py @@ -0,0 +1,482 @@ +# testing/engines.py +# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors +# +# +# 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 re +import typing +from typing import Any +from typing import Dict +from typing import Optional +from typing import Union +import warnings +import weakref + +from . import config +from .util import decorator +from .util import gc_collect +from .. import event +from .. import pool +from ..util import await_only +from ..util.typing import Literal + +if typing.TYPE_CHECKING: + from ..engine import Engine + from ..engine.url import URL + from ..ext.asyncio import AsyncEngine + + +class ConnectionKiller: + def __init__(self): + self.proxy_refs = weakref.WeakKeyDictionary() + self.testing_engines = collections.defaultdict(set) + self.dbapi_connections = set() + + def add_pool(self, pool): + event.listen(pool, "checkout", self._add_conn) + event.listen(pool, "checkin", self._remove_conn) + event.listen(pool, "close", self._remove_conn) + event.listen(pool, "close_detached", self._remove_conn) + # note we are keeping "invalidated" here, as those are still + # opened connections we would like to roll back + + def _add_conn(self, dbapi_con, con_record, con_proxy): + self.dbapi_connections.add(dbapi_con) + self.proxy_refs[con_proxy] = True + + def _remove_conn(self, dbapi_conn, *arg): + self.dbapi_connections.discard(dbapi_conn) + + def add_engine(self, engine, scope): + self.add_pool(engine.pool) + + assert scope in ("class", "global", "function", "fixture") + self.testing_engines[scope].add(engine) + + def _safe(self, fn): + try: + fn() + except Exception as e: + warnings.warn( + "testing_reaper couldn't rollback/close connection: %s" % e + ) + + def rollback_all(self): + for rec in list(self.proxy_refs): + if rec is not None and rec.is_valid: + self._safe(rec.rollback) + + def checkin_all(self): + # run pool.checkin() for all ConnectionFairy instances we have + # tracked. + + for rec in list(self.proxy_refs): + if rec is not None and rec.is_valid: + self.dbapi_connections.discard(rec.dbapi_connection) + self._safe(rec._checkin) + + # for fairy refs that were GCed and could not close the connection, + # such as asyncio, roll back those remaining connections + for con in self.dbapi_connections: + self._safe(con.rollback) + self.dbapi_connections.clear() + + def close_all(self): + self.checkin_all() + + def prepare_for_drop_tables(self, connection): + # don't do aggressive checks for third party test suites + if not config.bootstrapped_as_sqlalchemy: + return + + from . import provision + + provision.prepare_for_drop_tables(connection.engine.url, connection) + + def _drop_testing_engines(self, scope): + eng = self.testing_engines[scope] + for rec in list(eng): + for proxy_ref in list(self.proxy_refs): + if proxy_ref is not None and proxy_ref.is_valid: + if ( + proxy_ref._pool is not None + and proxy_ref._pool is rec.pool + ): + self._safe(proxy_ref._checkin) + + if hasattr(rec, "sync_engine"): + await_only(rec.dispose()) + else: + rec.dispose() + + eng.clear() + + def _dispose_testing_engines(self, scope): + eng = self.testing_engines[scope] + for rec in list(eng): + if hasattr(rec, "sync_engine"): + await_only(rec.dispose()) + else: + rec.dispose() + + def after_test(self): + self._drop_testing_engines("function") + + def after_test_outside_fixtures(self, test): + # don't do aggressive checks for third party test suites + if not config.bootstrapped_as_sqlalchemy: + return + + if test.__class__.__leave_connections_for_teardown__: + return + + self.checkin_all() + + # on PostgreSQL, this will test for any "idle in transaction" + # connections. useful to identify tests with unusual patterns + # that can't be cleaned up correctly. + from . import provision + + with config.db.connect() as conn: + provision.prepare_for_drop_tables(conn.engine.url, conn) + + def stop_test_class_inside_fixtures(self): + self.checkin_all() + self._drop_testing_engines("function") + self._drop_testing_engines("class") + + def stop_test_class_outside_fixtures(self): + # ensure no refs to checked out connections at all. + + if pool.base._strong_ref_connection_records: + gc_collect() + + if pool.base._strong_ref_connection_records: + ln = len(pool.base._strong_ref_connection_records) + pool.base._strong_ref_connection_records.clear() + + if ln > 2: + # allow two connections to linger, as on loaded down + # CI hardware there seem to be occasional GC lapses that + # are not easily preventable + assert ( + False + ), "%d connection recs not cleared after test suite" % (ln) + if config.options and config.options.low_connections: + # for suites running with --low-connections, dispose the "global" + # engines to disconnect everything before making a testing engine + self._dispose_testing_engines("global") + + def final_cleanup(self): + self.checkin_all() + for scope in self.testing_engines: + self._drop_testing_engines(scope) + + def assert_all_closed(self): + for rec in self.proxy_refs: + if rec.is_valid: + assert False + + +testing_reaper = ConnectionKiller() + + +@decorator +def assert_conns_closed(fn, *args, **kw): + try: + fn(*args, **kw) + finally: + testing_reaper.assert_all_closed() + + +@decorator +def rollback_open_connections(fn, *args, **kw): + """Decorator that rolls back all open connections after fn execution.""" + + try: + fn(*args, **kw) + finally: + testing_reaper.rollback_all() + + +@decorator +def close_first(fn, *args, **kw): + """Decorator that closes all connections before fn execution.""" + + testing_reaper.checkin_all() + fn(*args, **kw) + + +@decorator +def close_open_connections(fn, *args, **kw): + """Decorator that closes all connections after fn execution.""" + try: + fn(*args, **kw) + finally: + testing_reaper.checkin_all() + + +def all_dialects(exclude=None): + import sqlalchemy.dialects as d + + for name in d.__all__: + # TEMPORARY + if exclude and name in exclude: + continue + mod = getattr(d, name, None) + if not mod: + mod = getattr( + __import__("sqlalchemy.dialects.%s" % name).dialects, name + ) + yield mod.dialect() + + +class ReconnectFixture: + def __init__(self, dbapi): + self.dbapi = dbapi + self.connections = [] + self.is_stopped = False + + def __getattr__(self, key): + return getattr(self.dbapi, key) + + def connect(self, *args, **kwargs): + conn = self.dbapi.connect(*args, **kwargs) + if self.is_stopped: + self._safe(conn.close) + curs = conn.cursor() # should fail on Oracle etc. + # should fail for everything that didn't fail + # above, connection is closed + curs.execute("select 1") + assert False, "simulated connect failure didn't work" + else: + self.connections.append(conn) + return conn + + def _safe(self, fn): + try: + fn() + except Exception as e: + warnings.warn("ReconnectFixture couldn't close connection: %s" % e) + + def shutdown(self, stop=False): + # TODO: this doesn't cover all cases + # as nicely as we'd like, namely MySQLdb. + # would need to implement R. Brewer's + # proxy server idea to get better + # coverage. + self.is_stopped = stop + for c in list(self.connections): + self._safe(c.close) + self.connections = [] + + def restart(self): + self.is_stopped = False + + +def reconnecting_engine(url=None, options=None): + url = url or config.db.url + dbapi = config.db.dialect.dbapi + if not options: + options = {} + options["module"] = ReconnectFixture(dbapi) + engine = testing_engine(url, options) + _dispose = engine.dispose + + def dispose(): + engine.dialect.dbapi.shutdown() + engine.dialect.dbapi.is_stopped = False + _dispose() + + engine.test_shutdown = engine.dialect.dbapi.shutdown + engine.test_restart = engine.dialect.dbapi.restart + engine.dispose = dispose + return engine + + +@typing.overload +def testing_engine( + url: Optional[URL] = ..., + options: Optional[Dict[str, Any]] = ..., + *, + asyncio: Literal[False], +) -> Engine: ... + + +@typing.overload +def testing_engine( + url: Optional[URL] = ..., + options: Optional[Dict[str, Any]] = ..., + *, + asyncio: Literal[True], +) -> AsyncEngine: ... + + +def testing_engine( + url: Optional[URL] = None, + options: Optional[Dict[str, Any]] = None, + *, + asyncio: bool = False, +) -> Union[Engine, AsyncEngine]: + + if asyncio: + from sqlalchemy.ext.asyncio import ( + create_async_engine as create_engine, + ) + else: + from sqlalchemy import create_engine + from sqlalchemy.engine.url import make_url + + url = make_url(url if url else config.db.url) + + if not options: + options = {} + + use_options = {} + + for opt_dict in (config.db_opts, options): + if not opt_dict: + continue + use_options.update( + { + opt: value + for opt, value in opt_dict.items() + if opt not in ("scope", "use_reaper") + and not opt.startswith("sqlite_") + } + ) + + engine = create_engine(url, **use_options) + + if config.options and config.options.low_connections: + # for suites running with --low-connections, dispose the "global" + # engines to disconnect everything before making a testing engine + testing_reaper._dispose_testing_engines("global") + + scope = options.get("scope", "function") + if scope == "global": + if asyncio: + engine.sync_engine._has_events = True + else: + engine._has_events = ( + True # enable event blocks, helps with profiling + ) + + from . import provision + + provision.post_configure_testing_engine(engine.url, engine, options, scope) + + # post_configure_testing_engine may have modified the options dictionary + # in place; consume additional post arguments afterwards + + use_reaper = options.get("use_reaper", True) + if use_reaper: + testing_reaper.add_engine(engine, scope) + + if ( + isinstance(engine.pool, pool.QueuePool) + and "pool" not in options + and "pool_timeout" not in options + and "max_overflow" not in options + ): + engine.pool._timeout = 0 + engine.pool._max_overflow = 0 + + return engine + + +def mock_engine(dialect_name=None): + """Provides a mocking engine based on the current testing.db. + + This is normally used to test DDL generation flow as emitted + by an Engine. + + It should not be used in other cases, as assert_compile() and + assert_sql_execution() are much better choices with fewer + moving parts. + + """ + + from sqlalchemy import create_mock_engine + + if not dialect_name: + dialect_name = config.db.name + + buffer = [] + + def executor(sql, *a, **kw): + buffer.append(sql) + + def assert_sql(stmts): + recv = [re.sub(r"[\n\t]", "", str(s)) for s in buffer] + assert recv == stmts, recv + + def print_sql(): + d = engine.dialect + return "\n".join(str(s.compile(dialect=d)) for s in engine.mock) + + engine = create_mock_engine(dialect_name + "://", executor) + assert not hasattr(engine, "mock") + engine.mock = buffer + engine.assert_sql = assert_sql + engine.print_sql = print_sql + return engine + + +class DBAPIProxyCursor: + """Proxy a DBAPI cursor. + + Tests can provide subclasses of this to intercept + DBAPI-level cursor operations. + + """ + + def __init__(self, engine, conn, *args, **kwargs): + self.engine = engine + self.connection = conn + self.cursor = conn.cursor(*args, **kwargs) + + def execute(self, stmt, parameters=None, **kw): + if parameters: + return self.cursor.execute(stmt, parameters, **kw) + else: + return self.cursor.execute(stmt, **kw) + + def executemany(self, stmt, params, **kw): + return self.cursor.executemany(stmt, params, **kw) + + def __iter__(self): + return iter(self.cursor) + + def __getattr__(self, key): + return getattr(self.cursor, key) + + +class DBAPIProxyConnection: + """Proxy a DBAPI connection. + + Tests can provide subclasses of this to intercept + DBAPI-level connection operations. + + """ + + def __init__(self, engine, conn, cursor_cls): + self.conn = conn + self.engine = engine + self.cursor_cls = cursor_cls + + def cursor(self, *args, **kwargs): + return self.cursor_cls(self.engine, self.conn, *args, **kwargs) + + def close(self): + self.conn.close() + + def __getattr__(self, key): + return getattr(self.conn, key) diff --git a/venv/Lib/site-packages/sqlalchemy/testing/entities.py b/venv/Lib/site-packages/sqlalchemy/testing/entities.py new file mode 100644 index 0000000..a2d374b --- /dev/null +++ b/venv/Lib/site-packages/sqlalchemy/testing/entities.py @@ -0,0 +1,117 @@ +# testing/entities.py +# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors +# +# +# 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 sqlalchemy as sa +from .. import exc as sa_exc +from ..orm.writeonly import WriteOnlyCollection + +_repr_stack = set() + + +class BasicEntity: + def __init__(self, **kw): + for key, value in kw.items(): + setattr(self, key, value) + + def __repr__(self): + if id(self) in _repr_stack: + return object.__repr__(self) + _repr_stack.add(id(self)) + try: + return "%s(%s)" % ( + (self.__class__.__name__), + ", ".join( + [ + "%s=%r" % (key, getattr(self, key)) + for key in sorted(self.__dict__.keys()) + if not key.startswith("_") + ] + ), + ) + finally: + _repr_stack.remove(id(self)) + + +_recursion_stack = set() + + +class ComparableMixin: + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + """'Deep, sparse compare. + + Deeply compare two entities, following the non-None attributes of the + non-persisted object, if possible. + + """ + if other is self: + return True + elif not self.__class__ == other.__class__: + return False + + if id(self) in _recursion_stack: + return True + _recursion_stack.add(id(self)) + + try: + # pick the entity that's not SA persisted as the source + try: + self_key = sa.orm.attributes.instance_state(self).key + except sa.orm.exc.NO_STATE: + self_key = None + + if other is None: + a = self + b = other + elif self_key is not None: + a = other + b = self + else: + a = self + b = other + + for attr in list(a.__dict__): + if attr.startswith("_"): + continue + + value = getattr(a, attr) + + if isinstance(value, WriteOnlyCollection): + continue + + try: + # handle lazy loader errors + battr = getattr(b, attr) + except (AttributeError, sa_exc.UnboundExecutionError): + return False + + if hasattr(value, "__iter__") and not isinstance(value, str): + if hasattr(value, "__getitem__") and not hasattr( + value, "keys" + ): + if list(value) != list(battr): + return False + else: + if set(value) != set(battr): + return False + else: + if value is not None and value != battr: + return False + return True + finally: + _recursion_stack.remove(id(self)) + + +class ComparableEntity(ComparableMixin, BasicEntity): + def __hash__(self): + return hash(self.__class__) diff --git a/venv/Lib/site-packages/sqlalchemy/testing/exclusions.py b/venv/Lib/site-packages/sqlalchemy/testing/exclusions.py new file mode 100644 index 0000000..9280e18 --- /dev/null +++ b/venv/Lib/site-packages/sqlalchemy/testing/exclusions.py @@ -0,0 +1,476 @@ +# testing/exclusions.py +# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php +# mypy: ignore-errors + +import contextlib +import operator +import re +import sys + +from . import config +from .. import util +from ..util import decorator +from ..util.compat import inspect_getfullargspec + + +def skip_if(predicate, reason=None): + rule = compound() + pred = _as_predicate(predicate, reason) + rule.skips.add(pred) + return rule + + +def fails_if(predicate, reason=None): + rule = compound() + pred = _as_predicate(predicate, reason) + rule.fails.add(pred) + return rule + + +def warns_if(predicate, expression, assert_): + rule = compound() + pred = _as_predicate(predicate) + rule.warns[pred] = (expression, assert_) + return rule + + +class compound: + def __init__(self): + self.fails = set() + self.skips = set() + self.warns = {} + + def __add__(self, other): + return self.add(other) + + def as_skips(self): + rule = compound() + rule.skips.update(self.skips) + rule.skips.update(self.fails) + return rule + + def add(self, *others): + copy = compound() + copy.fails.update(self.fails) + copy.skips.update(self.skips) + copy.warns.update(self.warns) + + for other in others: + copy.fails.update(other.fails) + copy.skips.update(other.skips) + copy.warns.update(other.warns) + return copy + + def not_(self): + copy = compound() + copy.fails.update(NotPredicate(fail) for fail in self.fails) + copy.skips.update(NotPredicate(skip) for skip in self.skips) + copy.warns.update( + { + NotPredicate(warn): element + for warn, element in self.warns.items() + } + ) + return copy + + @property + def enabled(self): + return self.enabled_for_config(config._current) + + def enabled_for_config(self, config): + for predicate in self.skips.union(self.fails): + if predicate(config): + return False + else: + return True + + def matching_warnings(self, config): + return [ + message + for predicate, (message, assert_) in self.warns.items() + if predicate(config) + ] + + def matching_config_reasons(self, config): + return [ + predicate._as_string(config) + for predicate in self.skips.union(self.fails) + if predicate(config) + ] + + def _extend(self, other): + self.skips.update(other.skips) + self.fails.update(other.fails) + self.warns.update(other.warns) + + def __call__(self, fn): + if hasattr(fn, "_sa_exclusion_extend"): + fn._sa_exclusion_extend._extend(self) + return fn + + @decorator + def decorate(fn, *args, **kw): + return self._do(config._current, fn, *args, **kw) + + decorated = decorate(fn) + decorated._sa_exclusion_extend = self + return decorated + + @contextlib.contextmanager + def fail_if(self): + all_fails = compound() + all_fails.fails.update(self.skips.union(self.fails)) + + try: + yield + except Exception as ex: + all_fails._expect_failure(config._current, ex) + else: + all_fails._expect_success(config._current) + + def _do(self, cfg, fn, *args, **kw): + for skip in self.skips: + if skip(cfg): + msg = "'%s' : %s" % ( + config.get_current_test_name(), + skip._as_string(cfg), + ) + config.skip_test(msg) + + if self.warns: + from .assertions import expect_warnings + + @contextlib.contextmanager + def _expect_warnings(): + with contextlib.ExitStack() as stack: + for expression, assert_ in self.warns.values(): + stack.enter_context( + expect_warnings(expression, assert_=assert_) + ) + yield + + ctx = _expect_warnings() + else: + ctx = contextlib.nullcontext() + + try: + with ctx: + return_value = fn(*args, **kw) + except Exception as ex: + self._expect_failure(cfg, ex, name=fn.__name__) + else: + self._expect_success(cfg, name=fn.__name__) + return return_value + + def _expect_failure(self, config, ex, name="block"): + for fail in self.fails: + if fail(config): + print( + "%s failed as expected (%s): %s " + % (name, fail._as_string(config), ex) + ) + break + else: + raise ex.with_traceback(sys.exc_info()[2]) + + def _expect_success(self, config, name="block"): + if not self.fails: + return + + for fail in self.fails: + if fail(config): + raise AssertionError( + "Unexpected success for '%s' (%s)" + % ( + name, + " and ".join( + fail._as_string(config) for fail in self.fails + ), + ) + ) + + +def only_if(predicate, reason=None): + predicate = _as_predicate(predicate) + return skip_if(NotPredicate(predicate), reason) + + +def succeeds_if(predicate, reason=None): + predicate = _as_predicate(predicate) + return fails_if(NotPredicate(predicate), reason) + + +class Predicate: + @classmethod + def as_predicate(cls, predicate, description=None): + if isinstance(predicate, compound): + return cls.as_predicate(predicate.enabled_for_config, description) + elif isinstance(predicate, Predicate): + if description and predicate.description is None: + predicate.description = description + return predicate + elif isinstance(predicate, (list, set)): + return OrPredicate( + [cls.as_predicate(pred) for pred in predicate], description + ) + elif isinstance(predicate, tuple): + return SpecPredicate(*predicate) + elif isinstance(predicate, str): + tokens = re.match( + r"([\+\w]+)\s*(?:(>=|==|!=|<=|<|>)\s*([\d\.]+))?", predicate + ) + if not tokens: + raise ValueError( + "Couldn't locate DB name in predicate: %r" % predicate + ) + db = tokens.group(1) + op = tokens.group(2) + spec = ( + tuple(int(d) for d in tokens.group(3).split(".")) + if tokens.group(3) + else None + ) + + return SpecPredicate(db, op, spec, description=description) + elif callable(predicate): + return LambdaPredicate(predicate, description) + else: + assert False, "unknown predicate type: %s" % predicate + + def _format_description(self, config, negate=False): + bool_ = self(config) + if negate: + bool_ = not negate + return self.description % { + "driver": ( + config.db.url.get_driver_name() if config else "" + ), + "database": ( + config.db.url.get_backend_name() if config else "" + ), + "doesnt_support": "doesn't support" if bool_ else "does support", + "does_support": "does support" if bool_ else "doesn't support", + } + + def _as_string(self, config=None, negate=False): + raise NotImplementedError() + + +class BooleanPredicate(Predicate): + def __init__(self, value, description=None): + self.value = value + self.description = description or "boolean %s" % value + + def __call__(self, config): + return self.value + + def _as_string(self, config, negate=False): + return self._format_description(config, negate=negate) + + +class SpecPredicate(Predicate): + def __init__(self, db, op=None, spec=None, description=None): + self.db = db + self.op = op + self.spec = spec + self.description = description + + _ops = { + "<": operator.lt, + ">": operator.gt, + "==": operator.eq, + "!=": operator.ne, + "<=": operator.le, + ">=": operator.ge, + "in": operator.contains, + "between": lambda val, pair: val >= pair[0] and val <= pair[1], + } + + def __call__(self, config): + if config is None: + return False + + engine = config.db + + if "+" in self.db: + dialect, driver = self.db.split("+") + else: + dialect, driver = self.db, None + + if dialect and engine.name != dialect: + return False + if driver is not None and engine.driver != driver: + return False + + if self.op is not None: + assert driver is None, "DBAPI version specs not supported yet" + + version = _server_version(engine) + oper = ( + hasattr(self.op, "__call__") and self.op or self._ops[self.op] + ) + return oper(version, self.spec) + else: + return True + + def _as_string(self, config, negate=False): + if self.description is not None: + return self._format_description(config) + elif self.op is None: + if negate: + return "not %s" % self.db + else: + return "%s" % self.db + else: + if negate: + return "not %s %s %s" % (self.db, self.op, self.spec) + else: + return "%s %s %s" % (self.db, self.op, self.spec) + + +class LambdaPredicate(Predicate): + def __init__(self, lambda_, description=None, args=None, kw=None): + spec = inspect_getfullargspec(lambda_) + if not spec[0]: + self.lambda_ = lambda db: lambda_() + else: + self.lambda_ = lambda_ + self.args = args or () + self.kw = kw or {} + if description: + self.description = description + elif lambda_.__doc__: + self.description = lambda_.__doc__ + else: + self.description = "custom function" + + def __call__(self, config): + return self.lambda_(config) + + def _as_string(self, config, negate=False): + return self._format_description(config) + + +class NotPredicate(Predicate): + def __init__(self, predicate, description=None): + self.predicate = predicate + self.description = description + + def __call__(self, config): + return not self.predicate(config) + + def _as_string(self, config, negate=False): + if self.description: + return self._format_description(config, not negate) + else: + return self.predicate._as_string(config, not negate) + + +class OrPredicate(Predicate): + def __init__(self, predicates, description=None): + self.predicates = predicates + self.description = description + + def __call__(self, config): + for pred in self.predicates: + if pred(config): + return True + return False + + def _eval_str(self, config, negate=False): + if negate: + conjunction = " and " + else: + conjunction = " or " + return conjunction.join( + p._as_string(config, negate=negate) for p in self.predicates + ) + + def _negation_str(self, config): + if self.description is not None: + return "Not " + self._format_description(config) + else: + return self._eval_str(config, negate=True) + + def _as_string(self, config, negate=False): + if negate: + return self._negation_str(config) + else: + if self.description is not None: + return self._format_description(config) + else: + return self._eval_str(config) + + +_as_predicate = Predicate.as_predicate + + +def _is_excluded(db, op, spec): + return SpecPredicate(db, op, spec)(config._current) + + +def _server_version(engine): + """Return a server_version_info tuple.""" + + # force metadata to be retrieved + conn = engine.connect() + version = getattr(engine.dialect, "server_version_info", None) + if version is None: + version = () + conn.close() + return version + + +def db_spec(*dbs): + return OrPredicate([Predicate.as_predicate(db) for db in dbs]) + + +def open(): # noqa + return skip_if(BooleanPredicate(False, "mark as execute")) + + +def closed(reason="marked as skip"): + return skip_if(BooleanPredicate(True, reason)) + + +def fails(reason=None): + return fails_if(BooleanPredicate(True, reason or "expected to fail")) + + +def future(): + return fails_if(BooleanPredicate(True, "Future feature")) + + +def fails_on(db, reason=None): + return fails_if(db, reason) + + +def fails_on_everything_except(*dbs): + return succeeds_if(OrPredicate([Predicate.as_predicate(db) for db in dbs])) + + +def skip(db, reason=None): + return skip_if(db, reason) + + +def only_on(dbs, reason=None): + return only_if( + OrPredicate( + [Predicate.as_predicate(db, reason) for db in util.to_list(dbs)] + ) + ) + + +def exclude(db, op, spec, reason=None): + return skip_if(SpecPredicate(db, op, spec), reason) + + +def against(config, *queries): + assert queries, "no queries sent!" + return OrPredicate([Predicate.as_predicate(query) for query in queries])( + config + ) diff --git a/venv/Lib/site-packages/sqlalchemy/testing/pickleable.py b/venv/Lib/site-packages/sqlalchemy/testing/pickleable.py new file mode 100644 index 0000000..94d808c --- /dev/null +++ b/venv/Lib/site-packages/sqlalchemy/testing/pickleable.py @@ -0,0 +1,155 @@ +# testing/pickleable.py +# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php +# mypy: ignore-errors + + +"""Classes used in pickling tests, need to be at the module level for +unpickling. +""" + +from __future__ import annotations + +from .entities import ComparableEntity +from ..schema import Column +from ..types import String + + +class User(ComparableEntity): + pass + + +class Order(ComparableEntity): + pass + + +class Dingaling(ComparableEntity): + pass + + +class EmailUser(User): + pass + + +class Address(ComparableEntity): + pass + + +# TODO: these are kind of arbitrary.... +class Child1(ComparableEntity): + pass + + +class Child2(ComparableEntity): + pass + + +class Parent(ComparableEntity): + pass + + +class Screen: + def __init__(self, obj, parent=None): + self.obj = obj + self.parent = parent + + +class Mixin: + email_address = Column(String) + + +class AddressWMixin(Mixin, ComparableEntity): + pass + + +class Foo: + def __init__(self, moredata, stuff="im stuff"): + self.data = "im data" + self.stuff = stuff + self.moredata = moredata + + __hash__ = object.__hash__ + + def __eq__(self, other): + return ( + other.data == self.data + and other.stuff == self.stuff + and other.moredata == self.moredata + ) + + +class Bar: + def __init__(self, x, y): + self.x = x + self.y = y + + __hash__ = object.__hash__ + + def __eq__(self, other): + return ( + other.__class__ is self.__class__ + and other.x == self.x + and other.y == self.y + ) + + def __str__(self): + return "Bar(%d, %d)" % (self.x, self.y) + + +class OldSchool: + def __init__(self, x, y): + self.x = x + self.y = y + + def __eq__(self, other): + return ( + other.__class__ is self.__class__ + and other.x == self.x + and other.y == self.y + ) + + +class OldSchoolWithoutCompare: + def __init__(self, x, y): + self.x = x + self.y = y + + +class BarWithoutCompare: + def __init__(self, x, y): + self.x = x + self.y = y + + def __str__(self): + return "Bar(%d, %d)" % (self.x, self.y) + + +class NotComparable: + def __init__(self, data): + self.data = data + + def __hash__(self): + return id(self) + + def __eq__(self, other): + return NotImplemented + + def __ne__(self, other): + return NotImplemented + + +class BrokenComparable: + def __init__(self, data): + self.data = data + + def __hash__(self): + return id(self) + + def __eq__(self, other): + raise NotImplementedError + + def __ne__(self, other): + raise NotImplementedError diff --git a/venv/Lib/site-packages/sqlalchemy/testing/profiling.py b/venv/Lib/site-packages/sqlalchemy/testing/profiling.py new file mode 100644 index 0000000..2f0f931 --- /dev/null +++ b/venv/Lib/site-packages/sqlalchemy/testing/profiling.py @@ -0,0 +1,328 @@ +# testing/profiling.py +# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php +# mypy: ignore-errors + + +"""Profiling support for unit and performance tests. + +These are special purpose profiling methods which operate +in a more fine-grained way than nose's profiling plugin. + +""" + +from __future__ import annotations + +import collections +import contextlib +import os +import platform +import pstats +import re +import sys + +from . import config +from .util import gc_collect +from ..util import freethreading +from ..util import has_compiled_ext + +try: + import cProfile +except ImportError: + cProfile = None + +_profile_stats = None +"""global ProfileStatsFileInstance. + +plugin_base assigns this at the start of all tests. + +""" + + +_current_test = None +"""String id of current test. + +plugin_base assigns this at the start of each test using +_start_current_test. + +""" + + +def _start_current_test(id_): + global _current_test + _current_test = id_ + + if _profile_stats.force_write: + _profile_stats.reset_count() + + +class ProfileStatsFile: + """Store per-platform/fn profiling results in a file. + + There was no json module available when this was written, but now + the file format which is very deterministically line oriented is kind of + handy in any case for diffs and merges. + + """ + + def __init__(self, filename, sort="cumulative", dump=None): + self.force_write = ( + config.options is not None and config.options.force_write_profiles + ) + self.write = self.force_write or ( + config.options is not None and config.options.write_profiles + ) + self.fname = os.path.abspath(filename) + self.short_fname = os.path.split(self.fname)[-1] + self.data = collections.defaultdict( + lambda: collections.defaultdict(dict) + ) + self.dump = dump + self.sort = sort + self._read() + if self.write: + # rewrite for the case where features changed, + # etc. + self._write() + + @property + def platform_key(self): + dbapi_key = config.db.name + "_" + config.db.driver + if config.db.dialect.is_async: + dbapi_key += "_async" + + if config.db.name == "sqlite" and config.db.dialect._is_url_file_db( + config.db.url + ): + dbapi_key += "_file" + + # keep it at 2.7, 3.1, 3.2, etc. for now. + py_version = ".".join([str(v) for v in sys.version_info[0:2]]) + if freethreading: + py_version += "t" + + platform_tokens = [ + platform.machine(), + platform.system().lower(), + platform.python_implementation().lower(), + py_version, + dbapi_key, + ] + + platform_tokens.append("dbapiunicode") + _has_cext = has_compiled_ext() + platform_tokens.append(_has_cext and "cextensions" or "nocextensions") + return "_".join(platform_tokens) + + def has_stats(self): + test_key = _current_test + return ( + test_key in self.data and self.platform_key in self.data[test_key] + ) + + def result(self, callcount): + test_key = _current_test + per_fn = self.data[test_key] + per_platform = per_fn[self.platform_key] + + if "counts" not in per_platform: + per_platform["counts"] = counts = [] + else: + counts = per_platform["counts"] + + if "current_count" not in per_platform: + per_platform["current_count"] = current_count = 0 + else: + current_count = per_platform["current_count"] + + has_count = len(counts) > current_count + + if not has_count: + counts.append(callcount) + if self.write: + self._write() + result = None + else: + result = per_platform["lineno"], counts[current_count] + per_platform["current_count"] += 1 + return result + + def reset_count(self): + test_key = _current_test + # since self.data is a defaultdict, don't access a key + # if we don't know it's there first. + if test_key not in self.data: + return + per_fn = self.data[test_key] + if self.platform_key not in per_fn: + return + per_platform = per_fn[self.platform_key] + if "counts" in per_platform: + per_platform["counts"][:] = [] + + def replace(self, callcount): + test_key = _current_test + per_fn = self.data[test_key] + per_platform = per_fn[self.platform_key] + counts = per_platform["counts"] + current_count = per_platform["current_count"] + if current_count < len(counts): + counts[current_count - 1] = callcount + else: + counts[-1] = callcount + if self.write: + self._write() + + def _header(self): + return ( + "# %s\n" + "# This file is written out on a per-environment basis.\n" + "# For each test in aaa_profiling, the corresponding " + "function and \n" + "# environment is located within this file. " + "If it doesn't exist,\n" + "# the test is skipped.\n" + "# If a callcount does exist, it is compared " + "to what we received. \n" + "# assertions are raised if the counts do not match.\n" + "# \n" + "# To add a new callcount test, apply the function_call_count \n" + "# decorator and re-run the tests using the --write-profiles \n" + "# option - this file will be rewritten including the new count.\n" + "# \n" + ) % (self.fname) + + def _read(self): + try: + profile_f = open(self.fname) + except OSError: + return + for lineno, line in enumerate(profile_f): + line = line.strip() + if not line or line.startswith("#"): + continue + + test_key, platform_key, counts = line.split() + per_fn = self.data[test_key] + per_platform = per_fn[platform_key] + c = [int(count) for count in counts.split(",")] + per_platform["counts"] = c + per_platform["lineno"] = lineno + 1 + per_platform["current_count"] = 0 + profile_f.close() + + def _write(self): + print("Writing profile file %s" % self.fname) + profile_f = open(self.fname, "w") + profile_f.write(self._header()) + for test_key in sorted(self.data): + per_fn = self.data[test_key] + profile_f.write("\n# TEST: %s\n\n" % test_key) + for platform_key in sorted(per_fn): + per_platform = per_fn[platform_key] + c = ",".join(str(count) for count in per_platform["counts"]) + profile_f.write("%s %s %s\n" % (test_key, platform_key, c)) + profile_f.close() + + +def function_call_count(variance=0.05, times=1, warmup=0): + """Assert a target for a test case's function call count. + + The main purpose of this assertion is to detect changes in + callcounts for various functions - the actual number is not as important. + Callcounts are stored in a file keyed to Python version and OS platform + information. This file is generated automatically for new tests, + and versioned so that unexpected changes in callcounts will be detected. + + """ + + # use signature-rewriting decorator function so that pytest fixtures + # still work on py27. In Py3, update_wrapper() alone is good enough, + # likely due to the introduction of __signature__. + + from sqlalchemy.util import decorator + + @decorator + def wrap(fn, *args, **kw): + for warm in range(warmup): + fn(*args, **kw) + + timerange = range(times) + with count_functions(variance=variance): + for time in timerange: + rv = fn(*args, **kw) + return rv + + return wrap + + +@contextlib.contextmanager +def count_functions(variance=0.05): + if cProfile is None: + raise config._skip_test_exception("cProfile is not installed") + + if not _profile_stats.has_stats() and not _profile_stats.write: + config.skip_test( + "No profiling stats available on this " + "platform for this function. Run tests with " + "--write-profiles to add statistics to %s for " + "this platform." % _profile_stats.short_fname + ) + + gc_collect() + + pr = cProfile.Profile() + pr.enable() + # began = time.time() + yield + # ended = time.time() + pr.disable() + + # s = StringIO() + stats = pstats.Stats(pr, stream=sys.stdout) + + # timespent = ended - began + callcount = stats.total_calls + + expected = _profile_stats.result(callcount) + + if expected is None: + expected_count = None + else: + line_no, expected_count = expected + + print("Pstats calls: %d Expected %s" % (callcount, expected_count)) + stats.sort_stats(*re.split(r"[, ]", _profile_stats.sort)) + stats.print_stats() + if _profile_stats.dump: + base, ext = os.path.splitext(_profile_stats.dump) + test_name = _current_test.split(".")[-1] + dumpfile = "%s_%s%s" % (base, test_name, ext or ".profile") + stats.dump_stats(dumpfile) + print("Dumped stats to file %s" % dumpfile) + # stats.print_callers() + if _profile_stats.force_write: + _profile_stats.replace(callcount) + elif expected_count: + deviance = int(callcount * variance) + failed = abs(callcount - expected_count) > deviance + + if failed: + if _profile_stats.write: + _profile_stats.replace(callcount) + else: + raise AssertionError( + "Adjusted function call count %s not within %s%% " + "of expected %s, platform %s. Rerun with " + "--write-profiles to " + "regenerate this callcount." + % ( + callcount, + (variance * 100), + expected_count, + _profile_stats.platform_key, + ) + )