From 924cc31975f8874d369db6599575e361bdb34be9 Mon Sep 17 00:00:00 2001 From: Tom Ritchford Date: Sun, 5 Dec 2021 13:27:45 -0500 Subject: Add __copy__, __deepcopy__ to URL. Fixes: #7400 Added support for ``copy()`` and ``deepcopy()`` to the :class:`_url.URL` class. Pull request courtesy Tom Ritchford. Fixes: #7400 Closes: #7401 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/7401 Pull-request-sha: a2c1b8992f5d153c6210178cda47b8ae96b91fb5 Change-Id: I55977338b2655a7d4f733ae786d31e589185e9ca --- doc/build/changelog/unreleased_14/7400.rst | 6 ++++++ lib/sqlalchemy/engine/url.py | 16 +++++++++++++++ test/engine/test_parseconnect.py | 32 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 doc/build/changelog/unreleased_14/7400.rst diff --git a/doc/build/changelog/unreleased_14/7400.rst b/doc/build/changelog/unreleased_14/7400.rst new file mode 100644 index 000000000..799b3b9a3 --- /dev/null +++ b/doc/build/changelog/unreleased_14/7400.rst @@ -0,0 +1,6 @@ +.. change:: + :tags: usecase, engine + :tickets: 7400 + + Added support for ``copy()`` and ``deepcopy()`` to the :class:`_url.URL` + class. Pull request courtesy Tom Ritchford. diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 7cdf25c21..778d2112f 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -560,6 +560,22 @@ class URL( def __repr__(self): return self.render_as_string() + def __copy__(self): + return self.__class__.create( + self.drivername, + self.username, + self.password, + self.host, + self.port, + self.database, + # note this is an immutabledict of str-> str / tuple of str, + # also fully immutable. does not require deepcopy + self.query, + ) + + def __deepcopy__(self, memo): + return self.__copy__() + def __hash__(self): return hash(str(self)) diff --git a/test/engine/test_parseconnect.py b/test/engine/test_parseconnect.py index f12d32d5d..19fce5c18 100644 --- a/test/engine/test_parseconnect.py +++ b/test/engine/test_parseconnect.py @@ -1,3 +1,4 @@ +import copy from unittest.mock import call from unittest.mock import MagicMock from unittest.mock import Mock @@ -18,6 +19,7 @@ from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_ from sqlalchemy.testing import is_false +from sqlalchemy.testing import is_not from sqlalchemy.testing import is_true from sqlalchemy.testing import mock from sqlalchemy.testing import ne_ @@ -196,6 +198,25 @@ class URLTest(fixtures.TestBase): is_true(url1 != url3) is_false(url1 == url3) + def test_copy(self): + url1 = url.make_url( + "dialect://user:pass@host/db?arg1%3D=param1&arg2=param+2" + ) + url2 = copy.copy(url1) + eq_(url1, url2) + is_not(url1, url2) + + def test_deepcopy(self): + url1 = url.make_url( + "dialect://user:pass@host/db?arg1%3D=param1&arg2=param+2" + ) + url2 = copy.deepcopy(url1) + eq_(url1, url2) + is_not(url1, url2) + is_not(url1.query, url2.query) # immutabledict of immutable k/v, + # but it copies it on constructor + # in any case if params are present + @testing.combinations( "drivername", "username", @@ -242,6 +263,17 @@ class URLTest(fixtures.TestBase): url.make_url("drivername:///?%s" % expected), ) + @testing.combinations( + "drivername://", + "drivername://?foo=bar", + "drivername://?foo=bar&foo=bat", + ) + def test_query_dict_immutable(self, urlstr): + url_obj = url.make_url(urlstr) + + with expect_raises_message(TypeError, ".*immutable"): + url_obj.query["foo"] = "hoho" + @testing.combinations( ( "foo1=bar1&foo2=bar2", -- cgit v1.2.1