sqla-fancy-core 1.1.0__tar.gz → 1.1.2__tar.gz

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.

Potentially problematic release.


This version of sqla-fancy-core might be problematic. Click here for more details.

Files changed (20) hide show
  1. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/PKG-INFO +5 -6
  2. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/README.md +4 -5
  3. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/pyproject.toml +1 -1
  4. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/sqla_fancy_core/decorators.py +50 -14
  5. sqla_fancy_core-1.1.2/tests/test_decorators.py +448 -0
  6. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/uv.lock +1 -1
  7. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/.github/workflows/ci.yaml +0 -0
  8. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/.gitignore +0 -0
  9. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/LICENSE +0 -0
  10. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/sqla_fancy_core/__init__.py +0 -0
  11. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/sqla_fancy_core/factories.py +0 -0
  12. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/sqla_fancy_core/wrappers.py +0 -0
  13. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/tests/__init__.py +0 -0
  14. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/tests/test_async_fancy_engine.py +0 -0
  15. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/tests/test_connect.py +0 -0
  16. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/tests/test_fancy_engine.py +0 -0
  17. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/tests/test_field.py +0 -0
  18. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/tests/test_table_factory.py +0 -0
  19. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/tests/test_table_factory_async.py +0 -0
  20. {sqla_fancy_core-1.1.0 → sqla_fancy_core-1.1.2}/tests/test_transact.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqla-fancy-core
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: SQLAlchemy core, but fancier
5
5
  Project-URL: Homepage, https://github.com/sayanarijit/sqla-fancy-core
6
6
  Author-email: Arijit Basu <sayanarijit@gmail.com>
@@ -242,27 +242,24 @@ users = sa.Table(
242
242
  )
243
243
  metadata.create_all(engine)
244
244
 
245
- # 1) Ensure a connection is available (no implicit transaction)
246
245
  @connect
247
246
  def get_user_count(conn=Inject(engine)):
248
247
  return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
249
248
 
250
249
  assert get_user_count() == 0
251
250
 
252
- # 2) Wrap in a transaction automatically
253
251
  @transact
254
252
  def create_user(name: str, conn=Inject(engine)):
255
253
  conn.execute(sa.insert(users).values(name=name))
256
254
 
255
+ # Without an explicit transaction
257
256
  create_user("alice")
258
257
  assert get_user_count() == 1
259
258
 
260
- # 3) Reuse an explicit connection or transaction
259
+ # With an explicit transaction
261
260
  with engine.begin() as txn:
262
261
  create_user("bob", conn=txn)
263
262
  assert get_user_count(conn=txn) == 2
264
-
265
- assert get_user_count() == 2
266
263
  ```
267
264
 
268
265
  ### Async examples
@@ -293,10 +290,12 @@ async def get_user_count(conn=Inject(engine)):
293
290
  async def create_user(name: str, conn=Inject(engine)):
294
291
  await conn.execute(sa.insert(users).values(name=name))
295
292
 
293
+ # Without an explicit transaction
296
294
  assert await get_user_count() == 0
297
295
  await create_user("carol")
298
296
  assert await get_user_count() == 1
299
297
 
298
+ # With an explicit transaction
300
299
  async with engine.connect() as conn:
301
300
  await create_user("dave", conn=conn)
302
301
  assert await get_user_count(conn=conn) == 2
@@ -190,27 +190,24 @@ users = sa.Table(
190
190
  )
191
191
  metadata.create_all(engine)
192
192
 
193
- # 1) Ensure a connection is available (no implicit transaction)
194
193
  @connect
195
194
  def get_user_count(conn=Inject(engine)):
196
195
  return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
197
196
 
198
197
  assert get_user_count() == 0
199
198
 
200
- # 2) Wrap in a transaction automatically
201
199
  @transact
202
200
  def create_user(name: str, conn=Inject(engine)):
203
201
  conn.execute(sa.insert(users).values(name=name))
204
202
 
203
+ # Without an explicit transaction
205
204
  create_user("alice")
206
205
  assert get_user_count() == 1
207
206
 
208
- # 3) Reuse an explicit connection or transaction
207
+ # With an explicit transaction
209
208
  with engine.begin() as txn:
210
209
  create_user("bob", conn=txn)
211
210
  assert get_user_count(conn=txn) == 2
212
-
213
- assert get_user_count() == 2
214
211
  ```
215
212
 
216
213
  ### Async examples
@@ -241,10 +238,12 @@ async def get_user_count(conn=Inject(engine)):
241
238
  async def create_user(name: str, conn=Inject(engine)):
242
239
  await conn.execute(sa.insert(users).values(name=name))
243
240
 
241
+ # Without an explicit transaction
244
242
  assert await get_user_count() == 0
245
243
  await create_user("carol")
246
244
  assert await get_user_count() == 1
247
245
 
246
+ # With an explicit transaction
248
247
  async with engine.connect() as conn:
249
248
  await create_user("dave", conn=conn)
250
249
  assert await get_user_count(conn=conn) == 2
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = 'sqla-fancy-core'
3
- version = '1.1.0'
3
+ version = '1.1.2'
4
4
  description = 'SQLAlchemy core, but fancier'
5
5
  readme = 'README.md'
6
6
  requires-python = ">=3.7"
@@ -7,7 +7,9 @@ from typing import Union, overload
7
7
  import sqlalchemy as sa
8
8
  from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
9
9
 
10
- EngineType = Union[sa.engine.Engine, AsyncEngine]
10
+ from sqla_fancy_core.wrappers import AsyncFancyEngineWrapper, FancyEngineWrapper
11
+
12
+ EngineType = Union[sa.Engine, AsyncEngine, FancyEngineWrapper, AsyncFancyEngineWrapper]
11
13
 
12
14
 
13
15
  class _Injectable:
@@ -16,9 +18,9 @@ class _Injectable:
16
18
 
17
19
 
18
20
  @overload
19
- def Inject(engine: sa.Engine) -> sa.Connection: ...
21
+ def Inject(engine: Union[sa.Engine, FancyEngineWrapper]) -> sa.Connection: ...
20
22
  @overload
21
- def Inject(engine: AsyncEngine) -> AsyncConnection: ...
23
+ def Inject(engine: Union[AsyncEngine, AsyncFancyEngineWrapper]) -> AsyncConnection: ...
22
24
  def Inject(engine: EngineType): # type: ignore
23
25
  """A marker class for dependency injection."""
24
26
  return _Injectable(engine)
@@ -44,7 +46,6 @@ def transact(func):
44
46
  create_user(name="existing", conn=conn)
45
47
  """
46
48
 
47
- # Find the parameter with value Inject
48
49
  sig = inspect.signature(func)
49
50
  inject_param_name = None
50
51
  for name, param in sig.parameters.items():
@@ -56,8 +57,21 @@ def transact(func):
56
57
  if inject_param_name is None:
57
58
  return func # No injection needed
58
59
 
59
- engine = sig.parameters[inject_param_name].default.engine
60
- is_async = isinstance(engine, AsyncEngine)
60
+ engine_arg = sig.parameters[inject_param_name].default.engine
61
+ if isinstance(engine_arg, sa.Engine):
62
+ is_async = False
63
+ engine = engine_arg
64
+ elif isinstance(engine_arg, FancyEngineWrapper):
65
+ is_async = False
66
+ engine = engine_arg.engine
67
+ elif isinstance(engine_arg, AsyncEngine):
68
+ is_async = True
69
+ engine = engine_arg
70
+ elif isinstance(engine_arg, AsyncFancyEngineWrapper):
71
+ is_async = True
72
+ engine = engine_arg.engine
73
+ else:
74
+ raise TypeError("Unsupported engine type")
61
75
 
62
76
  if is_async:
63
77
 
@@ -70,8 +84,14 @@ def transact(func):
70
84
  else:
71
85
  async with conn.begin():
72
86
  return await func(*args, **kwargs)
87
+ elif isinstance(conn, sa.Connection):
88
+ if conn.in_transaction():
89
+ return await func(*args, **kwargs)
90
+ else:
91
+ with conn.begin():
92
+ return await func(*args, **kwargs)
73
93
  else:
74
- async with engine.begin() as conn:
94
+ async with engine.begin() as conn: # type: ignore
75
95
  kwargs[inject_param_name] = conn
76
96
  return await func(*args, **kwargs)
77
97
 
@@ -88,8 +108,10 @@ def transact(func):
88
108
  else:
89
109
  with conn.begin():
90
110
  return func(*args, **kwargs)
111
+ elif isinstance(conn, AsyncConnection):
112
+ raise TypeError("AsyncConnection cannot be used in sync function")
91
113
  else:
92
- with engine.begin() as conn:
114
+ with engine.begin() as conn: # type: ignore
93
115
  kwargs[inject_param_name] = conn
94
116
  return func(*args, **kwargs)
95
117
 
@@ -116,7 +138,6 @@ def connect(func):
116
138
  count = get_user_count(conn)
117
139
  """
118
140
 
119
- # Find the parameter with value Inject
120
141
  sig = inspect.signature(func)
121
142
  inject_param_name = None
122
143
  for name, param in sig.parameters.items():
@@ -128,18 +149,31 @@ def connect(func):
128
149
  if inject_param_name is None:
129
150
  return func # No injection needed
130
151
 
131
- engine = sig.parameters[inject_param_name].default.engine
132
- is_async = isinstance(engine, AsyncEngine)
152
+ engine_arg = sig.parameters[inject_param_name].default.engine
153
+ if isinstance(engine_arg, sa.Engine):
154
+ is_async = False
155
+ engine = engine_arg
156
+ elif isinstance(engine_arg, FancyEngineWrapper):
157
+ is_async = False
158
+ engine = engine_arg.engine
159
+ elif isinstance(engine_arg, AsyncEngine):
160
+ is_async = True
161
+ engine = engine_arg
162
+ elif isinstance(engine_arg, AsyncFancyEngineWrapper):
163
+ is_async = True
164
+ engine = engine_arg.engine
165
+ else:
166
+ raise TypeError("Unsupported engine type")
133
167
 
134
168
  if is_async:
135
169
 
136
170
  @functools.wraps(func)
137
171
  async def async_wrapper(*args, **kwargs):
138
172
  conn = kwargs.get(inject_param_name)
139
- if isinstance(conn, AsyncConnection):
173
+ if isinstance(conn, (AsyncConnection, sa.Connection)):
140
174
  return await func(*args, **kwargs)
141
175
  else:
142
- async with engine.connect() as conn:
176
+ async with engine.connect() as conn: # type: ignore
143
177
  kwargs[inject_param_name] = conn
144
178
  return await func(*args, **kwargs)
145
179
 
@@ -152,8 +186,10 @@ def connect(func):
152
186
  conn = kwargs.get(inject_param_name)
153
187
  if isinstance(conn, sa.Connection):
154
188
  return func(*args, **kwargs)
189
+ elif isinstance(conn, AsyncConnection):
190
+ raise TypeError("AsyncConnection cannot be used in sync function")
155
191
  else:
156
- with engine.connect() as conn:
192
+ with engine.connect() as conn: # type: ignore
157
193
  kwargs[inject_param_name] = conn
158
194
  return func(*args, **kwargs)
159
195
 
@@ -0,0 +1,448 @@
1
+ """Tests for decorator functionality with fancy wrappers and edge cases."""
2
+
3
+ import pytest
4
+ import pytest_asyncio
5
+ import sqlalchemy as sa
6
+ from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine
7
+
8
+ from sqla_fancy_core.decorators import Inject, connect, transact
9
+ from sqla_fancy_core.wrappers import fancy
10
+
11
+ # Define a simple table for testing
12
+ metadata = sa.MetaData()
13
+ users = sa.Table(
14
+ "users",
15
+ metadata,
16
+ sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
17
+ sa.Column("name", sa.String),
18
+ )
19
+
20
+
21
+ @pytest.fixture
22
+ def sync_engine():
23
+ """Provides a synchronous in-memory SQLite engine."""
24
+ engine = sa.create_engine(
25
+ "sqlite:///:memory:",
26
+ connect_args={"check_same_thread": False},
27
+ poolclass=sa.pool.StaticPool,
28
+ )
29
+ metadata.create_all(engine)
30
+ yield engine
31
+ metadata.drop_all(engine)
32
+ engine.dispose()
33
+
34
+
35
+ @pytest_asyncio.fixture
36
+ async def async_engine():
37
+ """Provides an asynchronous in-memory SQLite engine."""
38
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:")
39
+ async with engine.begin() as conn:
40
+ await conn.run_sync(metadata.create_all)
41
+ yield engine
42
+ async with engine.begin() as conn:
43
+ await conn.run_sync(metadata.drop_all)
44
+ await engine.dispose()
45
+
46
+
47
+ # Tests for @connect with FancyEngineWrapper
48
+ def test_connect_with_fancy_wrapper(sync_engine):
49
+ """Test @connect decorator with FancyEngineWrapper."""
50
+ fancy_engine = fancy(sync_engine)
51
+
52
+ @connect
53
+ def get_user_count(conn=Inject(fancy_engine)):
54
+ return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
55
+
56
+ # Should work without explicit connection
57
+ assert get_user_count() == 0
58
+
59
+ # Should work with explicit connection
60
+ with sync_engine.connect() as conn:
61
+ assert get_user_count(conn=conn) == 0
62
+
63
+
64
+ def test_connect_with_fancy_wrapper_and_data(sync_engine):
65
+ """Test @connect decorator with FancyEngineWrapper after inserting data."""
66
+ fancy_engine = fancy(sync_engine)
67
+
68
+ @connect
69
+ def get_user_count(conn=Inject(fancy_engine)):
70
+ return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
71
+
72
+ # Insert some data
73
+ with sync_engine.begin() as conn:
74
+ conn.execute(sa.insert(users).values(name="Alice"))
75
+ conn.execute(sa.insert(users).values(name="Bob"))
76
+
77
+ assert get_user_count() == 2
78
+
79
+
80
+ # Tests for @transact with FancyEngineWrapper
81
+ def test_transact_with_fancy_wrapper(sync_engine):
82
+ """Test @transact decorator with FancyEngineWrapper."""
83
+ fancy_engine = fancy(sync_engine)
84
+
85
+ @transact
86
+ def create_user(name: str, conn=Inject(fancy_engine)):
87
+ conn.execute(sa.insert(users).values(name=name))
88
+
89
+ @connect
90
+ def get_user_count(conn=Inject(fancy_engine)):
91
+ return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
92
+
93
+ # Create user without explicit transaction
94
+ create_user("Alice")
95
+ assert get_user_count() == 1
96
+
97
+ # Create user with explicit transaction
98
+ with sync_engine.begin() as conn:
99
+ create_user("Bob", conn=conn)
100
+
101
+ assert get_user_count() == 2
102
+
103
+
104
+ def test_transact_with_fancy_wrapper_rollback(sync_engine):
105
+ """Test @transact decorator with FancyEngineWrapper and rollback."""
106
+ fancy_engine = fancy(sync_engine)
107
+
108
+ @transact
109
+ def create_user(name: str, conn=Inject(fancy_engine)):
110
+ conn.execute(sa.insert(users).values(name=name))
111
+
112
+ @connect
113
+ def get_user_count(conn=Inject(fancy_engine)):
114
+ return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
115
+
116
+ # Test that rollback works
117
+ try:
118
+ with sync_engine.begin() as conn:
119
+ create_user("Alice", conn=conn)
120
+ raise ValueError("Test rollback")
121
+ except ValueError:
122
+ pass
123
+
124
+ # User should not be created due to rollback
125
+ assert get_user_count() == 0
126
+
127
+
128
+ def test_transact_reuses_existing_transaction(sync_engine):
129
+ """Test that @transact reuses an existing transaction."""
130
+ fancy_engine = fancy(sync_engine)
131
+
132
+ call_count = {"begin": 0}
133
+
134
+ @transact
135
+ def create_user(name: str, conn=Inject(fancy_engine)):
136
+ conn.execute(sa.insert(users).values(name=name))
137
+
138
+ @connect
139
+ def get_user_count(conn=Inject(fancy_engine)):
140
+ return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
141
+
142
+ # When called with a connection already in a transaction,
143
+ # it should not start a new transaction
144
+ with sync_engine.begin() as conn:
145
+ assert conn.in_transaction()
146
+ create_user("Alice", conn=conn)
147
+ create_user("Bob", conn=conn)
148
+
149
+ assert get_user_count() == 2
150
+
151
+
152
+ def test_transact_starts_transaction_for_connection_without_transaction(sync_engine):
153
+ """Test that @transact starts a transaction if connection is not in one."""
154
+ fancy_engine = fancy(sync_engine)
155
+
156
+ @transact
157
+ def create_user(name: str, conn=Inject(fancy_engine)):
158
+ assert conn.in_transaction(), "Connection should be in a transaction"
159
+ conn.execute(sa.insert(users).values(name=name))
160
+
161
+ @connect
162
+ def get_user_count(conn=Inject(fancy_engine)):
163
+ return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
164
+
165
+ # Pass a connection that is NOT in a transaction
166
+ with sync_engine.connect() as conn:
167
+ assert not conn.in_transaction()
168
+ create_user("Alice", conn=conn)
169
+
170
+ assert get_user_count() == 1
171
+
172
+
173
+ # Tests for async @connect with AsyncFancyEngineWrapper
174
+ @pytest.mark.asyncio
175
+ async def test_async_connect_with_fancy_wrapper(async_engine):
176
+ """Test async @connect decorator with AsyncFancyEngineWrapper."""
177
+ fancy_engine = fancy(async_engine)
178
+
179
+ @connect
180
+ async def get_user_count(conn=Inject(fancy_engine)):
181
+ result = await conn.execute(sa.select(sa.func.count()).select_from(users))
182
+ return result.scalar_one()
183
+
184
+ # Should work without explicit connection
185
+ assert await get_user_count() == 0
186
+
187
+ # Should work with explicit connection
188
+ async with async_engine.connect() as conn:
189
+ assert await get_user_count(conn=conn) == 0
190
+
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_async_connect_with_fancy_wrapper_and_data(async_engine):
194
+ """Test async @connect decorator with AsyncFancyEngineWrapper after inserting data."""
195
+ fancy_engine = fancy(async_engine)
196
+
197
+ @connect
198
+ async def get_user_count(conn=Inject(fancy_engine)):
199
+ result = await conn.execute(sa.select(sa.func.count()).select_from(users))
200
+ return result.scalar_one()
201
+
202
+ # Insert some data
203
+ async with async_engine.begin() as conn:
204
+ await conn.execute(sa.insert(users).values(name="Alice"))
205
+ await conn.execute(sa.insert(users).values(name="Bob"))
206
+
207
+ assert await get_user_count() == 2
208
+
209
+
210
+ # Tests for async @transact with AsyncFancyEngineWrapper
211
+ @pytest.mark.asyncio
212
+ async def test_async_transact_with_fancy_wrapper(async_engine):
213
+ """Test async @transact decorator with AsyncFancyEngineWrapper."""
214
+ fancy_engine = fancy(async_engine)
215
+
216
+ @transact
217
+ async def create_user(name: str, conn=Inject(fancy_engine)):
218
+ await conn.execute(sa.insert(users).values(name=name))
219
+
220
+ @connect
221
+ async def get_user_count(conn=Inject(fancy_engine)):
222
+ result = await conn.execute(sa.select(sa.func.count()).select_from(users))
223
+ return result.scalar_one()
224
+
225
+ # Create user without explicit transaction
226
+ await create_user("Alice")
227
+ assert await get_user_count() == 1
228
+
229
+ # Create user with explicit transaction
230
+ async with async_engine.begin() as conn:
231
+ await create_user("Bob", conn=conn)
232
+
233
+ assert await get_user_count() == 2
234
+
235
+
236
+ @pytest.mark.asyncio
237
+ async def test_async_transact_with_fancy_wrapper_rollback(async_engine):
238
+ """Test async @transact decorator with AsyncFancyEngineWrapper and rollback."""
239
+ fancy_engine = fancy(async_engine)
240
+
241
+ @transact
242
+ async def create_user(name: str, conn=Inject(fancy_engine)):
243
+ await conn.execute(sa.insert(users).values(name=name))
244
+
245
+ @connect
246
+ async def get_user_count(conn=Inject(fancy_engine)):
247
+ result = await conn.execute(sa.select(sa.func.count()).select_from(users))
248
+ return result.scalar_one()
249
+
250
+ # Test that rollback works
251
+ try:
252
+ async with async_engine.begin() as conn:
253
+ await create_user("Alice", conn=conn)
254
+ raise ValueError("Test rollback")
255
+ except ValueError:
256
+ pass
257
+
258
+ # User should not be created due to rollback
259
+ assert await get_user_count() == 0
260
+
261
+
262
+ @pytest.mark.asyncio
263
+ async def test_async_transact_reuses_existing_transaction(async_engine):
264
+ """Test that async @transact reuses an existing transaction."""
265
+ fancy_engine = fancy(async_engine)
266
+
267
+ @transact
268
+ async def create_user(name: str, conn=Inject(fancy_engine)):
269
+ await conn.execute(sa.insert(users).values(name=name))
270
+
271
+ @connect
272
+ async def get_user_count(conn=Inject(fancy_engine)):
273
+ result = await conn.execute(sa.select(sa.func.count()).select_from(users))
274
+ return result.scalar_one()
275
+
276
+ # When called with a connection already in a transaction,
277
+ # it should not start a new transaction
278
+ async with async_engine.begin() as conn:
279
+ assert conn.in_transaction()
280
+ await create_user("Alice", conn=conn)
281
+ await create_user("Bob", conn=conn)
282
+
283
+ assert await get_user_count() == 2
284
+
285
+
286
+ @pytest.mark.asyncio
287
+ async def test_async_transact_starts_transaction_for_connection_without_transaction(
288
+ async_engine,
289
+ ):
290
+ """Test that async @transact starts a transaction if connection is not in one."""
291
+ fancy_engine = fancy(async_engine)
292
+
293
+ @transact
294
+ async def create_user(name: str, conn=Inject(fancy_engine)):
295
+ assert conn.in_transaction(), "Connection should be in a transaction"
296
+ await conn.execute(sa.insert(users).values(name=name))
297
+
298
+ @connect
299
+ async def get_user_count(conn=Inject(fancy_engine)):
300
+ result = await conn.execute(sa.select(sa.func.count()).select_from(users))
301
+ return result.scalar_one()
302
+
303
+ # Pass a connection that is NOT in a transaction
304
+ async with async_engine.connect() as conn:
305
+ assert not conn.in_transaction()
306
+ await create_user("Alice", conn=conn)
307
+
308
+ assert await get_user_count() == 1
309
+
310
+
311
+ # Error handling tests
312
+ @pytest.mark.asyncio
313
+ async def test_sync_decorator_rejects_async_connection(sync_engine, async_engine):
314
+ """Test that sync decorator properly rejects AsyncConnection."""
315
+ fancy_engine = fancy(sync_engine)
316
+
317
+ @connect
318
+ def get_user_count(conn=Inject(fancy_engine)):
319
+ return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
320
+
321
+ # Try to pass an actual AsyncConnection to a sync function
322
+ async with async_engine.connect() as async_conn:
323
+ with pytest.raises(TypeError, match="AsyncConnection cannot be used in sync function"):
324
+ get_user_count(conn=async_conn)
325
+
326
+
327
+ @pytest.mark.asyncio
328
+ async def test_sync_transact_rejects_async_connection(sync_engine, async_engine):
329
+ """Test that sync @transact properly rejects AsyncConnection."""
330
+ fancy_engine = fancy(sync_engine)
331
+
332
+ @transact
333
+ def create_user(name: str, conn=Inject(fancy_engine)):
334
+ conn.execute(sa.insert(users).values(name=name))
335
+
336
+ # Try to pass an actual AsyncConnection to a sync function
337
+ async with async_engine.connect() as async_conn:
338
+ with pytest.raises(TypeError, match="AsyncConnection cannot be used in sync function"):
339
+ create_user("Alice", conn=async_conn)
340
+
341
+
342
+ def test_inject_with_unsupported_engine_type():
343
+ """Test that decorators reject unsupported engine types."""
344
+
345
+ class UnsupportedEngine:
346
+ pass
347
+
348
+ unsupported = UnsupportedEngine()
349
+
350
+ with pytest.raises(TypeError, match="Unsupported engine type"):
351
+
352
+ @connect
353
+ def test_func(conn=Inject(unsupported)): # type: ignore
354
+ pass
355
+
356
+ test_func()
357
+
358
+
359
+ def test_transact_with_unsupported_engine_type():
360
+ """Test that @transact rejects unsupported engine types."""
361
+
362
+ class UnsupportedEngine:
363
+ pass
364
+
365
+ unsupported = UnsupportedEngine()
366
+
367
+ with pytest.raises(TypeError, match="Unsupported engine type"):
368
+
369
+ @transact
370
+ def test_func(conn=Inject(unsupported)): # type: ignore
371
+ pass
372
+
373
+ test_func()
374
+
375
+
376
+ # Test decorator without Inject parameter
377
+ def test_connect_without_inject_parameter():
378
+ """Test that @connect works on functions without Inject parameter."""
379
+
380
+ @connect
381
+ def simple_function():
382
+ return "hello"
383
+
384
+ # Should just return the function as-is without wrapping
385
+ assert simple_function() == "hello"
386
+
387
+
388
+ def test_transact_without_inject_parameter():
389
+ """Test that @transact works on functions without Inject parameter."""
390
+
391
+ @transact
392
+ def simple_function():
393
+ return "hello"
394
+
395
+ # Should just return the function as-is without wrapping
396
+ assert simple_function() == "hello"
397
+
398
+
399
+ # Integration test combining both decorators
400
+ def test_integration_connect_and_transact(sync_engine):
401
+ """Test integration of @connect and @transact decorators."""
402
+ fancy_engine = fancy(sync_engine)
403
+
404
+ @transact
405
+ def create_user(name: str, conn=Inject(fancy_engine)):
406
+ conn.execute(sa.insert(users).values(name=name))
407
+
408
+ @connect
409
+ def get_users(conn=Inject(fancy_engine)):
410
+ result = conn.execute(sa.select(users.c.name).order_by(users.c.name))
411
+ return [row[0] for row in result]
412
+
413
+ @transact
414
+ def create_multiple_users(names: list[str], conn=Inject(fancy_engine)):
415
+ for name in names:
416
+ create_user(name, conn=conn)
417
+
418
+ # Create users using nested decorators
419
+ create_multiple_users(["Alice", "Bob", "Charlie"])
420
+
421
+ # Verify all users were created
422
+ assert get_users() == ["Alice", "Bob", "Charlie"]
423
+
424
+
425
+ @pytest.mark.asyncio
426
+ async def test_async_integration_connect_and_transact(async_engine):
427
+ """Test async integration of @connect and @transact decorators."""
428
+ fancy_engine = fancy(async_engine)
429
+
430
+ @transact
431
+ async def create_user(name: str, conn=Inject(fancy_engine)):
432
+ await conn.execute(sa.insert(users).values(name=name))
433
+
434
+ @connect
435
+ async def get_users(conn=Inject(fancy_engine)):
436
+ result = await conn.execute(sa.select(users.c.name).order_by(users.c.name))
437
+ return [row[0] for row in result]
438
+
439
+ @transact
440
+ async def create_multiple_users(names: list[str], conn=Inject(fancy_engine)):
441
+ for name in names:
442
+ await create_user(name, conn=conn)
443
+
444
+ # Create users using nested decorators
445
+ await create_multiple_users(["Alice", "Bob", "Charlie"])
446
+
447
+ # Verify all users were created
448
+ assert await get_users() == ["Alice", "Bob", "Charlie"]
@@ -2968,7 +2968,7 @@ wheels = [
2968
2968
 
2969
2969
  [[package]]
2970
2970
  name = "sqla-fancy-core"
2971
- version = "1.1.0"
2971
+ version = "1.1.2"
2972
2972
  source = { editable = "." }
2973
2973
  dependencies = [
2974
2974
  { name = "sqlalchemy" },
File without changes