]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Allow multiple plugin names
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 13 Feb 2018 20:11:53 +0000 (15:11 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 13 Feb 2018 20:28:11 +0000 (15:28 -0500)
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
doc/build/changelog/unreleased_12/4170.rst [new file with mode: 0644]
lib/sqlalchemy/engine/__init__.py
lib/sqlalchemy/engine/interfaces.py
lib/sqlalchemy/engine/strategies.py
lib/sqlalchemy/engine/url.py
test/engine/test_parseconnect.py

diff --git a/doc/build/changelog/unreleased_12/4170.rst b/doc/build/changelog/unreleased_12/4170.rst
new file mode 100644 (file)
index 0000000..abc9917
--- /dev/null
@@ -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.
index 7ecc46a92f938f28351b9c63a4d80bc2c29dc060..841dc405e7090611a952dd951641f9a6e29bcf48 100644 (file)
@@ -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:
 
index b9efce92ee5cffbef8f4e725cbdce9b9ab952ae0..9c3b24e9a8e72fc7f96295262249db8b46ee4513 100644 (file)
@@ -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::
index aff1dd360a6ed09c1a9c7b932bcc4235556f8872..4b6ee77fdc89f15db5c7d06e01777fc708d5077b 100644 (file)
@@ -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)
index 483d40703cac23c45290248771145cdbfdd60914..1662efe2099f609a79247b848f554bd7c4f48e09 100644 (file)
@@ -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
index deb4e3c49b0025000129c6c5b74c0639c506ad49..3e3ba7b0359b58fbe11cb8c2131d8851170a2218 100644 (file)
@@ -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