diff options
| -rw-r--r-- | doc/build/changelog/unreleased_14/sqlite_autocommit.rst | 6 | ||||
| -rw-r--r-- | doc/build/errors.rst | 28 | ||||
| -rw-r--r-- | doc/build/orm/extensions/asyncio.rst | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/sqlite/base.py | 10 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/sqlite/pysqlite.py | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/ext/asyncio/base.py | 5 | ||||
| -rw-r--r-- | lib/sqlalchemy/ext/asyncio/engine.py | 23 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/requirements.py | 4 | ||||
| -rw-r--r-- | test/dialect/test_sqlite.py | 17 | ||||
| -rw-r--r-- | test/ext/asyncio/test_engine_py3k.py | 35 | ||||
| -rw-r--r-- | test/ext/asyncio/test_session_py3k.py | 8 |
11 files changed, 137 insertions, 7 deletions
diff --git a/doc/build/changelog/unreleased_14/sqlite_autocommit.rst b/doc/build/changelog/unreleased_14/sqlite_autocommit.rst new file mode 100644 index 000000000..183e0eeed --- /dev/null +++ b/doc/build/changelog/unreleased_14/sqlite_autocommit.rst @@ -0,0 +1,6 @@ +.. change:: + :tags: bug, sqlite + + Fixed bug where the error message for SQLite invalid isolation level on the + pysqlite driver would fail to indicate that "AUTOCOMMIT" is one of the + valid isolation levels. diff --git a/doc/build/errors.rst b/doc/build/errors.rst index 5081928dd..4058b06ea 100644 --- a/doc/build/errors.rst +++ b/doc/build/errors.rst @@ -1265,6 +1265,34 @@ attempt, which is unsupported when using SQLAlchemy with AsyncIO dialects. :ref:`asyncio_orm_avoid_lazyloads` - covers most ORM scenarios where this problem can occur and how to mitigate. +.. _error_xd3s: + +No Inspection Avaliable +----------------------- + +Using the :func:`_sa.inspect` function directly on an +:class:`_asyncio.AsyncConnection` or :class:`_asyncio.AsyncEngine` object is +not currently supported, as there is not yet an awaitable form of the +:class:`_reflection.Inspector` object available. Instead, the object +is used by acquiring it using the +:func:`_sa.inspect` function in such a way that it refers to the underlying +:attr:`_asyncio.AsyncConnection.sync_connection` attribute of the +:class:`_asyncio.AsyncConnection` object; the :class:`_engine.Inspector` is +then used in a "synchronous" calling style by using the +:meth:`_asyncio.AsyncConnection.run_sync` method along with a custom function +that performs the desired operations:: + + async def async_main(): + async with engine.connect() as conn: + tables = await conn.run_sync( + lambda sync_conn: inspect(sync_conn).get_table_names() + ) + +.. seealso:: + + :ref:`asyncio_inspector` - additional examples of using :func:`_sa.inspect` + with the asyncio extension. + Core Exception Classes ====================== diff --git a/doc/build/orm/extensions/asyncio.rst b/doc/build/orm/extensions/asyncio.rst index 940c19a7e..f3e89c647 100644 --- a/doc/build/orm/extensions/asyncio.rst +++ b/doc/build/orm/extensions/asyncio.rst @@ -498,6 +498,8 @@ the usual ``await`` keywords are necessary, including for the .. currentmodule:: sqlalchemy.ext.asyncio +.. _asyncio_inspector: + Using the Inspector to inspect schema objects --------------------------------------------------- diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index dc5ebc3f0..c4a6bf8e9 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -1915,7 +1915,9 @@ class SQLiteDialect(default.DefaultDialect): 14, ) - _isolation_lookup = {"READ UNCOMMITTED": 1, "SERIALIZABLE": 0} + _isolation_lookup = util.immutabledict( + {"READ UNCOMMITTED": 1, "SERIALIZABLE": 0} + ) def set_isolation_level(self, connection, level): try: @@ -1925,7 +1927,11 @@ class SQLiteDialect(default.DefaultDialect): exc.ArgumentError( "Invalid value '%s' for isolation_level. " "Valid isolation levels for %s are %s" - % (level, self.name, ", ".join(self._isolation_lookup)) + % ( + level, + self.name, + ", ".join(self._isolation_lookup), + ) ), replace_context=err, ) diff --git a/lib/sqlalchemy/dialects/sqlite/pysqlite.py b/lib/sqlalchemy/dialects/sqlite/pysqlite.py index 96a5351da..0f96e8830 100644 --- a/lib/sqlalchemy/dialects/sqlite/pysqlite.py +++ b/lib/sqlalchemy/dialects/sqlite/pysqlite.py @@ -492,6 +492,12 @@ class SQLiteDialect_pysqlite(SQLiteDialect): def _get_server_version_info(self, connection): return self.dbapi.sqlite_version_info + _isolation_lookup = SQLiteDialect._isolation_lookup.union( + { + "AUTOCOMMIT": None, + } + ) + def set_isolation_level(self, connection, level): if hasattr(connection, "connection"): dbapi_connection = connection.connection diff --git a/lib/sqlalchemy/ext/asyncio/base.py b/lib/sqlalchemy/ext/asyncio/base.py index 3f2c084f4..3f77f5500 100644 --- a/lib/sqlalchemy/ext/asyncio/base.py +++ b/lib/sqlalchemy/ext/asyncio/base.py @@ -8,6 +8,7 @@ from . import exc as async_exc class ReversibleProxy: # weakref.ref(async proxy object) -> weakref.ref(sync proxied object) _proxy_objects = {} + __slots__ = ("__weakref__",) def _assign_proxied(self, target): if target is not None: @@ -46,6 +47,8 @@ class ReversibleProxy: class StartableContext(abc.ABC): + __slots__ = () + @abc.abstractmethod async def start(self, is_ctxmanager=False): pass @@ -68,6 +71,8 @@ class StartableContext(abc.ABC): class ProxyComparable(ReversibleProxy): + __slots__ = () + def __hash__(self): return id(self) diff --git a/lib/sqlalchemy/ext/asyncio/engine.py b/lib/sqlalchemy/ext/asyncio/engine.py index f5c3bdca4..5a692ffb1 100644 --- a/lib/sqlalchemy/ext/asyncio/engine.py +++ b/lib/sqlalchemy/ext/asyncio/engine.py @@ -9,6 +9,7 @@ from .base import ProxyComparable from .base import StartableContext from .result import AsyncResult from ... import exc +from ... import inspection from ... import util from ...engine import create_engine as _create_engine from ...engine.base import NestedTransaction @@ -80,6 +81,7 @@ class AsyncConnection(ProxyComparable, StartableContext, AsyncConnectable): # create a new AsyncConnection that matches this one given only the # "sync" elements. __slots__ = ( + "engine", "sync_engine", "sync_connection", ) @@ -709,3 +711,24 @@ def _get_sync_engine_or_connection(async_engine): raise exc.ArgumentError( "AsyncEngine expected, got %r" % async_engine ) from e + + +@inspection._inspects(AsyncConnection) +def _no_insp_for_async_conn_yet(subject): + raise exc.NoInspectionAvailable( + "Inspection on an AsyncConnection is currently not supported. " + "Please use ``run_sync`` to pass a callable where it's possible " + "to call ``inspect`` on the passed connection.", + code="xd3s", + ) + + +@inspection._inspects(AsyncEngine) +def _no_insp_for_async_engine_xyet(subject): + raise exc.NoInspectionAvailable( + "Inspection on an AsyncEngine is currently not supported. " + "Please obtain a connection then use ``conn.run_sync`` to pass a " + "callable where it's possible to call ``inspect`` on the " + "passed connection.", + code="xd3s", + ) diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index 742a9a1f8..e6e5db774 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -1227,6 +1227,10 @@ class SuiteRequirements(Requirements): return self.python36 @property + def insert_order_dicts(self): + return self.python37 + + @property def python36(self): return exclusions.skip_if( lambda: sys.version_info < (3, 6), diff --git a/test/dialect/test_sqlite.py b/test/dialect/test_sqlite.py index b5ce291eb..ed0f11907 100644 --- a/test/dialect/test_sqlite.py +++ b/test/dialect/test_sqlite.py @@ -54,6 +54,7 @@ from sqlalchemy.testing import expect_warnings from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_ from sqlalchemy.testing import mock +from sqlalchemy.testing.assertions import expect_raises_message from sqlalchemy.types import Boolean from sqlalchemy.types import Date from sqlalchemy.types import DateTime @@ -596,6 +597,22 @@ class DialectTest( ) ) + @testing.requires.insert_order_dicts + @testing.only_on("sqlite+pysqlite") + def test_isolation_level_message(self): + # needs to test that all three words are present and we also + # dont want to default all isolation level messages to use + # sorted(), so rely on python 3.7 for ordering of keywords + # in the message + with expect_raises_message( + exc.ArgumentError, + "Invalid value 'invalid' for " + "isolation_level. Valid isolation levels for " + "sqlite are READ UNCOMMITTED, SERIALIZABLE, AUTOCOMMIT", + ): + with testing.db.connect() as conn: + conn.execution_options(isolation_level="invalid") + @testing.only_on("sqlite+pysqlcipher") def test_pysqlcipher_connects(self): """test #6586""" diff --git a/test/ext/asyncio/test_engine_py3k.py b/test/ext/asyncio/test_engine_py3k.py index fec8bc6da..c75dd8665 100644 --- a/test/ext/asyncio/test_engine_py3k.py +++ b/test/ext/asyncio/test_engine_py3k.py @@ -6,6 +6,7 @@ from sqlalchemy import delete from sqlalchemy import event from sqlalchemy import exc from sqlalchemy import func +from sqlalchemy import inspect from sqlalchemy import Integer from sqlalchemy import select from sqlalchemy import String @@ -653,6 +654,39 @@ class AsyncEventTest(EngineFixture): [mock.call(sync_conn, mock.ANY, "select 1", (), mock.ANY, False)], ) + @async_test + async def test_event_on_sync_connection(self, async_engine): + canary = mock.Mock() + + async with async_engine.connect() as conn: + event.listen(conn.sync_connection, "begin", canary) + async with conn.begin(): + eq_( + canary.mock_calls, + [mock.call(conn.sync_connection)], + ) + + +class AsyncInspection(EngineFixture): + __backend__ = True + + @async_test + async def test_inspect_engine(self, async_engine): + with testing.expect_raises_message( + exc.NoInspectionAvailable, + "Inspection on an AsyncEngine is currently not supported.", + ): + inspect(async_engine) + + @async_test + async def test_inspect_connection(self, async_engine): + async with async_engine.connect() as conn: + with testing.expect_raises_message( + exc.NoInspectionAvailable, + "Inspection on an AsyncConnection is currently not supported.", + ): + inspect(conn) + class AsyncResultTest(EngineFixture): @testing.combinations( @@ -945,6 +979,7 @@ class AsyncProxyTest(EngineFixture, fixtures.TestBase): is_not(async_connection.engine, None) @testing.requires.predictable_gc + @async_test async def test_gc_engine(self, testing_engine): ReversibleProxy._proxy_objects.clear() diff --git a/test/ext/asyncio/test_session_py3k.py b/test/ext/asyncio/test_session_py3k.py index 48faa1ca1..459d95ea6 100644 --- a/test/ext/asyncio/test_session_py3k.py +++ b/test/ext/asyncio/test_session_py3k.py @@ -582,12 +582,10 @@ class AsyncEventTest(AsyncFixture): @async_test async def test_no_async_listeners(self, async_session): - with testing.expect_raises( + with testing.expect_raises_message( NotImplementedError, - "NotImplementedError: asynchronous events are not implemented " - "at this time. Apply synchronous listeners to the " - "AsyncEngine.sync_engine or " - "AsyncConnection.sync_connection attributes.", + "asynchronous events are not implemented at this time. " + "Apply synchronous listeners to the AsyncSession.sync_session.", ): event.listen(async_session, "before_flush", mock.Mock()) |
