Загрузить файлы в «venv/Lib/site-packages/sqlalchemy/testing»
This commit is contained in:
482
venv/Lib/site-packages/sqlalchemy/testing/engines.py
Normal file
482
venv/Lib/site-packages/sqlalchemy/testing/engines.py
Normal 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)
|
||||||
117
venv/Lib/site-packages/sqlalchemy/testing/entities.py
Normal file
117
venv/Lib/site-packages/sqlalchemy/testing/entities.py
Normal 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__)
|
||||||
476
venv/Lib/site-packages/sqlalchemy/testing/exclusions.py
Normal file
476
venv/Lib/site-packages/sqlalchemy/testing/exclusions.py
Normal 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
|
||||||
|
)
|
||||||
155
venv/Lib/site-packages/sqlalchemy/testing/pickleable.py
Normal file
155
venv/Lib/site-packages/sqlalchemy/testing/pickleable.py
Normal 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
|
||||||
328
venv/Lib/site-packages/sqlalchemy/testing/profiling.py
Normal file
328
venv/Lib/site-packages/sqlalchemy/testing/profiling.py
Normal 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,
|
||||||
|
)
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user