Загрузить файлы в «venv/Lib/site-packages/sqlalchemy/testing»

This commit is contained in:
2026-07-02 20:39:14 +00:00
parent 66d6713675
commit 98ce470685
5 changed files with 1558 additions and 0 deletions

View File

@@ -0,0 +1,482 @@
# testing/engines.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 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)

View File

@@ -0,0 +1,117 @@
# testing/entities.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 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__)

View File

@@ -0,0 +1,476 @@
# testing/exclusions.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
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 "<no driver>"
),
"database": (
config.db.url.get_backend_name() if config else "<no database>"
),
"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
)

View File

@@ -0,0 +1,155 @@
# testing/pickleable.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
"""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

View File

@@ -0,0 +1,328 @@
# testing/profiling.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
"""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,
)
)