diff options
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/dialects/sqlite/provision.py | 66 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/sqlite/pysqlcipher.py | 109 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/create.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/interfaces.py | 71 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/requirements.py | 2 |
5 files changed, 181 insertions, 69 deletions
diff --git a/lib/sqlalchemy/dialects/sqlite/provision.py b/lib/sqlalchemy/dialects/sqlite/provision.py index d0d12695d..e5b17e829 100644 --- a/lib/sqlalchemy/dialects/sqlite/provision.py +++ b/lib/sqlalchemy/dialects/sqlite/provision.py @@ -1,9 +1,12 @@ import os +import re +from ... import exc from ...engine import url as sa_url from ...testing.provision import create_db from ...testing.provision import drop_db from ...testing.provision import follower_url_from_main +from ...testing.provision import generate_driver_url from ...testing.provision import log from ...testing.provision import post_configure_engine from ...testing.provision import run_reap_dbs @@ -11,21 +14,38 @@ from ...testing.provision import stop_test_class_outside_fixtures from ...testing.provision import temp_table_keyword_args -# likely needs a generate_driver_url() def here for the --dbdriver part to -# work +# TODO: I can't get this to build dynamically with pytest-xdist procs +_drivernames = {"pysqlite", "aiosqlite", "pysqlcipher"} -_drivernames = set() + +@generate_driver_url.for_db("sqlite") +def generate_driver_url(url, driver, query_str): + if driver == "pysqlcipher" and url.get_driver_name() != "pysqlcipher": + if url.database: + url = url.set(database=url.database + ".enc") + url = url.set(password="test") + url = url.set(drivername="sqlite+%s" % (driver,)) + try: + url.get_dialect() + except exc.NoSuchModuleError: + return None + else: + return url @follower_url_from_main.for_db("sqlite") def _sqlite_follower_url_from_main(url, ident): url = sa_url.make_url(url) + if not url.database or url.database == ":memory:": return url else: - _drivernames.add(url.get_driver_name()) + + m = re.match(r"(.+?)\.(.+)$", url.database) + name, ext = m.group(1, 2) + drivername = url.get_driver_name() return sa_url.make_url( - "sqlite+%s:///%s.db" % (url.get_driver_name(), ident) + "sqlite+%s:///%s_%s.%s" % (drivername, drivername, ident, ext) ) @@ -81,7 +101,6 @@ def stop_test_class_outside_fixtures(config, db, cls): if files: db.dispose() - # some sqlite file tests are not cleaning up well yet, so do this # just to make things simple for now for file_ in files: @@ -102,19 +121,22 @@ def _reap_sqlite_dbs(url, idents): for ident in idents: # we don't have a config so we can't call _sqlite_drop_db due to the # decorator - for path in ( - [ - "%s.db" % ident, - ] - + [ - "%s_test_schema.db" % (drivername,) - for drivername in _drivernames - ] - + [ - "%s_%s_test_schema.db" % (ident, drivername) - for drivername in _drivernames - ] - ): - if os.path.exists(path): - log.info("deleting SQLite database file: %s" % path) - os.remove(path) + for ext in ("db", "db.enc"): + for path in ( + ["%s.%s" % (ident, ext)] + + [ + "%s_%s.%s" % (drivername, ident, ext) + for drivername in _drivernames + ] + + [ + "%s_test_schema.%s" % (drivername, ext) + for drivername in _drivernames + ] + + [ + "%s_%s_test_schema.%s" % (ident, drivername, ext) + for drivername in _drivernames + ] + ): + if os.path.exists(path): + log.info("deleting SQLite database file: %s" % path) + os.remove(path) diff --git a/lib/sqlalchemy/dialects/sqlite/pysqlcipher.py b/lib/sqlalchemy/dialects/sqlite/pysqlcipher.py index 659043366..8f0f46acb 100644 --- a/lib/sqlalchemy/dialects/sqlite/pysqlcipher.py +++ b/lib/sqlalchemy/dialects/sqlite/pysqlcipher.py @@ -8,32 +8,43 @@ """ .. dialect:: sqlite+pysqlcipher :name: pysqlcipher - :dbapi: pysqlcipher + :dbapi: sqlcipher 3 or pysqlcipher :connectstring: sqlite+pysqlcipher://:passphrase/file_path[?kdf_iter=<iter>] - :url: https://pypi.python.org/pypi/pysqlcipher - ``pysqlcipher`` is a fork of the standard ``pysqlite`` driver to make - use of the `SQLCipher <https://www.zetetic.net/sqlcipher>`_ backend. + Dialect for support of DBAPIs that make use of the + `SQLCipher <https://www.zetetic.net/sqlcipher>`_ backend. - ``pysqlcipher3`` is a fork of ``pysqlcipher`` for Python 3. This dialect - will attempt to import it if ``pysqlcipher`` is non-present. - - .. versionadded:: 1.1.4 - added fallback import for pysqlcipher3 - - .. versionadded:: 0.9.9 - added pysqlcipher dialect Driver ------ -The driver here is the -`pysqlcipher <https://pypi.python.org/pypi/pysqlcipher>`_ -driver, which makes use of the SQLCipher engine. This system essentially +Current dialect selection logic is: + +* If the :paramref:`_sa.create_engine.module` parameter supplies a DBAPI module, + that module is used. +* Otherwise for Python 3, choose https://pypi.org/project/sqlcipher3/ +* If not available, fall back to https://pypi.org/project/pysqlcipher3/ +* For Python 2, https://pypi.org/project/pysqlcipher/ is used. + +.. warning:: The ``pysqlcipher3`` and ``pysqlcipher`` DBAPI drivers are no + longer maintained; the ``sqlcipher3`` driver as of this writing appears + to be current. For future compatibility, any pysqlcipher-compatible DBAPI + may be used as follows:: + + import sqlcipher_compatible_driver + + from sqlalchemy import create_engine + + e = create_engine( + "sqlite+pysqlcipher://:password@/dbname.db", + module=sqlcipher_compatible_driver + ) + +These drivers make use of the SQLCipher engine. This system essentially introduces new PRAGMA commands to SQLite which allows the setting of a -passphrase and other encryption parameters, allowing the database -file to be encrypted. +passphrase and other encryption parameters, allowing the database file to be +encrypted. -`pysqlcipher3` is a fork of `pysqlcipher` with support for Python 3, -the driver is the same. Connect Strings --------------- @@ -82,7 +93,7 @@ from __future__ import absolute_import from .pysqlite import SQLiteDialect_pysqlite from ... import pool -from ...engine import url as _url +from ... import util class SQLiteDialect_pysqlcipher(SQLiteDialect_pysqlite): @@ -92,13 +103,18 @@ class SQLiteDialect_pysqlcipher(SQLiteDialect_pysqlite): @classmethod def dbapi(cls): - try: - from pysqlcipher import dbapi2 as sqlcipher - except ImportError as e: + if util.py3k: try: - from pysqlcipher3 import dbapi2 as sqlcipher + import sqlcipher3 as sqlcipher except ImportError: - raise e + pass + else: + return sqlcipher + + from pysqlcipher3 import dbapi2 as sqlcipher + + else: + from pysqlcipher import dbapi2 as sqlcipher return sqlcipher @@ -106,34 +122,37 @@ class SQLiteDialect_pysqlcipher(SQLiteDialect_pysqlite): def get_pool_class(cls, url): return pool.SingletonThreadPool - def connect(self, *cargs, **cparams): - passphrase = cparams.pop("passphrase", "") + def on_connect_url(self, url): + super_on_connect = super( + SQLiteDialect_pysqlcipher, self + ).on_connect_url(url) - pragmas = dict((key, cparams.pop(key, None)) for key in self.pragmas) + # pull the info we need from the URL early. Even though URL + # is immutable, we don't want any in-place changes to the URL + # to affect things + passphrase = url.password or "" + url_query = dict(url.query) - conn = super(SQLiteDialect_pysqlcipher, self).connect( - *cargs, **cparams - ) - conn.exec_driver_sql('pragma key="%s"' % passphrase) - for prag, value in pragmas.items(): - if value is not None: - conn.exec_driver_sql('pragma %s="%s"' % (prag, value)) + def on_connect(conn): + cursor = conn.cursor() + cursor.execute('pragma key="%s"' % passphrase) + for prag in self.pragmas: + value = url_query.get(prag, None) + if value is not None: + cursor.execute('pragma %s="%s"' % (prag, value)) + cursor.close() - return conn + if super_on_connect: + super_on_connect(conn) + + return on_connect def create_connect_args(self, url): - super_url = _url.URL( - url.drivername, - username=url.username, - host=url.host, - database=url.database, - query=url.query, + plain_url = url._replace(password=None) + plain_url = plain_url.difference_update_query(self.pragmas) + return super(SQLiteDialect_pysqlcipher, self).create_connect_args( + plain_url ) - c_args, opts = super( - SQLiteDialect_pysqlcipher, self - ).create_connect_args(super_url) - opts["passphrase"] = url.password - return c_args, opts dialect = SQLiteDialect_pysqlcipher diff --git a/lib/sqlalchemy/engine/create.py b/lib/sqlalchemy/engine/create.py index 789030f2b..682d0dd5d 100644 --- a/lib/sqlalchemy/engine/create.py +++ b/lib/sqlalchemy/engine/create.py @@ -646,7 +646,7 @@ def create_engine(url, **kwargs): engine = engineclass(pool, dialect, u, **engine_args) if _initialize: - do_on_connect = dialect.on_connect() + do_on_connect = dialect.on_connect_url(url) if do_on_connect: def on_connect(dbapi_connection, connection_record): diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 010abcc24..24e0e5b0d 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -746,6 +746,67 @@ class Dialect(object): """ + def on_connect_url(self, url): + """return a callable which sets up a newly created DBAPI connection. + + This method is a new hook that supersedes the + :meth:`_engine.Dialect.on_connect` method when implemented by a + dialect. When not implemented by a dialect, it invokes the + :meth:`_engine.Dialect.on_connect` method directly to maintain + compatibility with existing dialects. There is no deprecation + for :meth:`_engine.Dialect.on_connect` expected. + + The callable should accept a single argument "conn" which is the + DBAPI connection itself. The inner callable has no + return value. + + E.g.:: + + class MyDialect(default.DefaultDialect): + # ... + + def on_connect_url(self, url): + def do_on_connect(connection): + connection.execute("SET SPECIAL FLAGS etc") + + return do_on_connect + + This is used to set dialect-wide per-connection options such as + isolation modes, Unicode modes, etc. + + This method differs from :meth:`_engine.Dialect.on_connect` in that + it is passed the :class:`_engine.URL` object that's relevant to the + connect args. Normally the only way to get this is from the + :meth:`_engine.Dialect.on_connect` hook is to look on the + :class:`_engine.Engine` itself, however this URL object may have been + replaced by plugins. + + .. note:: + + The default implementation of + :meth:`_engine.Dialect.on_connect_url` is to invoke the + :meth:`_engine.Dialect.on_connect` method. Therefore if a dialect + implements this method, the :meth:`_engine.Dialect.on_connect` + method **will not be called** unless the overriding dialect calls + it directly from here. + + .. versionadded:: 1.4.3 added :meth:`_engine.Dialect.on_connect_url` + which normally calls into :meth:`_engine.Dialect.on_connect`. + + :param url: a :class:`_engine.URL` object representing the + :class:`_engine.URL` that was passed to the + :meth:`_engine.Dialect.create_connect_args` method. + + :return: a callable that accepts a single DBAPI connection as an + argument, or None. + + .. seealso:: + + :meth:`_engine.Dialect.on_connect` + + """ + return self.on_connect() + def on_connect(self): """return a callable which sets up a newly created DBAPI connection. @@ -776,6 +837,12 @@ class Dialect(object): for the first connection of a dialect. The on_connect hook is still called before the :meth:`_engine.Dialect.initialize` method however. + .. versionchanged:: 1.4.3 the on_connect hook is invoked from a new + method on_connect_url that passes the URL that was used to create + the connect args. Dialects can implement on_connect_url instead + of on_connect if they need the URL object that was used for the + connection in order to get additional context. + If None is returned, no event listener is generated. :return: a callable that accepts a single DBAPI connection as an @@ -786,6 +853,10 @@ class Dialect(object): :meth:`.Dialect.connect` - allows the DBAPI ``connect()`` sequence itself to be controlled. + :meth:`.Dialect.on_connect_url` - supersedes + :meth:`.Dialect.on_connect` to also receive the + :class:`_engine.URL` object in context. + """ return None diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index 208ba0091..46844803b 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -18,9 +18,9 @@ to provide specific inclusion/exclusions. import platform import sys -from sqlalchemy.pool.impl import QueuePool from . import exclusions from .. import util +from ..pool import QueuePool class Requirements(object): |
