SQLAlchemy 2.0.41__py3-none-any.whl → 2.0.43__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. sqlalchemy/__init__.py +1 -1
  2. sqlalchemy/connectors/aioodbc.py +10 -0
  3. sqlalchemy/connectors/asyncio.py +210 -72
  4. sqlalchemy/connectors/pyodbc.py +8 -5
  5. sqlalchemy/cyextension/util.pyx +15 -16
  6. sqlalchemy/dialects/__init__.py +2 -1
  7. sqlalchemy/dialects/mssql/base.py +85 -56
  8. sqlalchemy/dialects/mssql/information_schema.py +47 -16
  9. sqlalchemy/dialects/mysql/aiomysql.py +85 -176
  10. sqlalchemy/dialects/mysql/asyncmy.py +76 -190
  11. sqlalchemy/dialects/mysql/base.py +615 -273
  12. sqlalchemy/dialects/mysql/cymysql.py +34 -12
  13. sqlalchemy/dialects/mysql/enumerated.py +65 -26
  14. sqlalchemy/dialects/mysql/expression.py +6 -3
  15. sqlalchemy/dialects/mysql/json.py +24 -14
  16. sqlalchemy/dialects/mysql/mariadb.py +12 -6
  17. sqlalchemy/dialects/mysql/mariadbconnector.py +76 -31
  18. sqlalchemy/dialects/mysql/mysqlconnector.py +84 -35
  19. sqlalchemy/dialects/mysql/mysqldb.py +59 -50
  20. sqlalchemy/dialects/mysql/provision.py +0 -1
  21. sqlalchemy/dialects/mysql/pymysql.py +32 -10
  22. sqlalchemy/dialects/mysql/pyodbc.py +32 -14
  23. sqlalchemy/dialects/mysql/reflection.py +87 -37
  24. sqlalchemy/dialects/mysql/reserved_words.py +0 -1
  25. sqlalchemy/dialects/mysql/types.py +120 -58
  26. sqlalchemy/dialects/oracle/__init__.py +4 -0
  27. sqlalchemy/dialects/oracle/base.py +79 -24
  28. sqlalchemy/dialects/oracle/cx_oracle.py +3 -0
  29. sqlalchemy/dialects/oracle/vector.py +104 -6
  30. sqlalchemy/dialects/postgresql/_psycopg_common.py +3 -0
  31. sqlalchemy/dialects/postgresql/array.py +1 -1
  32. sqlalchemy/dialects/postgresql/asyncpg.py +7 -1
  33. sqlalchemy/dialects/postgresql/base.py +66 -15
  34. sqlalchemy/dialects/postgresql/named_types.py +0 -14
  35. sqlalchemy/dialects/postgresql/pg8000.py +3 -0
  36. sqlalchemy/dialects/postgresql/pg_catalog.py +14 -0
  37. sqlalchemy/dialects/postgresql/ranges.py +2 -2
  38. sqlalchemy/dialects/sqlite/aiosqlite.py +89 -44
  39. sqlalchemy/dialects/sqlite/base.py +8 -0
  40. sqlalchemy/dialects/sqlite/pysqlite.py +23 -2
  41. sqlalchemy/engine/_py_util.py +2 -2
  42. sqlalchemy/engine/base.py +12 -8
  43. sqlalchemy/engine/create.py +15 -0
  44. sqlalchemy/engine/cursor.py +3 -3
  45. sqlalchemy/engine/default.py +15 -6
  46. sqlalchemy/engine/interfaces.py +61 -10
  47. sqlalchemy/engine/result.py +1 -1
  48. sqlalchemy/engine/strategies.py +1 -4
  49. sqlalchemy/event/api.py +1 -3
  50. sqlalchemy/ext/associationproxy.py +15 -1
  51. sqlalchemy/ext/asyncio/base.py +1 -1
  52. sqlalchemy/ext/asyncio/engine.py +1 -1
  53. sqlalchemy/ext/asyncio/result.py +1 -1
  54. sqlalchemy/ext/indexable.py +1 -0
  55. sqlalchemy/ext/mutable.py +1 -0
  56. sqlalchemy/ext/mypy/names.py +1 -1
  57. sqlalchemy/ext/orderinglist.py +48 -36
  58. sqlalchemy/orm/_orm_constructors.py +79 -8
  59. sqlalchemy/orm/attributes.py +21 -11
  60. sqlalchemy/orm/base.py +3 -5
  61. sqlalchemy/orm/bulk_persistence.py +17 -5
  62. sqlalchemy/orm/collections.py +1 -1
  63. sqlalchemy/orm/context.py +2 -2
  64. sqlalchemy/orm/decl_api.py +3 -0
  65. sqlalchemy/orm/decl_base.py +11 -5
  66. sqlalchemy/orm/dependency.py +1 -3
  67. sqlalchemy/orm/descriptor_props.py +16 -1
  68. sqlalchemy/orm/events.py +2 -4
  69. sqlalchemy/orm/interfaces.py +7 -1
  70. sqlalchemy/orm/loading.py +13 -9
  71. sqlalchemy/orm/mapper.py +12 -8
  72. sqlalchemy/orm/path_registry.py +1 -3
  73. sqlalchemy/orm/persistence.py +8 -2
  74. sqlalchemy/orm/properties.py +39 -16
  75. sqlalchemy/orm/relationships.py +1 -2
  76. sqlalchemy/orm/session.py +1 -1
  77. sqlalchemy/orm/state_changes.py +1 -3
  78. sqlalchemy/orm/strategies.py +1 -1
  79. sqlalchemy/orm/strategy_options.py +25 -6
  80. sqlalchemy/orm/util.py +1 -1
  81. sqlalchemy/orm/writeonly.py +2 -6
  82. sqlalchemy/pool/base.py +3 -3
  83. sqlalchemy/pool/impl.py +5 -7
  84. sqlalchemy/schema.py +1 -3
  85. sqlalchemy/sql/_selectable_constructors.py +61 -13
  86. sqlalchemy/sql/_typing.py +2 -2
  87. sqlalchemy/sql/base.py +104 -82
  88. sqlalchemy/sql/coercions.py +1 -1
  89. sqlalchemy/sql/compiler.py +66 -13
  90. sqlalchemy/sql/crud.py +66 -0
  91. sqlalchemy/sql/ddl.py +3 -1
  92. sqlalchemy/sql/default_comparator.py +1 -2
  93. sqlalchemy/sql/elements.py +41 -32
  94. sqlalchemy/sql/expression.py +1 -4
  95. sqlalchemy/sql/functions.py +2 -4
  96. sqlalchemy/sql/lambdas.py +11 -9
  97. sqlalchemy/sql/naming.py +1 -4
  98. sqlalchemy/sql/schema.py +3 -3
  99. sqlalchemy/sql/selectable.py +42 -16
  100. sqlalchemy/sql/sqltypes.py +14 -5
  101. sqlalchemy/sql/type_api.py +16 -10
  102. sqlalchemy/sql/util.py +2 -4
  103. sqlalchemy/sql/visitors.py +1 -4
  104. sqlalchemy/testing/assertions.py +2 -0
  105. sqlalchemy/testing/exclusions.py +2 -2
  106. sqlalchemy/testing/fixtures/base.py +5 -0
  107. sqlalchemy/testing/requirements.py +25 -0
  108. sqlalchemy/testing/suite/test_cte.py +26 -0
  109. sqlalchemy/testing/suite/test_dialect.py +37 -1
  110. sqlalchemy/testing/suite/test_reflection.py +114 -19
  111. sqlalchemy/testing/util.py +5 -8
  112. sqlalchemy/types.py +1 -3
  113. sqlalchemy/util/_py_collections.py +4 -4
  114. {sqlalchemy-2.0.41.dist-info → sqlalchemy-2.0.43.dist-info}/METADATA +1 -1
  115. {sqlalchemy-2.0.41.dist-info → sqlalchemy-2.0.43.dist-info}/RECORD +118 -118
  116. {sqlalchemy-2.0.41.dist-info → sqlalchemy-2.0.43.dist-info}/WHEEL +1 -1
  117. {sqlalchemy-2.0.41.dist-info → sqlalchemy-2.0.43.dist-info}/licenses/LICENSE +0 -0
  118. {sqlalchemy-2.0.41.dist-info → sqlalchemy-2.0.43.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py CHANGED
@@ -269,7 +269,7 @@ from .types import Uuid as Uuid
269
269
  from .types import VARBINARY as VARBINARY
270
270
  from .types import VARCHAR as VARCHAR
271
271
 
272
- __version__ = "2.0.41"
272
+ __version__ = "2.0.43"
273
273
 
274
274
 
275
275
  def __go(lcls: Any) -> None:
@@ -20,6 +20,7 @@ from .. import util
20
20
  from ..util.concurrency import await_fallback
21
21
  from ..util.concurrency import await_only
22
22
 
23
+
23
24
  if TYPE_CHECKING:
24
25
  from ..engine.interfaces import ConnectArgsType
25
26
  from ..engine.url import URL
@@ -58,6 +59,15 @@ class AsyncAdapt_aioodbc_connection(AsyncAdapt_dbapi_connection):
58
59
 
59
60
  self._connection._conn.autocommit = value
60
61
 
62
+ def ping(self, reconnect):
63
+ return self.await_(self._connection.ping(reconnect))
64
+
65
+ def add_output_converter(self, *arg, **kw):
66
+ self._connection.add_output_converter(*arg, **kw)
67
+
68
+ def character_set_name(self):
69
+ return self._connection.character_set_name()
70
+
61
71
  def cursor(self, server_side=False):
62
72
  # aioodbc sets connection=None when closed and just fails with
63
73
  # AttributeError here. Here we use the same ProgrammingError +
@@ -4,18 +4,121 @@
4
4
  #
5
5
  # This module is part of SQLAlchemy and is released under
6
6
  # the MIT License: https://www.opensource.org/licenses/mit-license.php
7
- # mypy: ignore-errors
8
7
 
9
8
  """generic asyncio-adapted versions of DBAPI connection and cursor"""
10
9
 
11
10
  from __future__ import annotations
12
11
 
12
+ import asyncio
13
13
  import collections
14
+ import sys
15
+ from typing import Any
16
+ from typing import AsyncIterator
17
+ from typing import Deque
18
+ from typing import Iterator
19
+ from typing import NoReturn
20
+ from typing import Optional
21
+ from typing import Sequence
22
+ from typing import TYPE_CHECKING
14
23
 
15
24
  from ..engine import AdaptedConnection
16
- from ..util.concurrency import asyncio
17
25
  from ..util.concurrency import await_fallback
18
26
  from ..util.concurrency import await_only
27
+ from ..util.typing import Protocol
28
+
29
+ if TYPE_CHECKING:
30
+ from ..engine.interfaces import _DBAPICursorDescription
31
+ from ..engine.interfaces import _DBAPIMultiExecuteParams
32
+ from ..engine.interfaces import _DBAPISingleExecuteParams
33
+ from ..engine.interfaces import DBAPIModule
34
+ from ..util.typing import Self
35
+
36
+
37
+ class AsyncIODBAPIConnection(Protocol):
38
+ """protocol representing an async adapted version of a
39
+ :pep:`249` database connection.
40
+
41
+
42
+ """
43
+
44
+ # note that async DBAPIs dont agree if close() should be awaitable,
45
+ # so it is omitted here and picked up by the __getattr__ hook below
46
+
47
+ async def commit(self) -> None: ...
48
+
49
+ def cursor(self, *args: Any, **kwargs: Any) -> AsyncIODBAPICursor: ...
50
+
51
+ async def rollback(self) -> None: ...
52
+
53
+ def __getattr__(self, key: str) -> Any: ...
54
+
55
+ def __setattr__(self, key: str, value: Any) -> None: ...
56
+
57
+
58
+ class AsyncIODBAPICursor(Protocol):
59
+ """protocol representing an async adapted version
60
+ of a :pep:`249` database cursor.
61
+
62
+
63
+ """
64
+
65
+ def __aenter__(self) -> Any: ...
66
+
67
+ @property
68
+ def description(
69
+ self,
70
+ ) -> _DBAPICursorDescription:
71
+ """The description attribute of the Cursor."""
72
+ ...
73
+
74
+ @property
75
+ def rowcount(self) -> int: ...
76
+
77
+ arraysize: int
78
+
79
+ lastrowid: int
80
+
81
+ async def close(self) -> None: ...
82
+
83
+ async def execute(
84
+ self,
85
+ operation: Any,
86
+ parameters: Optional[_DBAPISingleExecuteParams] = None,
87
+ ) -> Any: ...
88
+
89
+ async def executemany(
90
+ self,
91
+ operation: Any,
92
+ parameters: _DBAPIMultiExecuteParams,
93
+ ) -> Any: ...
94
+
95
+ async def fetchone(self) -> Optional[Any]: ...
96
+
97
+ async def fetchmany(self, size: Optional[int] = ...) -> Sequence[Any]: ...
98
+
99
+ async def fetchall(self) -> Sequence[Any]: ...
100
+
101
+ async def setinputsizes(self, sizes: Sequence[Any]) -> None: ...
102
+
103
+ def setoutputsize(self, size: Any, column: Any) -> None: ...
104
+
105
+ async def callproc(
106
+ self, procname: str, parameters: Sequence[Any] = ...
107
+ ) -> Any: ...
108
+
109
+ async def nextset(self) -> Optional[bool]: ...
110
+
111
+ def __aiter__(self) -> AsyncIterator[Any]: ...
112
+
113
+
114
+ class AsyncAdapt_dbapi_module:
115
+ if TYPE_CHECKING:
116
+ Error = DBAPIModule.Error
117
+ OperationalError = DBAPIModule.OperationalError
118
+ InterfaceError = DBAPIModule.InterfaceError
119
+ IntegrityError = DBAPIModule.IntegrityError
120
+
121
+ def __getattr__(self, key: str) -> Any: ...
19
122
 
20
123
 
21
124
  class AsyncAdapt_dbapi_cursor:
@@ -28,96 +131,136 @@ class AsyncAdapt_dbapi_cursor:
28
131
  "_rows",
29
132
  )
30
133
 
31
- def __init__(self, adapt_connection):
134
+ _cursor: AsyncIODBAPICursor
135
+ _adapt_connection: AsyncAdapt_dbapi_connection
136
+ _connection: AsyncIODBAPIConnection
137
+ _rows: Deque[Any]
138
+
139
+ def __init__(self, adapt_connection: AsyncAdapt_dbapi_connection):
32
140
  self._adapt_connection = adapt_connection
33
141
  self._connection = adapt_connection._connection
142
+
34
143
  self.await_ = adapt_connection.await_
35
144
 
36
- cursor = self._connection.cursor()
145
+ cursor = self._make_new_cursor(self._connection)
37
146
  self._cursor = self._aenter_cursor(cursor)
38
147
 
39
148
  if not self.server_side:
40
149
  self._rows = collections.deque()
41
150
 
42
- def _aenter_cursor(self, cursor):
43
- return self.await_(cursor.__aenter__())
151
+ def _aenter_cursor(self, cursor: AsyncIODBAPICursor) -> AsyncIODBAPICursor:
152
+ return self.await_(cursor.__aenter__()) # type: ignore[no-any-return]
153
+
154
+ def _make_new_cursor(
155
+ self, connection: AsyncIODBAPIConnection
156
+ ) -> AsyncIODBAPICursor:
157
+ return connection.cursor()
44
158
 
45
159
  @property
46
- def description(self):
160
+ def description(self) -> Optional[_DBAPICursorDescription]:
47
161
  return self._cursor.description
48
162
 
49
163
  @property
50
- def rowcount(self):
164
+ def rowcount(self) -> int:
51
165
  return self._cursor.rowcount
52
166
 
53
167
  @property
54
- def arraysize(self):
168
+ def arraysize(self) -> int:
55
169
  return self._cursor.arraysize
56
170
 
57
171
  @arraysize.setter
58
- def arraysize(self, value):
172
+ def arraysize(self, value: int) -> None:
59
173
  self._cursor.arraysize = value
60
174
 
61
175
  @property
62
- def lastrowid(self):
176
+ def lastrowid(self) -> int:
63
177
  return self._cursor.lastrowid
64
178
 
65
- def close(self):
179
+ def close(self) -> None:
66
180
  # note we aren't actually closing the cursor here,
67
181
  # we are just letting GC do it. see notes in aiomysql dialect
68
182
  self._rows.clear()
69
183
 
70
- def execute(self, operation, parameters=None):
71
- return self.await_(self._execute_async(operation, parameters))
72
-
73
- def executemany(self, operation, seq_of_parameters):
74
- return self.await_(
75
- self._executemany_async(operation, seq_of_parameters)
76
- )
184
+ def execute(
185
+ self,
186
+ operation: Any,
187
+ parameters: Optional[_DBAPISingleExecuteParams] = None,
188
+ ) -> Any:
189
+ try:
190
+ return self.await_(self._execute_async(operation, parameters))
191
+ except Exception as error:
192
+ self._adapt_connection._handle_exception(error)
193
+
194
+ def executemany(
195
+ self,
196
+ operation: Any,
197
+ seq_of_parameters: _DBAPIMultiExecuteParams,
198
+ ) -> Any:
199
+ try:
200
+ return self.await_(
201
+ self._executemany_async(operation, seq_of_parameters)
202
+ )
203
+ except Exception as error:
204
+ self._adapt_connection._handle_exception(error)
77
205
 
78
- async def _execute_async(self, operation, parameters):
206
+ async def _execute_async(
207
+ self, operation: Any, parameters: Optional[_DBAPISingleExecuteParams]
208
+ ) -> Any:
79
209
  async with self._adapt_connection._execute_mutex:
80
- result = await self._cursor.execute(operation, parameters or ())
210
+ if parameters is None:
211
+ result = await self._cursor.execute(operation)
212
+ else:
213
+ result = await self._cursor.execute(operation, parameters)
81
214
 
82
215
  if self._cursor.description and not self.server_side:
83
216
  self._rows = collections.deque(await self._cursor.fetchall())
84
217
  return result
85
218
 
86
- async def _executemany_async(self, operation, seq_of_parameters):
219
+ async def _executemany_async(
220
+ self,
221
+ operation: Any,
222
+ seq_of_parameters: _DBAPIMultiExecuteParams,
223
+ ) -> Any:
87
224
  async with self._adapt_connection._execute_mutex:
88
225
  return await self._cursor.executemany(operation, seq_of_parameters)
89
226
 
90
- def nextset(self):
227
+ def nextset(self) -> None:
91
228
  self.await_(self._cursor.nextset())
92
229
  if self._cursor.description and not self.server_side:
93
230
  self._rows = collections.deque(
94
231
  self.await_(self._cursor.fetchall())
95
232
  )
96
233
 
97
- def setinputsizes(self, *inputsizes):
234
+ def setinputsizes(self, *inputsizes: Any) -> None:
98
235
  # NOTE: this is overrridden in aioodbc due to
99
236
  # see https://github.com/aio-libs/aioodbc/issues/451
100
237
  # right now
101
238
 
102
239
  return self.await_(self._cursor.setinputsizes(*inputsizes))
103
240
 
104
- def __iter__(self):
241
+ def __enter__(self) -> Self:
242
+ return self
243
+
244
+ def __exit__(self, type_: Any, value: Any, traceback: Any) -> None:
245
+ self.close()
246
+
247
+ def __iter__(self) -> Iterator[Any]:
105
248
  while self._rows:
106
249
  yield self._rows.popleft()
107
250
 
108
- def fetchone(self):
251
+ def fetchone(self) -> Optional[Any]:
109
252
  if self._rows:
110
253
  return self._rows.popleft()
111
254
  else:
112
255
  return None
113
256
 
114
- def fetchmany(self, size=None):
257
+ def fetchmany(self, size: Optional[int] = None) -> Sequence[Any]:
115
258
  if size is None:
116
259
  size = self.arraysize
117
260
  rr = self._rows
118
261
  return [rr.popleft() for _ in range(min(size, len(rr)))]
119
262
 
120
- def fetchall(self):
263
+ def fetchall(self) -> Sequence[Any]:
121
264
  retval = list(self._rows)
122
265
  self._rows.clear()
123
266
  return retval
@@ -127,30 +270,21 @@ class AsyncAdapt_dbapi_ss_cursor(AsyncAdapt_dbapi_cursor):
127
270
  __slots__ = ()
128
271
  server_side = True
129
272
 
130
- def __init__(self, adapt_connection):
131
- self._adapt_connection = adapt_connection
132
- self._connection = adapt_connection._connection
133
- self.await_ = adapt_connection.await_
134
-
135
- cursor = self._connection.cursor()
136
-
137
- self._cursor = self.await_(cursor.__aenter__())
138
-
139
- def close(self):
273
+ def close(self) -> None:
140
274
  if self._cursor is not None:
141
275
  self.await_(self._cursor.close())
142
- self._cursor = None
276
+ self._cursor = None # type: ignore
143
277
 
144
- def fetchone(self):
278
+ def fetchone(self) -> Optional[Any]:
145
279
  return self.await_(self._cursor.fetchone())
146
280
 
147
- def fetchmany(self, size=None):
281
+ def fetchmany(self, size: Optional[int] = None) -> Any:
148
282
  return self.await_(self._cursor.fetchmany(size=size))
149
283
 
150
- def fetchall(self):
284
+ def fetchall(self) -> Sequence[Any]:
151
285
  return self.await_(self._cursor.fetchall())
152
286
 
153
- def __iter__(self):
287
+ def __iter__(self) -> Iterator[Any]:
154
288
  iterator = self._cursor.__aiter__()
155
289
  while True:
156
290
  try:
@@ -164,46 +298,50 @@ class AsyncAdapt_dbapi_connection(AdaptedConnection):
164
298
  _ss_cursor_cls = AsyncAdapt_dbapi_ss_cursor
165
299
 
166
300
  await_ = staticmethod(await_only)
301
+
167
302
  __slots__ = ("dbapi", "_execute_mutex")
168
303
 
169
- def __init__(self, dbapi, connection):
304
+ _connection: AsyncIODBAPIConnection
305
+
306
+ def __init__(self, dbapi: Any, connection: AsyncIODBAPIConnection):
170
307
  self.dbapi = dbapi
171
308
  self._connection = connection
172
309
  self._execute_mutex = asyncio.Lock()
173
310
 
174
- def ping(self, reconnect):
175
- return self.await_(self._connection.ping(reconnect))
176
-
177
- def add_output_converter(self, *arg, **kw):
178
- self._connection.add_output_converter(*arg, **kw)
179
-
180
- def character_set_name(self):
181
- return self._connection.character_set_name()
182
-
183
- @property
184
- def autocommit(self):
185
- return self._connection.autocommit
186
-
187
- @autocommit.setter
188
- def autocommit(self, value):
189
- # https://github.com/aio-libs/aioodbc/issues/448
190
- # self._connection.autocommit = value
191
-
192
- self._connection._conn.autocommit = value
193
-
194
- def cursor(self, server_side=False):
311
+ def cursor(self, server_side: bool = False) -> AsyncAdapt_dbapi_cursor:
195
312
  if server_side:
196
313
  return self._ss_cursor_cls(self)
197
314
  else:
198
315
  return self._cursor_cls(self)
199
316
 
200
- def rollback(self):
201
- self.await_(self._connection.rollback())
202
-
203
- def commit(self):
204
- self.await_(self._connection.commit())
205
-
206
- def close(self):
317
+ def execute(
318
+ self,
319
+ operation: Any,
320
+ parameters: Optional[_DBAPISingleExecuteParams] = None,
321
+ ) -> Any:
322
+ """lots of DBAPIs seem to provide this, so include it"""
323
+ cursor = self.cursor()
324
+ cursor.execute(operation, parameters)
325
+ return cursor
326
+
327
+ def _handle_exception(self, error: Exception) -> NoReturn:
328
+ exc_info = sys.exc_info()
329
+
330
+ raise error.with_traceback(exc_info[2])
331
+
332
+ def rollback(self) -> None:
333
+ try:
334
+ self.await_(self._connection.rollback())
335
+ except Exception as error:
336
+ self._handle_exception(error)
337
+
338
+ def commit(self) -> None:
339
+ try:
340
+ self.await_(self._connection.commit())
341
+ except Exception as error:
342
+ self._handle_exception(error)
343
+
344
+ def close(self) -> None:
207
345
  self.await_(self._connection.close())
208
346
 
209
347
 
@@ -8,7 +8,6 @@
8
8
  from __future__ import annotations
9
9
 
10
10
  import re
11
- from types import ModuleType
12
11
  import typing
13
12
  from typing import Any
14
13
  from typing import Dict
@@ -29,6 +28,7 @@ from ..engine import URL
29
28
  from ..sql.type_api import TypeEngine
30
29
 
31
30
  if typing.TYPE_CHECKING:
31
+ from ..engine.interfaces import DBAPIModule
32
32
  from ..engine.interfaces import IsolationLevel
33
33
 
34
34
 
@@ -48,15 +48,13 @@ class PyODBCConnector(Connector):
48
48
  # hold the desired driver name
49
49
  pyodbc_driver_name: Optional[str] = None
50
50
 
51
- dbapi: ModuleType
52
-
53
51
  def __init__(self, use_setinputsizes: bool = False, **kw: Any):
54
52
  super().__init__(**kw)
55
53
  if use_setinputsizes:
56
54
  self.bind_typing = interfaces.BindTyping.SETINPUTSIZES
57
55
 
58
56
  @classmethod
59
- def import_dbapi(cls) -> ModuleType:
57
+ def import_dbapi(cls) -> DBAPIModule:
60
58
  return __import__("pyodbc")
61
59
 
62
60
  def create_connect_args(self, url: URL) -> ConnectArgsType:
@@ -150,7 +148,7 @@ class PyODBCConnector(Connector):
150
148
  ],
151
149
  cursor: Optional[interfaces.DBAPICursor],
152
150
  ) -> bool:
153
- if isinstance(e, self.dbapi.ProgrammingError):
151
+ if isinstance(e, self.loaded_dbapi.ProgrammingError):
154
152
  return "The cursor's connection has been closed." in str(
155
153
  e
156
154
  ) or "Attempt to use a closed connection." in str(e)
@@ -245,3 +243,8 @@ class PyODBCConnector(Connector):
245
243
  else:
246
244
  dbapi_connection.autocommit = False
247
245
  super().set_isolation_level(dbapi_connection, level)
246
+
247
+ def detect_autocommit_setting(
248
+ self, dbapi_conn: interfaces.DBAPIConnection
249
+ ) -> bool:
250
+ return bool(dbapi_conn.autocommit)
@@ -10,28 +10,24 @@ from sqlalchemy import exc
10
10
 
11
11
  cdef tuple _Empty_Tuple = ()
12
12
 
13
- cdef inline bint _mapping_or_tuple(object value):
13
+ cdef inline bint _is_mapping_or_tuple(object value):
14
14
  return isinstance(value, dict) or isinstance(value, tuple) or isinstance(value, Mapping)
15
15
 
16
- cdef inline bint _check_item(object params) except 0:
17
- cdef object item
18
- cdef bint ret = 1
19
- if params:
20
- item = params[0]
21
- if not _mapping_or_tuple(item):
22
- ret = 0
23
- raise exc.ArgumentError(
24
- "List argument must consist only of tuples or dictionaries"
25
- )
26
- return ret
16
+
17
+ cdef inline bint _is_mapping(object value):
18
+ return isinstance(value, dict) or isinstance(value, Mapping)
19
+
27
20
 
28
21
  def _distill_params_20(object params):
29
22
  if params is None:
30
23
  return _Empty_Tuple
31
24
  elif isinstance(params, list) or isinstance(params, tuple):
32
- _check_item(params)
25
+ if params and not _is_mapping(params[0]):
26
+ raise exc.ArgumentError(
27
+ "List argument must consist only of dictionaries"
28
+ )
33
29
  return params
34
- elif isinstance(params, dict) or isinstance(params, Mapping):
30
+ elif _is_mapping(params):
35
31
  return [params]
36
32
  else:
37
33
  raise exc.ArgumentError("mapping or list expected for parameters")
@@ -41,9 +37,12 @@ def _distill_raw_params(object params):
41
37
  if params is None:
42
38
  return _Empty_Tuple
43
39
  elif isinstance(params, list):
44
- _check_item(params)
40
+ if params and not _is_mapping_or_tuple(params[0]):
41
+ raise exc.ArgumentError(
42
+ "List argument must consist only of tuples or dictionaries"
43
+ )
45
44
  return params
46
- elif _mapping_or_tuple(params):
45
+ elif _is_mapping_or_tuple(params):
47
46
  return [params]
48
47
  else:
49
48
  raise exc.ArgumentError("mapping or sequence expected for parameters")
@@ -7,6 +7,7 @@
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from typing import Any
10
11
  from typing import Callable
11
12
  from typing import Optional
12
13
  from typing import Type
@@ -39,7 +40,7 @@ def _auto_fn(name: str) -> Optional[Callable[[], Type[Dialect]]]:
39
40
  # hardcoded. if mysql / mariadb etc were third party dialects
40
41
  # they would just publish all the entrypoints, which would actually
41
42
  # look much nicer.
42
- module = __import__(
43
+ module: Any = __import__(
43
44
  "sqlalchemy.dialects.mysql.mariadb"
44
45
  ).dialects.mysql.mariadb
45
46
  return module.loader(driver) # type: ignore