diff options
Diffstat (limited to 'doc/build')
-rw-r--r-- | doc/build/changelog/migration_14.rst | 151 | ||||
-rw-r--r-- | doc/build/changelog/unreleased_14/5526.rst | 12 | ||||
-rw-r--r-- | doc/build/conf.py | 2 | ||||
-rw-r--r-- | doc/build/core/engines.rst | 66 | ||||
-rw-r--r-- | doc/build/core/internals.rst | 6 |
5 files changed, 232 insertions, 5 deletions
diff --git a/doc/build/changelog/migration_14.rst b/doc/build/changelog/migration_14.rst index 14584fd43..e21c53ef6 100644 --- a/doc/build/changelog/migration_14.rst +++ b/doc/build/changelog/migration_14.rst @@ -577,6 +577,157 @@ producing:: SELECT address.email_address, user.name FROM user JOIN address ON user.id == address.user_id +.. _change_5526: + +The URL object is now immutable +------------------------------- + +The :class:`_engine.URL` object has been formalized such that it now presents +itself as a ``namedtuple`` with a fixed number of fields that are immutable. In +addition, the dictionary represented by the :attr:`_engine.URL.query` attribute +is also an immutable mapping. Mutation of the :class:`_engine.URL` object was +not a formally supported or documented use case which led to some open-ended +use cases that made it very difficult to intercept incorrect usages, most +commonly mutation of the :attr:`_engine.URL.query` dictionary to include non-string elements. +It also led to all the common problems of allowing mutability in a fundamental +data object, namely unwanted mutations elsewhere leaking into code that didn't +expect the URL to change. Finally, the namedtuple design is inspired by that +of Python's ``urllib.parse.urlparse()`` which returns the parsed object as a +named tuple. + +The decision to change the API outright is based on a calculus weighing the +infeasability of a deprecation path (which would involve changing the +:attr:`_engine.URL.query` dictionary to be a special dictionary that emits deprecation +warnings when any kind of standard library mutation methods are invoked, in +addition that when the dictionary would hold any kind of list of elements, the +list would also have to emit deprecation warnings on mutation) against the +unlikely use case of projects already mutating :class:`_engine.URL` objects in +the first place, as well as that small changes such as that of :ticket:`5341` +were creating backwards-incompatibility in any case. The primary case for +mutation of a +:class:`_engine.URL` object is that of parsing plugin arguments within the +:class:`_engine.CreateEnginePlugin` extension point, itself a fairly recent +addition that based on Github code search is in use by two repositories, +neither of which are actually mutating the URL object. + +The :class:`_engine.URL` object now provides a rich interface inspecting +and generating new :class:`_engine.URL` objects. The +existing mechanism to create a :class:`_engine.URL` object, the +:func:`_engine.make_url` function, remains unchanged:: + + >>> from sqlalchemy.engine import make_url + >>> url = make_url("postgresql+psycopg2://user:pass@host/dbname") + +For programmatic construction, code that may have been using the +:class:`_engine.URL` constructor or ``__init__`` method directly will +receive a deprecation warning if arguments are passed as keyword arguments +and not an exact 7-tuple. The keyword-style constructor is now available +via the :meth:`_engine.URL.create` method:: + + >>> from sqlalchemy.engine import URL + >>> url = URL.create("postgresql", "user", "pass", host="host", database="dbname") + >>> str(url) + 'postgresql://user:pass@host/dbname' + + +Fields can be altered typically using the :meth:`_engine.URL.set` method, which +returns a new :class:`_engine.URL` object with changes applied:: + + >>> mysql_url = url.set(drivername="mysql+pymysql") + >>> str(mysql_url) + 'mysql+pymysql://user:pass@host/dbname' + +To alter the contents of the :attr:`_engine.URL.query` dictionary, methods +such as :meth:`_engine.URL.update_query_dict` may be used:: + + >>> url.update_query_dict({"sslcert": '/path/to/crt'}) + postgresql://user:***@host/dbname?sslcert=%2Fpath%2Fto%2Fcrt + +To upgrade code that is mutating these fields directly, a **backwards and +forwards compatible approach** is to use a duck-typing, as in the following +style:: + + def set_url_drivername(some_url, some_drivername): + # check for 1.4 + if hasattr(some_url, "set"): + return some_url.set(drivername=some_drivername) + else: + # SQLAlchemy 1.3 or earlier, mutate in place + some_url.drivername = some_drivername + return some_url + + def set_ssl_cert(some_url, ssl_cert): + # check for 1.4 + if hasattr(some_url, "update_query_dict"): + return some_url.update_query_dict({"sslcert": ssl_cert}) + else: + # SQLAlchemy 1.3 or earlier, mutate in place + some_url.query["sslcert"] = ssl_cert + return some_url + +The query string retains its existing format as a dictionary of strings +to strings, using sequences of strings to represent multiple parameters. +For example:: + + >>> from sqlalchemy.engine import make_url + >>> url = make_url("postgresql://user:pass@host/dbname?alt_host=host1&alt_host=host2&sslcert=%2Fpath%2Fto%2Fcrt") + >>> url.query + immutabledict({'alt_host': ('host1', 'host2'), 'sslcert': '/path/to/crt'}) + +To work with the contents of the :attr:`_engine.URL.query` attribute such that all values are +normalized into sequences, use the :attr:`_engine.URL.normalized_query` attribute:: + + >>> url.normalized_query + immutabledict({'alt_host': ('host1', 'host2'), 'sslcert': ('/path/to/crt',)}) + +The query string can be appended to via methods such as :meth:`_engine.URL.update_query_dict`, +:meth:`_engine.URL.update_query_pairs`, :meth:`_engine.URL.update_query_string`:: + + >>> url.update_query_dict({"alt_host": "host3"}, append=True) + postgresql://user:***@host/dbname?alt_host=host1&alt_host=host2&alt_host=host3&sslcert=%2Fpath%2Fto%2Fcrt + +.. seealso:: + + :class:`_engine.URL` + + +Changes to CreateEnginePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :class:`_engine.CreateEnginePlugin` is also impacted by this change, +as the documentation for custom plugins indicated that the ``dict.pop()`` +method should be used to remove consumed arguments from the URL object. This +should now be acheived using the :meth:`_engine.CreateEnginePlugin.update_url` +method. A backwards compatible approach would look like:: + + from sqlalchemy.engine import CreateEnginePlugin + + class MyPlugin(CreateEnginePlugin): + def __init__(self, url, kwargs): + # check for 1.4 style + if hasattr(CreateEnginePlugin, "update_url"): + self.my_argument_one = url.query['my_argument_one'] + self.my_argument_two = url.query['my_argument_two'] + else: + # legacy + self.my_argument_one = url.query.pop('my_argument_one') + self.my_argument_two = url.query.pop('my_argument_two') + + self.my_argument_three = kwargs.pop('my_argument_three', None) + + def update_url(self, url): + # this method runs in 1.4 only and should be used to consume + # plugin-specific arguments + return url.difference_update_query( + ["my_argument_one", "my_argument_two"] + ) + +See the docstring at :class:`_engine.CreateEnginePlugin` for complete details +on how this class is used. + +:ticket:`5526` + + .. _change_5284: select(), case() now accept positional expressions diff --git a/doc/build/changelog/unreleased_14/5526.rst b/doc/build/changelog/unreleased_14/5526.rst new file mode 100644 index 000000000..34793565f --- /dev/null +++ b/doc/build/changelog/unreleased_14/5526.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: change, engine + :tickets: 5526 + + The :class:`_engine.URL` object is now an immutable named tuple. To modify + a URL object, use the :meth:`_engine.URL.set` method to produce a new URL + object. + + .. seealso:: + + :ref:`change_5526` - notes on migration + diff --git a/doc/build/conf.py b/doc/build/conf.py index d4fdf58a0..857bec649 100644 --- a/doc/build/conf.py +++ b/doc/build/conf.py @@ -103,6 +103,7 @@ autodocmods_convert_modname = { "sqlalchemy.sql.base": "sqlalchemy.sql.expression", "sqlalchemy.event.base": "sqlalchemy.event", "sqlalchemy.engine.base": "sqlalchemy.engine", + "sqlalchemy.engine.url": "sqlalchemy.engine", "sqlalchemy.engine.row": "sqlalchemy.engine", "sqlalchemy.engine.cursor": "sqlalchemy.engine", "sqlalchemy.engine.result": "sqlalchemy.engine", @@ -127,6 +128,7 @@ autodocmods_convert_modname_w_class = { zzzeeksphinx_module_prefixes = { "_sa": "sqlalchemy", "_engine": "sqlalchemy.engine", + "_url": "sqlalchemy.engine", "_result": "sqlalchemy.engine", "_row": "sqlalchemy.engine", "_schema": "sqlalchemy.schema", diff --git a/doc/build/core/engines.rst b/doc/build/core/engines.rst index 2404414d1..02495404c 100644 --- a/doc/build/core/engines.rst +++ b/doc/build/core/engines.rst @@ -194,12 +194,74 @@ Engine Creation API .. autofunction:: sqlalchemy.create_mock_engine -.. autofunction:: sqlalchemy.engine.url.make_url +.. autofunction:: sqlalchemy.engine.make_url -.. autoclass:: sqlalchemy.engine.url.URL +.. autoclass:: sqlalchemy.engine.URL :members: + .. py:attribute:: drivername + :annotation: str + + database backend and driver name, such as + ``postgresql+psycopg2`` + + .. py:attribute:: username + :annotation: str + + username string + + .. py:attribute:: password + :annotation: str + + password, which is normally a string but may also be any + object that has a ``__str__()`` method. + + .. py:attribute:: host + :annotation: str + + string hostname + + .. py:attribute:: port + :annotation: int + + integer port number + + .. py:attribute:: database + :annotation: str + + string database name + + .. py:attribute:: query + :annotation: Mapping[str, Union[str, Sequence[str]]] + + an immutable mapping representing the query string. contains strings + for keys and either strings or tuples of strings for values, e.g.:: + + >>> from sqlalchemy.engine import make_url + >>> url = make_url("postgresql://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt") + >>> url.query + immutabledict({'alt_host': ('host1', 'host2'), 'ssl_cipher': '/path/to/crt'}) + + To create a mutable copy of this mapping, use the ``dict`` constructor:: + + mutable_query_opts = dict(url.query) + + .. seealso:: + + :attr:`_engine.URL.normalized_query` - normalizes all values into sequences + for consistent processing + + Methods for altering the contents of :attr:`_engine.URL.query`: + + :meth:`_engine.URL.update_query_dict` + + :meth:`_engine.URL.update_query_string` + + :meth:`_engine.URL.update_query_pairs` + + :meth:`_engine.URL.difference_update_query` + Pooling ======= diff --git a/doc/build/core/internals.rst b/doc/build/core/internals.rst index 965c03fd9..34bf0407c 100644 --- a/doc/build/core/internals.rst +++ b/doc/build/core/internals.rst @@ -7,7 +7,7 @@ Some key internal constructs are listed here. .. currentmodule:: sqlalchemy -.. autoclass:: sqlalchemy.engine.interfaces.Compiled +.. autoclass:: sqlalchemy.engine.Compiled :members: .. autoclass:: sqlalchemy.sql.compiler.DDLCompiler @@ -18,14 +18,14 @@ Some key internal constructs are listed here. :members: :inherited-members: -.. autoclass:: sqlalchemy.engine.interfaces.Dialect +.. autoclass:: sqlalchemy.engine.Dialect :members: .. autoclass:: sqlalchemy.engine.default.DefaultExecutionContext :members: -.. autoclass:: sqlalchemy.engine.interfaces.ExecutionContext +.. autoclass:: sqlalchemy.engine.ExecutionContext :members: |