From d7498cf4adb8ef9f35d1efa6dc747ac9eb489e60 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 14 Sep 2014 21:41:13 -0400 Subject: - remove some crufty old testing options - reestablish the "bootstrap" system of loading the test runners in testing/plugin; using the updated approach we just came up with for alembic. Coverage should be fixed now when running either py.test or nose. fixes #3196 - upgrade tox.ini and start using a .coveragerc file --- lib/sqlalchemy/testing/plugin/bootstrap.py | 44 ++++++ lib/sqlalchemy/testing/plugin/noseplugin.py | 32 ++--- lib/sqlalchemy/testing/plugin/plugin_base.py | 56 ++------ lib/sqlalchemy/testing/plugin/provision.py | 187 ------------------------- lib/sqlalchemy/testing/plugin/pytestplugin.py | 14 +- lib/sqlalchemy/testing/provision.py | 189 ++++++++++++++++++++++++++ lib/sqlalchemy/testing/runner.py | 2 +- 7 files changed, 271 insertions(+), 253 deletions(-) create mode 100644 lib/sqlalchemy/testing/plugin/bootstrap.py delete mode 100644 lib/sqlalchemy/testing/plugin/provision.py create mode 100644 lib/sqlalchemy/testing/provision.py (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/plugin/bootstrap.py b/lib/sqlalchemy/testing/plugin/bootstrap.py new file mode 100644 index 000000000..497fcb7e5 --- /dev/null +++ b/lib/sqlalchemy/testing/plugin/bootstrap.py @@ -0,0 +1,44 @@ +""" +Bootstrapper for nose/pytest plugins. + +The entire rationale for this system is to get the modules in plugin/ +imported without importing all of the supporting library, so that we can +set up things for testing before coverage starts. + +The rationale for all of plugin/ being *in* the supporting library in the +first place is so that the testing and plugin suite is available to other +libraries, mainly external SQLAlchemy and Alembic dialects, to make use +of the same test environment and standard suites available to +SQLAlchemy/Alembic themselves without the need to ship/install a separate +package outside of SQLAlchemy. + +NOTE: copied/adapted from SQLAlchemy master for backwards compatibility; +this should be removable when Alembic targets SQLAlchemy 1.0.0. + +""" + +import os +import sys + +bootstrap_file = locals()['bootstrap_file'] +to_bootstrap = locals()['to_bootstrap'] + + +def load_file_as_module(name): + path = os.path.join(os.path.dirname(bootstrap_file), "%s.py" % name) + if sys.version_info >= (3, 3): + from importlib import machinery + mod = machinery.SourceFileLoader(name, path).load_module() + else: + import imp + mod = imp.load_source(name, path) + return mod + +if to_bootstrap == "pytest": + sys.modules["sqla_plugin_base"] = load_file_as_module("plugin_base") + sys.modules["sqla_pytestplugin"] = load_file_as_module("pytestplugin") +elif to_bootstrap == "nose": + sys.modules["sqla_plugin_base"] = load_file_as_module("plugin_base") + sys.modules["sqla_noseplugin"] = load_file_as_module("noseplugin") +else: + raise Exception("unknown bootstrap: %s" % to_bootstrap) # noqa diff --git a/lib/sqlalchemy/testing/plugin/noseplugin.py b/lib/sqlalchemy/testing/plugin/noseplugin.py index 6ef539142..eb4a3b258 100644 --- a/lib/sqlalchemy/testing/plugin/noseplugin.py +++ b/lib/sqlalchemy/testing/plugin/noseplugin.py @@ -12,6 +12,14 @@ way (e.g. as a package-less import). """ +try: + # installed by bootstrap.py + import sqla_plugin_base as plugin_base +except ImportError: + # assume we're a package, use traditional import + from . import plugin_base + + import os import sys @@ -19,16 +27,6 @@ from nose.plugins import Plugin fixtures = None py3k = sys.version_info >= (3, 0) -# no package imports yet! this prevents us from tripping coverage -# too soon. -path = os.path.join(os.path.dirname(__file__), "plugin_base.py") -if sys.version_info >= (3, 3): - from importlib import machinery - plugin_base = machinery.SourceFileLoader( - "plugin_base", path).load_module() -else: - import imp - plugin_base = imp.load_source("plugin_base", path) class NoseSQLAlchemy(Plugin): @@ -58,10 +56,10 @@ class NoseSQLAlchemy(Plugin): plugin_base.set_coverage_flag(options.enable_plugin_coverage) + def begin(self): global fixtures - from sqlalchemy.testing import fixtures + from sqlalchemy.testing import fixtures # noqa - def begin(self): plugin_base.post_begin() def describeTest(self, test): @@ -72,19 +70,21 @@ class NoseSQLAlchemy(Plugin): def wantMethod(self, fn): if py3k: + if not hasattr(fn.__self__, 'cls'): + return False cls = fn.__self__.cls else: cls = fn.im_class - print "METH:", fn, "CLS:", cls return plugin_base.want_method(cls, fn) def wantClass(self, cls): return plugin_base.want_class(cls) def beforeTest(self, test): - plugin_base.before_test(test, - test.test.cls.__module__, - test.test.cls, test.test.method.__name__) + plugin_base.before_test( + test, + test.test.cls.__module__, + test.test.cls, test.test.method.__name__) def afterTest(self, test): plugin_base.after_test(test) diff --git a/lib/sqlalchemy/testing/plugin/plugin_base.py b/lib/sqlalchemy/testing/plugin/plugin_base.py index 7ba31d3e3..6696427dc 100644 --- a/lib/sqlalchemy/testing/plugin/plugin_base.py +++ b/lib/sqlalchemy/testing/plugin/plugin_base.py @@ -31,8 +31,6 @@ if py3k: else: import ConfigParser as configparser -FOLLOWER_IDENT = None - # late imports fixtures = None engines = None @@ -72,8 +70,6 @@ def setup_options(make_option): help="Drop all tables in the target database first") make_option("--backend-only", action="store_true", dest="backend_only", help="Run only tests marked with __backend__") - make_option("--mockpool", action="store_true", dest="mockpool", - help="Use mock pool (asserts only one connection used)") make_option("--low-connections", action="store_true", dest="low_connections", help="Use a low number of distinct connections - " @@ -95,14 +91,6 @@ def setup_options(make_option): make_option("--exclude-tag", action="callback", callback=_exclude_tag, type="string", help="Exclude tests with tag ") - make_option("--serverside", action="store_true", - help="Turn on server side cursors for PG") - make_option("--mysql-engine", action="store", - dest="mysql_engine", default=None, - help="Use the specified MySQL storage engine for all tables, " - "default is a db-default/InnoDB combo.") - make_option("--tableopts", action="append", dest="tableopts", default=[], - help="Add a dialect-specific table option, key=value") make_option("--write-profiles", action="store_true", dest="write_profiles", default=False, help="Write/update profiling data.") @@ -115,8 +103,8 @@ def configure_follower(follower_ident): database creation. """ - global FOLLOWER_IDENT - FOLLOWER_IDENT = follower_ident + from sqlalchemy.testing import provision + provision.FOLLOWER_IDENT = follower_ident def memoize_important_follower_config(dict_): @@ -177,12 +165,14 @@ def post_begin(): global util, fixtures, engines, exclusions, \ assertions, warnings, profiling,\ config, testing - from sqlalchemy import testing - from sqlalchemy.testing import fixtures, engines, exclusions, \ - assertions, warnings, profiling, config - from sqlalchemy import util + from sqlalchemy import testing # noqa + from sqlalchemy.testing import fixtures, engines, exclusions # noqa + from sqlalchemy.testing import assertions, warnings, profiling # noqa + from sqlalchemy.testing import config # noqa + from sqlalchemy import util # noqa warnings.setup_filters() + def _log(opt_str, value, parser): global logging if not logging: @@ -233,12 +223,6 @@ def _setup_options(opt, file_config): options = opt -@pre -def _server_side_cursors(options, file_config): - if options.serverside: - db_opts['server_side_cursors'] = True - - @pre def _monkeypatch_cdecimal(options, file_config): if options.cdecimal: @@ -250,7 +234,7 @@ def _monkeypatch_cdecimal(options, file_config): def _engine_uri(options, file_config): from sqlalchemy.testing import config from sqlalchemy import testing - from sqlalchemy.testing.plugin import provision + from sqlalchemy.testing import provision if options.dburi: db_urls = list(options.dburi) @@ -273,19 +257,12 @@ def _engine_uri(options, file_config): for db_url in db_urls: cfg = provision.setup_config( - db_url, db_opts, options, file_config, FOLLOWER_IDENT) + db_url, db_opts, options, file_config, provision.FOLLOWER_IDENT) if not config._current: cfg.set_as_current(cfg, testing) -@post -def _engine_pool(options, file_config): - if options.mockpool: - from sqlalchemy import pool - db_opts['poolclass'] = pool.AssertionPool - - @post def _requirements(options, file_config): @@ -368,19 +345,6 @@ def _prep_testing_database(options, file_config): schema=enum['schema']))) -@post -def _set_table_options(options, file_config): - from sqlalchemy.testing import schema - - table_options = schema.table_options - for spec in options.tableopts: - key, value = spec.split('=') - table_options[key] = value - - if options.mysql_engine: - table_options['mysql_engine'] = options.mysql_engine - - @post def _reverse_topological(options, file_config): if options.reversetop: diff --git a/lib/sqlalchemy/testing/plugin/provision.py b/lib/sqlalchemy/testing/plugin/provision.py deleted file mode 100644 index c6b9030f5..000000000 --- a/lib/sqlalchemy/testing/plugin/provision.py +++ /dev/null @@ -1,187 +0,0 @@ -from sqlalchemy.engine import url as sa_url -from sqlalchemy import text -from sqlalchemy.util import compat -from .. import config, engines -import os - - -class register(object): - def __init__(self): - self.fns = {} - - @classmethod - def init(cls, fn): - return register().for_db("*")(fn) - - def for_db(self, dbname): - def decorate(fn): - self.fns[dbname] = fn - return self - return decorate - - def __call__(self, cfg, *arg): - if isinstance(cfg, compat.string_types): - url = sa_url.make_url(cfg) - elif isinstance(cfg, sa_url.URL): - url = cfg - else: - url = cfg.db.url - backend = url.get_backend_name() - if backend in self.fns: - return self.fns[backend](cfg, *arg) - else: - return self.fns['*'](cfg, *arg) - - -def create_follower_db(follower_ident): - - for cfg in _configs_for_db_operation(): - _create_db(cfg, cfg.db, follower_ident) - - -def configure_follower(follower_ident): - for cfg in config.Config.all_configs(): - _configure_follower(cfg, follower_ident) - - -def setup_config(db_url, db_opts, options, file_config, follower_ident): - if follower_ident: - db_url = _follower_url_from_main(db_url, follower_ident) - eng = engines.testing_engine(db_url, db_opts) - eng.connect().close() - cfg = config.Config.register(eng, db_opts, options, file_config) - if follower_ident: - _configure_follower(cfg, follower_ident) - return cfg - - -def drop_follower_db(follower_ident): - for cfg in _configs_for_db_operation(): - _drop_db(cfg, cfg.db, follower_ident) - - -def _configs_for_db_operation(): - hosts = set() - - for cfg in config.Config.all_configs(): - cfg.db.dispose() - - for cfg in config.Config.all_configs(): - url = cfg.db.url - backend = url.get_backend_name() - host_conf = ( - backend, - url.username, url.host, url.database) - - if host_conf not in hosts: - yield cfg - hosts.add(host_conf) - - for cfg in config.Config.all_configs(): - cfg.db.dispose() - - -@register.init -def _create_db(cfg, eng, ident): - raise NotImplementedError("no DB creation routine for cfg: %s" % eng.url) - - -@register.init -def _drop_db(cfg, eng, ident): - raise NotImplementedError("no DB drop routine for cfg: %s" % eng.url) - - -@register.init -def _configure_follower(cfg, ident): - pass - - -@register.init -def _follower_url_from_main(url, ident): - url = sa_url.make_url(url) - url.database = ident - return url - - -@_follower_url_from_main.for_db("sqlite") -def _sqlite_follower_url_from_main(url, ident): - url = sa_url.make_url(url) - if not url.database or url.database == ':memory:': - return url - else: - return sa_url.make_url("sqlite:///%s.db" % ident) - - -@_create_db.for_db("postgresql") -def _pg_create_db(cfg, eng, ident): - with eng.connect().execution_options( - isolation_level="AUTOCOMMIT") as conn: - try: - _pg_drop_db(cfg, conn, ident) - except: - pass - currentdb = conn.scalar("select current_database()") - conn.execute("CREATE DATABASE %s TEMPLATE %s" % (ident, currentdb)) - - -@_create_db.for_db("mysql") -def _mysql_create_db(cfg, eng, ident): - with eng.connect() as conn: - try: - _mysql_drop_db(cfg, conn, ident) - except: - pass - conn.execute("CREATE DATABASE %s" % ident) - conn.execute("CREATE DATABASE %s_test_schema" % ident) - conn.execute("CREATE DATABASE %s_test_schema_2" % ident) - - -@_configure_follower.for_db("mysql") -def _mysql_configure_follower(config, ident): - config.test_schema = "%s_test_schema" % ident - config.test_schema_2 = "%s_test_schema_2" % ident - - -@_create_db.for_db("sqlite") -def _sqlite_create_db(cfg, eng, ident): - pass - - -@_drop_db.for_db("postgresql") -def _pg_drop_db(cfg, eng, ident): - with eng.connect().execution_options( - isolation_level="AUTOCOMMIT") as conn: - conn.execute( - text( - "select pg_terminate_backend(pid) from pg_stat_activity " - "where usename=current_user and pid != pg_backend_pid() " - "and datname=:dname" - ), dname=ident) - conn.execute("DROP DATABASE %s" % ident) - - -@_drop_db.for_db("sqlite") -def _sqlite_drop_db(cfg, eng, ident): - pass - #os.remove("%s.db" % ident) - - -@_drop_db.for_db("mysql") -def _mysql_drop_db(cfg, eng, ident): - with eng.connect() as conn: - try: - conn.execute("DROP DATABASE %s_test_schema" % ident) - except: - pass - try: - conn.execute("DROP DATABASE %s_test_schema_2" % ident) - except: - pass - try: - conn.execute("DROP DATABASE %s" % ident) - except: - pass - - - - diff --git a/lib/sqlalchemy/testing/plugin/pytestplugin.py b/lib/sqlalchemy/testing/plugin/pytestplugin.py index 005942913..4bbc8ed9a 100644 --- a/lib/sqlalchemy/testing/plugin/pytestplugin.py +++ b/lib/sqlalchemy/testing/plugin/pytestplugin.py @@ -1,7 +1,13 @@ +try: + # installed by bootstrap.py + import sqla_plugin_base as plugin_base +except ImportError: + # assume we're a package, use traditional import + from . import plugin_base + import pytest import argparse import inspect -from . import plugin_base import collections import itertools @@ -42,6 +48,8 @@ def pytest_configure(config): plugin_base.set_coverage_flag(bool(getattr(config.option, "cov_source", False))) + +def pytest_sessionstart(session): plugin_base.post_begin() if has_xdist: @@ -54,11 +62,11 @@ if has_xdist: plugin_base.memoize_important_follower_config(node.slaveinput) node.slaveinput["follower_ident"] = "test_%s" % next(_follower_count) - from . import provision + from sqlalchemy.testing import provision provision.create_follower_db(node.slaveinput["follower_ident"]) def pytest_testnodedown(node, error): - from . import provision + from sqlalchemy.testing import provision provision.drop_follower_db(node.slaveinput["follower_ident"]) diff --git a/lib/sqlalchemy/testing/provision.py b/lib/sqlalchemy/testing/provision.py new file mode 100644 index 000000000..0bcdad959 --- /dev/null +++ b/lib/sqlalchemy/testing/provision.py @@ -0,0 +1,189 @@ +from sqlalchemy.engine import url as sa_url +from sqlalchemy import text +from sqlalchemy.util import compat +from . import config, engines + + +FOLLOWER_IDENT = None + + +class register(object): + def __init__(self): + self.fns = {} + + @classmethod + def init(cls, fn): + return register().for_db("*")(fn) + + def for_db(self, dbname): + def decorate(fn): + self.fns[dbname] = fn + return self + return decorate + + def __call__(self, cfg, *arg): + if isinstance(cfg, compat.string_types): + url = sa_url.make_url(cfg) + elif isinstance(cfg, sa_url.URL): + url = cfg + else: + url = cfg.db.url + backend = url.get_backend_name() + if backend in self.fns: + return self.fns[backend](cfg, *arg) + else: + return self.fns['*'](cfg, *arg) + + +def create_follower_db(follower_ident): + + for cfg in _configs_for_db_operation(): + _create_db(cfg, cfg.db, follower_ident) + + +def configure_follower(follower_ident): + for cfg in config.Config.all_configs(): + _configure_follower(cfg, follower_ident) + + +def setup_config(db_url, db_opts, options, file_config, follower_ident): + if follower_ident: + db_url = _follower_url_from_main(db_url, follower_ident) + eng = engines.testing_engine(db_url, db_opts) + eng.connect().close() + cfg = config.Config.register(eng, db_opts, options, file_config) + if follower_ident: + _configure_follower(cfg, follower_ident) + return cfg + + +def drop_follower_db(follower_ident): + for cfg in _configs_for_db_operation(): + _drop_db(cfg, cfg.db, follower_ident) + + +def _configs_for_db_operation(): + hosts = set() + + for cfg in config.Config.all_configs(): + cfg.db.dispose() + + for cfg in config.Config.all_configs(): + url = cfg.db.url + backend = url.get_backend_name() + host_conf = ( + backend, + url.username, url.host, url.database) + + if host_conf not in hosts: + yield cfg + hosts.add(host_conf) + + for cfg in config.Config.all_configs(): + cfg.db.dispose() + + +@register.init +def _create_db(cfg, eng, ident): + raise NotImplementedError("no DB creation routine for cfg: %s" % eng.url) + + +@register.init +def _drop_db(cfg, eng, ident): + raise NotImplementedError("no DB drop routine for cfg: %s" % eng.url) + + +@register.init +def _configure_follower(cfg, ident): + pass + + +@register.init +def _follower_url_from_main(url, ident): + url = sa_url.make_url(url) + url.database = ident + return url + + +@_follower_url_from_main.for_db("sqlite") +def _sqlite_follower_url_from_main(url, ident): + url = sa_url.make_url(url) + if not url.database or url.database == ':memory:': + return url + else: + return sa_url.make_url("sqlite:///%s.db" % ident) + + +@_create_db.for_db("postgresql") +def _pg_create_db(cfg, eng, ident): + with eng.connect().execution_options( + isolation_level="AUTOCOMMIT") as conn: + try: + _pg_drop_db(cfg, conn, ident) + except: + pass + currentdb = conn.scalar("select current_database()") + conn.execute("CREATE DATABASE %s TEMPLATE %s" % (ident, currentdb)) + + +@_create_db.for_db("mysql") +def _mysql_create_db(cfg, eng, ident): + with eng.connect() as conn: + try: + _mysql_drop_db(cfg, conn, ident) + except: + pass + conn.execute("CREATE DATABASE %s" % ident) + conn.execute("CREATE DATABASE %s_test_schema" % ident) + conn.execute("CREATE DATABASE %s_test_schema_2" % ident) + + +@_configure_follower.for_db("mysql") +def _mysql_configure_follower(config, ident): + config.test_schema = "%s_test_schema" % ident + config.test_schema_2 = "%s_test_schema_2" % ident + + +@_create_db.for_db("sqlite") +def _sqlite_create_db(cfg, eng, ident): + pass + + +@_drop_db.for_db("postgresql") +def _pg_drop_db(cfg, eng, ident): + with eng.connect().execution_options( + isolation_level="AUTOCOMMIT") as conn: + conn.execute( + text( + "select pg_terminate_backend(pid) from pg_stat_activity " + "where usename=current_user and pid != pg_backend_pid() " + "and datname=:dname" + ), dname=ident) + conn.execute("DROP DATABASE %s" % ident) + + +@_drop_db.for_db("sqlite") +def _sqlite_drop_db(cfg, eng, ident): + pass + #os.remove("%s.db" % ident) + + +@_drop_db.for_db("mysql") +def _mysql_drop_db(cfg, eng, ident): + with eng.connect() as conn: + try: + conn.execute("DROP DATABASE %s_test_schema" % ident) + except: + pass + try: + conn.execute("DROP DATABASE %s_test_schema_2" % ident) + except: + pass + try: + conn.execute("DROP DATABASE %s" % ident) + except: + pass + + + + diff --git a/lib/sqlalchemy/testing/runner.py b/lib/sqlalchemy/testing/runner.py index df254520b..23d7a0a91 100644 --- a/lib/sqlalchemy/testing/runner.py +++ b/lib/sqlalchemy/testing/runner.py @@ -30,7 +30,7 @@ SQLAlchemy itself is possible. """ -from sqlalchemy.testing.plugin.noseplugin import NoseSQLAlchemy +from .plugin.noseplugin import NoseSQLAlchemy import nose -- cgit v1.2.1 From 8b7f57d258e372dcac678b2b9cb9629b6f7a24a3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 15 Sep 2014 14:53:20 -0400 Subject: - fix issue where nose Failure object comes into play here --- lib/sqlalchemy/testing/plugin/noseplugin.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/plugin/noseplugin.py b/lib/sqlalchemy/testing/plugin/noseplugin.py index eb4a3b258..538087770 100644 --- a/lib/sqlalchemy/testing/plugin/noseplugin.py +++ b/lib/sqlalchemy/testing/plugin/noseplugin.py @@ -81,6 +81,8 @@ class NoseSQLAlchemy(Plugin): return plugin_base.want_class(cls) def beforeTest(self, test): + if not hasattr(test.test, 'cls'): + return plugin_base.before_test( test, test.test.cls.__module__, -- cgit v1.2.1 From cc3dba01db0367d4172cca1b902976ac7718e4cf Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 16 Sep 2014 16:20:22 -0400 Subject: - raise from cause here to preserve stack trace --- lib/sqlalchemy/testing/exclusions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/exclusions.py b/lib/sqlalchemy/testing/exclusions.py index 283d89e36..49211f805 100644 --- a/lib/sqlalchemy/testing/exclusions.py +++ b/lib/sqlalchemy/testing/exclusions.py @@ -133,7 +133,7 @@ class compound(object): name, fail._as_string(config), str(ex)))) break else: - raise ex + util.raise_from_cause(ex) def _expect_success(self, config, name='block'): if not self.fails: -- cgit v1.2.1 From cb23fa243f5138aac7acb2a134d567f1a297d42e Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 17 Sep 2014 15:15:21 -0400 Subject: - Added :meth:`.Inspector.get_temp_table_names` and :meth:`.Inspector.get_temp_view_names`; currently, only the SQLite dialect supports these methods. The return of temporary table and view names has been **removed** from SQLite's version of :meth:`.Inspector.get_table_names` and :meth:`.Inspector.get_view_names`; other database backends cannot support this information (such as MySQL), and the scope of operation is different in that the tables can be local to a session and typically aren't supported in remote schemas. fixes #3204 --- lib/sqlalchemy/testing/requirements.py | 14 +++++ lib/sqlalchemy/testing/suite/test_reflection.py | 78 +++++++++++++++++++++++++ 2 files changed, 92 insertions(+) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index a04bcbbdd..da3e3128a 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -313,6 +313,20 @@ class SuiteRequirements(Requirements): def foreign_key_constraint_reflection(self): return exclusions.open() + @property + def temp_table_reflection(self): + return exclusions.open() + + @property + def temp_table_names(self): + """target dialect supports listing of temporary table names""" + return exclusions.closed() + + @property + def temporary_views(self): + """target database supports temporary views""" + return exclusions.closed() + @property def index_reflection(self): return exclusions.open() diff --git a/lib/sqlalchemy/testing/suite/test_reflection.py b/lib/sqlalchemy/testing/suite/test_reflection.py index 575a38db9..690a880bb 100644 --- a/lib/sqlalchemy/testing/suite/test_reflection.py +++ b/lib/sqlalchemy/testing/suite/test_reflection.py @@ -95,6 +95,27 @@ class ComponentReflectionTest(fixtures.TablesTest): cls.define_index(metadata, users) if testing.requires.view_column_reflection.enabled: cls.define_views(metadata, schema) + if not schema and testing.requires.temp_table_reflection.enabled: + cls.define_temp_tables(metadata) + + @classmethod + def define_temp_tables(cls, metadata): + temp_table = Table( + "user_tmp", metadata, + Column("id", sa.INT, primary_key=True), + Column('name', sa.VARCHAR(50)), + Column('foo', sa.INT), + sa.UniqueConstraint('name', name='user_tmp_uq'), + sa.Index("user_tmp_ix", "foo"), + prefixes=['TEMPORARY'] + ) + if testing.requires.view_reflection.enabled and \ + testing.requires.temporary_views.enabled: + event.listen( + temp_table, "after_create", + DDL("create temporary view user_tmp_v as " + "select * from user_tmp") + ) @classmethod def define_index(cls, metadata, users): @@ -147,6 +168,7 @@ class ComponentReflectionTest(fixtures.TablesTest): users, addresses, dingalings = self.tables.users, \ self.tables.email_addresses, self.tables.dingalings insp = inspect(meta.bind) + if table_type == 'view': table_names = insp.get_view_names(schema) table_names.sort() @@ -162,6 +184,20 @@ class ComponentReflectionTest(fixtures.TablesTest): answer = ['dingalings', 'email_addresses', 'users'] eq_(sorted(table_names), answer) + @testing.requires.temp_table_names + def test_get_temp_table_names(self): + insp = inspect(self.metadata.bind) + temp_table_names = insp.get_temp_table_names() + eq_(sorted(temp_table_names), ['user_tmp']) + + @testing.requires.view_reflection + @testing.requires.temp_table_names + @testing.requires.temporary_views + def test_get_temp_view_names(self): + insp = inspect(self.metadata.bind) + temp_table_names = insp.get_temp_view_names() + eq_(sorted(temp_table_names), ['user_tmp_v']) + @testing.requires.table_reflection def test_get_table_names(self): self._test_get_table_names() @@ -294,6 +330,28 @@ class ComponentReflectionTest(fixtures.TablesTest): def test_get_columns_with_schema(self): self._test_get_columns(schema=testing.config.test_schema) + @testing.requires.temp_table_reflection + def test_get_temp_table_columns(self): + meta = MetaData(testing.db) + user_tmp = self.tables.user_tmp + insp = inspect(meta.bind) + cols = insp.get_columns('user_tmp') + self.assert_(len(cols) > 0, len(cols)) + + for i, col in enumerate(user_tmp.columns): + eq_(col.name, cols[i]['name']) + + @testing.requires.temp_table_reflection + @testing.requires.view_column_reflection + @testing.requires.temporary_views + def test_get_temp_view_columns(self): + insp = inspect(self.metadata.bind) + cols = insp.get_columns('user_tmp_v') + eq_( + [col['name'] for col in cols], + ['id', 'name', 'foo'] + ) + @testing.requires.view_column_reflection def test_get_view_columns(self): self._test_get_columns(table_type='view') @@ -426,6 +484,26 @@ class ComponentReflectionTest(fixtures.TablesTest): def test_get_unique_constraints(self): self._test_get_unique_constraints() + @testing.requires.temp_table_reflection + def test_get_temp_table_unique_constraints(self): + insp = inspect(self.metadata.bind) + eq_( + insp.get_unique_constraints('user_tmp'), + [{'column_names': ['name'], 'name': 'user_tmp_uq'}] + ) + + @testing.requires.temp_table_reflection + def test_get_temp_table_indexes(self): + insp = inspect(self.metadata.bind) + indexes = insp.get_indexes('user_tmp') + eq_( + # TODO: we need to add better filtering for indexes/uq constraints + # that are doubled up + [idx for idx in indexes if idx['name'] == 'user_tmp_ix'], + [{'unique': False, 'column_names': ['foo'], 'name': 'user_tmp_ix'}] + ) + + @testing.requires.unique_constraint_reflection @testing.requires.schemas def test_get_unique_constraints_with_schema(self): -- cgit v1.2.1 From 7fa21b22989f6d53ff70a8df71fc6d210c556e07 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Wed, 10 Sep 2014 07:37:59 -0700 Subject: Reflect unique constraints when reflecting a Table object Calls to reflect a table did not create any UniqueConstraint objects. The reflection core made no calls to get_unique_constraints and as a result, the sqlite dialect would never reflect any unique constraints. MySQL transparently converts unique constraints into unique indexes, but SQLAlchemy would reflect those as an Index object and as a UniqueConstraint. The reflection core will now deduplicate the unique constraints. PostgreSQL would reflect unique constraints as an Index object and as a UniqueConstraint object. The reflection core will now deduplicate the unique indexes. --- lib/sqlalchemy/testing/suite/test_reflection.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/suite/test_reflection.py b/lib/sqlalchemy/testing/suite/test_reflection.py index 690a880bb..bd0be5738 100644 --- a/lib/sqlalchemy/testing/suite/test_reflection.py +++ b/lib/sqlalchemy/testing/suite/test_reflection.py @@ -487,10 +487,12 @@ class ComponentReflectionTest(fixtures.TablesTest): @testing.requires.temp_table_reflection def test_get_temp_table_unique_constraints(self): insp = inspect(self.metadata.bind) - eq_( - insp.get_unique_constraints('user_tmp'), - [{'column_names': ['name'], 'name': 'user_tmp_uq'}] - ) + reflected = insp.get_unique_constraints('user_tmp') + for refl in reflected: + # Different dialects handle duplicate index and constraints + # differently, so ignore this flag + refl.pop('duplicates_index', None) + eq_(reflected, [{'column_names': ['name'], 'name': 'user_tmp_uq'}]) @testing.requires.temp_table_reflection def test_get_temp_table_indexes(self): @@ -544,6 +546,9 @@ class ComponentReflectionTest(fixtures.TablesTest): ) for orig, refl in zip(uniques, reflected): + # Different dialects handle duplicate index and constraints + # differently, so ignore this flag + refl.pop('duplicates_index', None) eq_(orig, refl) @testing.provide_metadata -- cgit v1.2.1 From e3f07f7206cf0d6a5f2ff9344a365f4657645338 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 17 Sep 2014 19:43:45 -0400 Subject: - Added support for the Oracle table option ON COMMIT. This is being kept separate from Postgresql's ON COMMIT for now even though ON COMMIT is in the SQL standard; the option is still very specific to temp tables and we eventually would provide a more first class temporary table feature. - oracle can apparently do get_temp_table_names() too, so implement that, fix its get_table_names(), and add it to #3204. fixes #3204 again. --- lib/sqlalchemy/testing/suite/test_reflection.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/suite/test_reflection.py b/lib/sqlalchemy/testing/suite/test_reflection.py index 690a880bb..60db9eb47 100644 --- a/lib/sqlalchemy/testing/suite/test_reflection.py +++ b/lib/sqlalchemy/testing/suite/test_reflection.py @@ -100,19 +100,31 @@ class ComponentReflectionTest(fixtures.TablesTest): @classmethod def define_temp_tables(cls, metadata): - temp_table = Table( + # cheat a bit, we should fix this with some dialect-level + # temp table fixture + if testing.against("oracle"): + kw = { + 'prefixes': ["GLOBAL TEMPORARY"], + 'oracle_on_commit': 'PRESERVE ROWS' + } + else: + kw = { + 'prefixes': ["TEMPORARY"], + } + + user_tmp = Table( "user_tmp", metadata, Column("id", sa.INT, primary_key=True), Column('name', sa.VARCHAR(50)), Column('foo', sa.INT), sa.UniqueConstraint('name', name='user_tmp_uq'), sa.Index("user_tmp_ix", "foo"), - prefixes=['TEMPORARY'] + **kw ) if testing.requires.view_reflection.enabled and \ testing.requires.temporary_views.enabled: event.listen( - temp_table, "after_create", + user_tmp, "after_create", DDL("create temporary view user_tmp_v as " "select * from user_tmp") ) @@ -186,7 +198,7 @@ class ComponentReflectionTest(fixtures.TablesTest): @testing.requires.temp_table_names def test_get_temp_table_names(self): - insp = inspect(self.metadata.bind) + insp = inspect(testing.db) temp_table_names = insp.get_temp_table_names() eq_(sorted(temp_table_names), ['user_tmp']) @@ -485,6 +497,7 @@ class ComponentReflectionTest(fixtures.TablesTest): self._test_get_unique_constraints() @testing.requires.temp_table_reflection + @testing.requires.unique_constraint_reflection def test_get_temp_table_unique_constraints(self): insp = inspect(self.metadata.bind) eq_( @@ -503,7 +516,6 @@ class ComponentReflectionTest(fixtures.TablesTest): [{'unique': False, 'column_names': ['foo'], 'name': 'user_tmp_ix'}] ) - @testing.requires.unique_constraint_reflection @testing.requires.schemas def test_get_unique_constraints_with_schema(self): -- cgit v1.2.1 From ce52dd9e3b71f2074d7821fe62803d4e0eefe512 Mon Sep 17 00:00:00 2001 From: ndparker Date: Tue, 23 Sep 2014 23:28:11 +0200 Subject: improve exception vs. exit handling --- lib/sqlalchemy/testing/provision.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/provision.py b/lib/sqlalchemy/testing/provision.py index 0bcdad959..64688d6b5 100644 --- a/lib/sqlalchemy/testing/provision.py +++ b/lib/sqlalchemy/testing/provision.py @@ -120,6 +120,8 @@ def _pg_create_db(cfg, eng, ident): isolation_level="AUTOCOMMIT") as conn: try: _pg_drop_db(cfg, conn, ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass currentdb = conn.scalar("select current_database()") @@ -131,6 +133,8 @@ def _mysql_create_db(cfg, eng, ident): with eng.connect() as conn: try: _mysql_drop_db(cfg, conn, ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass conn.execute("CREATE DATABASE %s" % ident) @@ -173,14 +177,20 @@ def _mysql_drop_db(cfg, eng, ident): with eng.connect() as conn: try: conn.execute("DROP DATABASE %s_test_schema" % ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass try: conn.execute("DROP DATABASE %s_test_schema_2" % ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass try: conn.execute("DROP DATABASE %s" % ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass -- cgit v1.2.1 From 690532131d8ce8250c62f1d3e27405902df03e70 Mon Sep 17 00:00:00 2001 From: ndparker Date: Thu, 2 Oct 2014 22:00:31 +0200 Subject: cleanup exception handling - use new exception hierarchy (since python 2.5) --- lib/sqlalchemy/testing/engines.py | 4 ---- lib/sqlalchemy/testing/provision.py | 20 +++++--------------- 2 files changed, 5 insertions(+), 19 deletions(-) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/engines.py b/lib/sqlalchemy/testing/engines.py index 67c13231e..1284f9c2a 100644 --- a/lib/sqlalchemy/testing/engines.py +++ b/lib/sqlalchemy/testing/engines.py @@ -37,8 +37,6 @@ class ConnectionKiller(object): def _safe(self, fn): try: fn() - except (SystemExit, KeyboardInterrupt): - raise except Exception as e: warnings.warn( "testing_reaper couldn't " @@ -168,8 +166,6 @@ class ReconnectFixture(object): def _safe(self, fn): try: fn() - except (SystemExit, KeyboardInterrupt): - raise except Exception as e: warnings.warn( "ReconnectFixture couldn't " diff --git a/lib/sqlalchemy/testing/provision.py b/lib/sqlalchemy/testing/provision.py index 64688d6b5..c8f7fdf30 100644 --- a/lib/sqlalchemy/testing/provision.py +++ b/lib/sqlalchemy/testing/provision.py @@ -120,9 +120,7 @@ def _pg_create_db(cfg, eng, ident): isolation_level="AUTOCOMMIT") as conn: try: _pg_drop_db(cfg, conn, ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass currentdb = conn.scalar("select current_database()") conn.execute("CREATE DATABASE %s TEMPLATE %s" % (ident, currentdb)) @@ -133,9 +131,7 @@ def _mysql_create_db(cfg, eng, ident): with eng.connect() as conn: try: _mysql_drop_db(cfg, conn, ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass conn.execute("CREATE DATABASE %s" % ident) conn.execute("CREATE DATABASE %s_test_schema" % ident) @@ -177,21 +173,15 @@ def _mysql_drop_db(cfg, eng, ident): with eng.connect() as conn: try: conn.execute("DROP DATABASE %s_test_schema" % ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass try: conn.execute("DROP DATABASE %s_test_schema_2" % ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass try: conn.execute("DROP DATABASE %s" % ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass -- cgit v1.2.1 From 95be42c06ff4e5f3528de42bb04dcba228ea74c2 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 10 Oct 2014 17:15:19 -0400 Subject: - :meth:`.Insert.from_select` now includes Python and SQL-expression defaults if otherwise unspecified; the limitation where non- server column defaults aren't included in an INSERT FROM SELECT is now lifted and these expressions are rendered as constants into the SELECT statement. --- lib/sqlalchemy/testing/suite/test_insert.py | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/suite/test_insert.py b/lib/sqlalchemy/testing/suite/test_insert.py index 92d3d93e5..c197145c7 100644 --- a/lib/sqlalchemy/testing/suite/test_insert.py +++ b/lib/sqlalchemy/testing/suite/test_insert.py @@ -4,7 +4,7 @@ from .. import exclusions from ..assertions import eq_ from .. import engines -from sqlalchemy import Integer, String, select, util +from sqlalchemy import Integer, String, select, literal_column from ..schema import Table, Column @@ -90,6 +90,13 @@ class InsertBehaviorTest(fixtures.TablesTest): Column('id', Integer, primary_key=True, autoincrement=False), Column('data', String(50)) ) + Table('includes_defaults', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('data', String(50)), + Column('x', Integer, default=5), + Column('y', Integer, + default=literal_column("2", type_=Integer) + 2)) def test_autoclose_on_insert(self): if requirements.returning.enabled: @@ -158,6 +165,34 @@ class InsertBehaviorTest(fixtures.TablesTest): ("data3", ), ("data3", )] ) + @requirements.insert_from_select + def test_insert_from_select_with_defaults(self): + table = self.tables.includes_defaults + config.db.execute( + table.insert(), + [ + dict(id=1, data="data1"), + dict(id=2, data="data2"), + dict(id=3, data="data3"), + ] + ) + + config.db.execute( + table.insert(inline=True). + from_select(("id", "data",), + select([table.c.id + 5, table.c.data]). + where(table.c.data.in_(["data2", "data3"])) + ), + ) + + eq_( + config.db.execute( + select([table]).order_by(table.c.data) + ).fetchall(), + [(1, 'data1', 5, 4), (2, 'data2', 5, 4), + (7, 'data2', 5, 4), (3, 'data3', 5, 4), (8, 'data3', 5, 4)] + ) + class ReturningTest(fixtures.TablesTest): run_create_tables = 'each' -- cgit v1.2.1 From 3e810771a8ace7351fcd43deaf55891fd1c30a53 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 11 Oct 2014 17:33:44 -0400 Subject: - change this literal so that the bound name doesn't have a numeric name, this is sort of a bug for oracle --- lib/sqlalchemy/testing/suite/test_insert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/suite/test_insert.py b/lib/sqlalchemy/testing/suite/test_insert.py index c197145c7..2334d3049 100644 --- a/lib/sqlalchemy/testing/suite/test_insert.py +++ b/lib/sqlalchemy/testing/suite/test_insert.py @@ -4,7 +4,7 @@ from .. import exclusions from ..assertions import eq_ from .. import engines -from sqlalchemy import Integer, String, select, literal_column +from sqlalchemy import Integer, String, select, literal_column, literal from ..schema import Table, Column @@ -96,7 +96,7 @@ class InsertBehaviorTest(fixtures.TablesTest): Column('data', String(50)), Column('x', Integer, default=5), Column('y', Integer, - default=literal_column("2", type_=Integer) + 2)) + default=literal_column("2", type_=Integer) + literal(2))) def test_autoclose_on_insert(self): if requirements.returning.enabled: -- cgit v1.2.1 From 83e465633793e4a6d76e41b12fb92d7cc4bbddf3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 11 Oct 2014 18:35:12 -0400 Subject: - embedding an existing predicate into a new one only seems to be used by test_oracle->test_coerce_to_unicode(). The predicate here should treat as a lambda based on enabled_for_config. not sure why this test is not failing on jenkins --- lib/sqlalchemy/testing/exclusions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/exclusions.py b/lib/sqlalchemy/testing/exclusions.py index 49211f805..f94724608 100644 --- a/lib/sqlalchemy/testing/exclusions.py +++ b/lib/sqlalchemy/testing/exclusions.py @@ -178,8 +178,7 @@ class Predicate(object): @classmethod def as_predicate(cls, predicate, description=None): if isinstance(predicate, compound): - return cls.as_predicate(predicate.fails.union(predicate.skips)) - + return cls.as_predicate(predicate.enabled_for_config, description) elif isinstance(predicate, Predicate): if description and predicate.description is None: predicate.description = description -- cgit v1.2.1 From 216b88894d95c17a1bd18b9d574e96530fb6f1cb Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 11 Oct 2014 19:02:32 -0400 Subject: add more order by here --- lib/sqlalchemy/testing/suite/test_insert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/testing') diff --git a/lib/sqlalchemy/testing/suite/test_insert.py b/lib/sqlalchemy/testing/suite/test_insert.py index 2334d3049..38519dfb9 100644 --- a/lib/sqlalchemy/testing/suite/test_insert.py +++ b/lib/sqlalchemy/testing/suite/test_insert.py @@ -187,7 +187,7 @@ class InsertBehaviorTest(fixtures.TablesTest): eq_( config.db.execute( - select([table]).order_by(table.c.data) + select([table]).order_by(table.c.data, table.c.id) ).fetchall(), [(1, 'data1', 5, 4), (2, 'data2', 5, 4), (7, 'data2', 5, 4), (3, 'data3', 5, 4), (8, 'data3', 5, 4)] -- cgit v1.2.1