From: Mike Bayer Date: Mon, 4 Dec 2017 16:56:14 +0000 (-0500) Subject: Allow url.password to be an object X-Git-Tag: rel_1_2_0~19 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e6438cf8c3d2200262815840ac597d3f4a22e944;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Allow url.password to be an object The "password" attribute of the :class:`.url.URL` object can now be any user-defined or user-subclassed string object that responds to the Python ``str()`` builtin. The object passed will be maintained as the datamember :attr:`.url.URL.password_original` and will be consulted when the :attr:`.url.URL.password` attribute is read to produce the string value. Change-Id: I91d101c3b10e135ae7e4de60a5104b51776db84f Fixes: #4089 --- diff --git a/doc/build/changelog/unreleased_12/4089.rst b/doc/build/changelog/unreleased_12/4089.rst new file mode 100644 index 0000000000..96d8120bc5 --- /dev/null +++ b/doc/build/changelog/unreleased_12/4089.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: feature, engine + :tickets: 4089 + + The "password" attribute of the :class:`.url.URL` object can now be + any user-defined or user-subclassed string object that responds to the + Python ``str()`` builtin. The object passed will be maintained as the + datamember :attr:`.url.URL.password_original` and will be consulted + when the :attr:`.url.URL.password` attribute is read to produce the + string value. \ No newline at end of file diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 1ca5983fd5..18b184878b 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -54,7 +54,7 @@ class URL(object): host=None, port=None, database=None, query=None): self.drivername = drivername self.username = username - self.password = password + self.password_original = password self.host = host if port is not None: self.port = int(port) @@ -105,6 +105,17 @@ class URL(object): self.database == other.database and \ self.query == other.query + @property + def password(self): + if self.password_original is None: + return None + else: + return util.text_type(self.password_original) + + @password.setter + def password(self, password): + self.password_original = password + def get_backend_name(self): if '+' not in self.drivername: return self.drivername diff --git a/test/engine/test_parseconnect.py b/test/engine/test_parseconnect.py index 07fedbe311..deb4e3c49b 100644 --- a/test/engine/test_parseconnect.py +++ b/test/engine/test_parseconnect.py @@ -1,4 +1,4 @@ -from sqlalchemy.testing import assert_raises, eq_, assert_raises_message +from sqlalchemy.testing import assert_raises, eq_, is_ import sqlalchemy.engine.url as url from sqlalchemy import create_engine, engine_from_config, exc, pool from sqlalchemy.engine.default import DefaultDialect @@ -82,6 +82,39 @@ class ParseConnectTest(fixtures.TestBase): eq_(u.password, 'pass/word') eq_(str(u), 'dbtype://username:pass%2Fword@hostspec/database') + def test_password_custom_obj(self): + class SecurePassword(str): + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value + + sp = SecurePassword("secured_password") + u = url.URL( + "dbtype", + username="x", password=sp, + host="localhost" + ) + + eq_(u.password, "secured_password") + eq_(str(u), "dbtype://x:secured_password@localhost") + + # test in-place modification + sp.value = "new_secured_password" + eq_(u.password, "new_secured_password") + eq_(str(u), "dbtype://x:new_secured_password@localhost") + + u.password = "hi" + + eq_(u.password, "hi") + eq_(str(u), "dbtype://x:hi@localhost") + + u.password = None + + is_(u.password, None) + eq_(str(u), "dbtype://x@localhost") + class DialectImportTest(fixtures.TestBase): def test_import_base_dialects(self):