--- /dev/null
+.. change::
+ :tags: bug, engine
+ :tickets: 6958
+
+ Ensure that ``str()`` is called on the an ``URL.password`` argument,
+ allowing usage of objects that implement the ``__str__()`` method
+ as password attributes.
+ Also clarified that one such object is not appropriate to dynamically
+ change the password.
def do_connect(self, dialect, conn_rec, cargs, cparams):
"""Receive connection arguments before a connection is made.
- Return a DBAPI connection to halt further events from invoking;
- the returned connection will be used.
-
- Alternatively, the event can manipulate the cargs and/or cparams
- collections; cargs will always be a Python list that can be mutated
- in-place and cparams a Python dictionary. Return None to
- allow control to pass to the next event handler and ultimately
- to allow the dialect to connect normally, given the updated
- arguments.
+ This event is useful in that it allows the handler to manipulate the
+ cargs and/or cparams collections that control how the DBAPI
+ ``connect()`` function will be called. ``cargs`` will always be a
+ Python list that can be mutated in-place, and ``cparams`` a Python
+ dictionary that may also be mutated::
+
+ e = create_engine("postgresql+psycopg2://user@host/dbname")
+
+ @event.listens_for(e, 'do_connect')
+ def receive_do_connect(dialect, conn_rec, cargs, cparams):
+ cparams["password"] = "some_password"
+
+ The event hook may also be used to override the call to ``connect()``
+ entirely, by returning a non-``None`` DBAPI connection object::
+
+ e = create_engine("postgresql+psycopg2://user@host/dbname")
+
+ @event.listens_for(e, 'do_connect')
+ def receive_do_connect(dialect, conn_rec, cargs, cparams):
+ return psycopg2.connect(*cargs, **cparams)
+
.. versionadded:: 1.0.3
* :attr:`_engine.URL.drivername`: database backend and driver name, such as
``postgresql+psycopg2``
* :attr:`_engine.URL.username`: username string
- * :attr:`_engine.URL.password`: password, which is normally a string but
- may also be any object that has a ``__str__()`` method.
+ * :attr:`_engine.URL.password`: password string, or object that includes
+ a ``__str__()`` method that produces a password.
+
+ .. note:: A password-producing object will be stringified only
+ **once** per :class:`_engine.Engine` object. For dynamic password
+ generation per connect, see :ref:`engines_dynamic_tokens`.
+
* :attr:`_engine.URL.host`: string hostname
* :attr:`_engine.URL.port`: integer port number
* :attr:`_engine.URL.database`: string database name
correspond to a module in sqlalchemy/databases or a third party
plug-in.
:param username: The user name.
- :param password: database password. May be a string or an object that
- can be stringified with ``str()``.
+ :param password: database password. Is typically a string, but may
+ also be an object that can be stringified with ``str()``.
+
+ .. note:: A password-producing object will be stringified only
+ **once** per :class:`_engine.Engine` object. For dynamic password
+ generation per connect, see :ref:`engines_dynamic_tokens`.
+
:param host: The name of the host.
:param port: The port number.
:param database: The database name.
names, but correlates the name to the original positionally.
"""
+ if names is not None:
+ util.warn_deprecated(
+ "The `URL.translate_connect_args.name`s parameter is "
+ "deprecated. Please pass the "
+ "alternate names as kw arguments.",
+ "1.4",
+ )
+
translated = {}
attribute_names = ["host", "database", "username", "password", "port"]
for sname in attribute_names:
else:
name = sname
if name is not None and getattr(self, sname, False):
- translated[name] = getattr(self, sname)
+ if sname == "password":
+ translated[name] = str(getattr(self, sname))
+ else:
+ translated[name] = getattr(self, sname)
+
return translated
)
eq_(u1, url.make_url("somedriver://user@hostname:52"))
+ def test_deprecated_translate_connect_args_names(self):
+ u = url.make_url("somedriver://user@hostname:52")
+
+ with testing.expect_deprecated(
+ "The `URL.translate_connect_args.name`s parameter is "
+ ):
+ res = u.translate_connect_args(["foo"])
+ is_true("foo" in res)
+ eq_(res["foo"], u.host)
+
class DialectImportTest(fixtures.TestBase):
def test_import_base_dialects(self):
_initialize=False,
)
+ @testing.combinations(True, False)
+ def test_password_object_str(self, creator):
+ class SecurePassword:
+ def __init__(self, value):
+ self.called = 0
+ self.value = value
+
+ def __str__(self):
+ self.called += 1
+ return self.value
+
+ sp = SecurePassword("secured_password")
+ u = url.URL.create(
+ "postgresql", username="x", password=sp, host="localhost"
+ )
+ if not creator:
+ dbapi = MockDBAPI(
+ user="x", password="secured_password", host="localhost"
+ )
+
+ e = create_engine(u, module=dbapi, _initialize=False)
+
+ else:
+ dbapi = MockDBAPI(foober=12, lala=18, fooz="somevalue")
+
+ def connect():
+ return dbapi.connect(foober=12, lala=18, fooz="somevalue")
+
+ e = create_engine(
+ u, creator=connect, module=dbapi, _initialize=False
+ )
+ e.connect()
+ e.connect()
+ e.connect()
+ e.connect()
+ eq_(sp.called, 1)
+
class TestRegNewDBAPI(fixtures.TestBase):
def test_register_base(self):