From: Mike Bayer Date: Tue, 13 Feb 2018 20:11:53 +0000 (-0500) Subject: Allow multiple plugin names X-Git-Tag: rel_1_2_3~9 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=15ea75981305fdad8286f6803671b864ccda13f2;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Allow multiple plugin names The :class:`.URL` object now allows query keys to be specified multiple times where their values will be joined into a list. This is to support the plugins feature documented at :class:`.CreateEnginePlugin` which documents that "plugin" can be passed multiple times. Additionally, the plugin names can be passed to :func:`.create_engine` outside of the URL using the new :paramref:`.create_engine.plugins` parameter. Change-Id: Ifc48ad120bd6c6204eda567492caf79832aeeaa5 Fixes: #4170 --- diff --git a/doc/build/changelog/unreleased_12/4170.rst b/doc/build/changelog/unreleased_12/4170.rst new file mode 100644 index 0000000000..abc9917a6d --- /dev/null +++ b/doc/build/changelog/unreleased_12/4170.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, engine + :tickets: 4170 + + The :class:`.URL` object now allows query keys to be specified multiple + times where their values will be joined into a list. This is to support + the plugins feature documented at :class:`.CreateEnginePlugin` which + documents that "plugin" can be passed multiple times. Additionally, the + plugin names can be passed to :func:`.create_engine` outside of the URL + using the new :paramref:`.create_engine.plugins` parameter. diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 7ecc46a92f..841dc405e7 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -398,6 +398,11 @@ def create_engine(*args, **kwargs): up on getting a connection from the pool. This is only used with :class:`~sqlalchemy.pool.QueuePool`. + :param plugins: string list of plugin names to load. See + :class:`.CreateEnginePlugin` for background. + + .. versionadded:: 1.2.3 + :param strategy='plain': selects alternate engine implementations. Currently available are: diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index b9efce92ee..9c3b24e9a8 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -844,6 +844,16 @@ class CreateEnginePlugin(object): engine = create_engine( "mysql+pymysql://scott:tiger@localhost/test?plugin=myplugin") + Alternatively, the :paramref:`.create_engine.plugins" argument may be + passed as a list to :func:`.create_engine`:: + + engine = create_engine( + "mysql+pymysql://scott:tiger@localhost/test", + plugins=["myplugin"]) + + .. versionadded:: 1.2.3 plugin names can also be specified + to :func:`.create_engine` as a list + The ``plugin`` argument supports multiple instances, so that a URL may specify multiple plugins; they are loaded in the order stated in the URL:: diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index aff1dd360a..4b6ee77fdc 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -52,6 +52,7 @@ class DefaultEngineStrategy(EngineStrategy): plugins = u._instantiate_plugins(kwargs) u.query.pop('plugin', None) + kwargs.pop('plugins', None) entrypoint = u._get_entrypoint() dialect_cls = entrypoint.get_dialect_cls(u) diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 483d40703c..1662efe209 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -83,7 +83,12 @@ class URL(object): if self.query: keys = list(self.query) keys.sort() - s += '?' + "&".join("%s=%s" % (k, self.query[k]) for k in keys) + s += '?' + "&".join( + "%s=%s" % ( + k, + element + ) for k in keys for element in util.to_list(self.query[k]) + ) return s def __str__(self): @@ -130,6 +135,7 @@ class URL(object): def _instantiate_plugins(self, kwargs): plugin_names = util.to_list(self.query.get('plugin', ())) + plugin_names += kwargs.get('plugins', []) return [ plugins.load(plugin_name)(self, kwargs) @@ -230,10 +236,20 @@ def _parse_rfc1738_args(name): if components['database'] is not None: tokens = components['database'].split('?', 2) components['database'] = tokens[0] - query = ( - len(tokens) > 1 and dict(util.parse_qsl(tokens[1]))) or None - if util.py2k and query is not None: - query = {k.encode('ascii'): query[k] for k in query} + + if len(tokens) > 1: + query = {} + + for key, value in util.parse_qsl(tokens[1]): + if util.py2k: + key = key.encode('ascii') + if key in query: + query[key] = util.to_list(query[key]) + query[key].append(value) + else: + query[key] = value + else: + query = None else: query = None components['query'] = query diff --git a/test/engine/test_parseconnect.py b/test/engine/test_parseconnect.py index deb4e3c49b..3e3ba7b035 100644 --- a/test/engine/test_parseconnect.py +++ b/test/engine/test_parseconnect.py @@ -31,6 +31,7 @@ class ParseConnectTest(fixtures.TestBase): 'dbtype:///foo/bar/im/a/file', 'dbtype:///E:/work/src/LEM/db/hello.db', 'dbtype:///E:/work/src/LEM/db/hello.db?foo=bar&hoho=lala', + 'dbtype:///E:/work/src/LEM/db/hello.db?foo=bar&hoho=lala&hoho=bat', 'dbtype://', 'dbtype://username:password@/database', 'dbtype:////usr/local/_xtest@example.com/members.db', @@ -115,6 +116,20 @@ class ParseConnectTest(fixtures.TestBase): is_(u.password, None) eq_(str(u), "dbtype://x@localhost") + def test_query_string(self): + u = url.make_url( + "dialect://user:pass@host/db?arg1=param1&arg2=param2") + eq_(u.query, {"arg1": "param1", "arg2": "param2"}) + eq_(str(u), "dialect://user:pass@host/db?arg1=param1&arg2=param2") + + u = url.make_url( + "dialect://user:pass@host/db?arg1=param1&arg2=param2&arg2=param3") + eq_(u.query, {"arg1": "param1", "arg2": ["param2", "param3"]}) + eq_( + str(u), + "dialect://user:pass@host/db?arg1=param1&arg2=param2&arg2=param3") + + class DialectImportTest(fixtures.TestBase): def test_import_base_dialects(self): @@ -413,14 +428,19 @@ class TestRegNewDBAPI(fixtures.TestBase): ) @testing.requires.sqlite - def test_plugin_registration(self): + def test_plugin_url_registration(self): from sqlalchemy.dialects import sqlite global MyEnginePlugin def side_effect(url, kw): + eq_( + url.query, + {"plugin": "engineplugin", "myplugin_arg": "bat", "foo": "bar"} + ) eq_(kw, {"logging_name": "foob"}) kw['logging_name'] = 'bar' + url.query.pop('myplugin_arg', None) return MyEnginePlugin MyEnginePlugin = Mock(side_effect=side_effect) @@ -428,25 +448,129 @@ class TestRegNewDBAPI(fixtures.TestBase): plugins.register("engineplugin", __name__, "MyEnginePlugin") e = create_engine( - "sqlite:///?plugin=engineplugin&foo=bar", logging_name='foob') + "sqlite:///?plugin=engineplugin&foo=bar&myplugin_arg=bat", + logging_name='foob') eq_(e.dialect.name, "sqlite") eq_(e.logging_name, "bar") + + # plugin args are removed from URL. + eq_( + e.url.query, + {"foo": "bar"} + ) assert isinstance(e.dialect, sqlite.dialect) eq_( MyEnginePlugin.mock_calls, [ - call(e.url, {}), + call(url.make_url("sqlite:///?foo=bar"), {}), call.handle_dialect_kwargs(sqlite.dialect, mock.ANY), call.handle_pool_kwargs(mock.ANY, {"dialect": e.dialect}), call.engine_created(e) ] ) + + # url was modified in place by MyEnginePlugin eq_( str(MyEnginePlugin.mock_calls[0][1][0]), "sqlite:///?foo=bar" ) + @testing.requires.sqlite + def test_plugin_multiple_url_registration(self): + from sqlalchemy.dialects import sqlite + + global MyEnginePlugin1 + global MyEnginePlugin2 + + def side_effect_1(url, kw): + eq_(kw, {"logging_name": "foob"}) + kw['logging_name'] = 'bar' + url.query.pop('myplugin1_arg', None) + return MyEnginePlugin1 + + def side_effect_2(url, kw): + url.query.pop('myplugin2_arg', None) + return MyEnginePlugin2 + + MyEnginePlugin1 = Mock(side_effect=side_effect_1) + MyEnginePlugin2 = Mock(side_effect=side_effect_2) + + plugins.register("engineplugin1", __name__, "MyEnginePlugin1") + plugins.register("engineplugin2", __name__, "MyEnginePlugin2") + + e = create_engine( + "sqlite:///?plugin=engineplugin1&foo=bar&myplugin1_arg=bat" + "&plugin=engineplugin2&myplugin2_arg=hoho", + logging_name='foob') + eq_(e.dialect.name, "sqlite") + eq_(e.logging_name, "bar") + + # plugin args are removed from URL. + eq_( + e.url.query, + {"foo": "bar"} + ) + assert isinstance(e.dialect, sqlite.dialect) + + eq_( + MyEnginePlugin1.mock_calls, + [ + call(url.make_url("sqlite:///?foo=bar"), {}), + call.handle_dialect_kwargs(sqlite.dialect, mock.ANY), + call.handle_pool_kwargs(mock.ANY, {"dialect": e.dialect}), + call.engine_created(e) + ] + ) + + eq_( + MyEnginePlugin2.mock_calls, + [ + call(url.make_url("sqlite:///?foo=bar"), {}), + call.handle_dialect_kwargs(sqlite.dialect, mock.ANY), + call.handle_pool_kwargs(mock.ANY, {"dialect": e.dialect}), + call.engine_created(e) + ] + ) + + @testing.requires.sqlite + def test_plugin_arg_registration(self): + from sqlalchemy.dialects import sqlite + + global MyEnginePlugin + + def side_effect(url, kw): + eq_( + kw, + {"logging_name": "foob", 'plugins': ['engineplugin'], + 'myplugin_arg': 'bat'} + ) + kw['logging_name'] = 'bar' + kw.pop('myplugin_arg', None) + return MyEnginePlugin + + MyEnginePlugin = Mock(side_effect=side_effect) + + plugins.register("engineplugin", __name__, "MyEnginePlugin") + + e = create_engine( + "sqlite:///?foo=bar", + logging_name='foob', plugins=["engineplugin"], myplugin_arg="bat") + eq_(e.dialect.name, "sqlite") + eq_(e.logging_name, "bar") + + assert isinstance(e.dialect, sqlite.dialect) + + eq_( + MyEnginePlugin.mock_calls, + [ + call(url.make_url("sqlite:///?foo=bar"), {}), + call.handle_dialect_kwargs(sqlite.dialect, mock.ANY), + call.handle_pool_kwargs(mock.ANY, {"dialect": e.dialect}), + call.engine_created(e) + ] + ) + class MockDialect(DefaultDialect): @classmethod