sqlakit 0.11.1__tar.gz → 0.12.0__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.
- {sqlakit-0.11.1 → sqlakit-0.12.0}/PKG-INFO +1 -1
- {sqlakit-0.11.1 → sqlakit-0.12.0}/pyproject.toml +1 -1
- {sqlakit-0.11.1 → sqlakit-0.12.0}/pyproject.toml.orig +1 -1
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_base.py +49 -2
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/exceptions.py +8 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/pytest_plugin.py +29 -1
- {sqlakit-0.11.1 → sqlakit-0.12.0}/LICENSE +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/README.md +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/__init__.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_cli.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_debugserver.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_model.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_query.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/_sql.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/asyncio/orm.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/debugserver.html +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/orm.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.11.1 → sqlakit-0.12.0}/sqlakit/types.py +0 -0
|
@@ -8,7 +8,7 @@ import time
|
|
|
8
8
|
from collections.abc import Callable, Mapping
|
|
9
9
|
from contextlib import ExitStack, contextmanager
|
|
10
10
|
from contextvars import ContextVar
|
|
11
|
-
from dataclasses import dataclass
|
|
11
|
+
from dataclasses import dataclass, replace
|
|
12
12
|
from functools import cache
|
|
13
13
|
from typing import (
|
|
14
14
|
TYPE_CHECKING,
|
|
@@ -39,6 +39,7 @@ from ._recording import (
|
|
|
39
39
|
from ._routing import Router, as_router
|
|
40
40
|
from .exceptions import (
|
|
41
41
|
DEFAULT_ALIAS,
|
|
42
|
+
HIDDEN_BLOCK,
|
|
42
43
|
REGISTERED_DEFAULT,
|
|
43
44
|
AliasInUseError,
|
|
44
45
|
ConflictingDatabaseUrlError,
|
|
@@ -169,6 +170,9 @@ class _Scope(Generic[ConnectionT, SessionT]):
|
|
|
169
170
|
autocommit: bool = False
|
|
170
171
|
"""Whether the connection is in ``AUTOCOMMIT``, where no transaction runs."""
|
|
171
172
|
|
|
173
|
+
hidden: bool = False
|
|
174
|
+
"""Whether `unbound()` hides this scope from the code running under it."""
|
|
175
|
+
|
|
172
176
|
|
|
173
177
|
@dataclass(slots=True)
|
|
174
178
|
class _Outer(Generic[ConnectionT]):
|
|
@@ -279,9 +283,14 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
279
283
|
|
|
280
284
|
"""
|
|
281
285
|
try:
|
|
282
|
-
|
|
286
|
+
scope = self._scope.get()
|
|
283
287
|
except LookupError:
|
|
284
288
|
raise MissingConnectionError from None
|
|
289
|
+
if scope.hidden:
|
|
290
|
+
raise MissingConnectionError(
|
|
291
|
+
HIDDEN_BLOCK.format(what="connection")
|
|
292
|
+
) from None
|
|
293
|
+
return scope
|
|
285
294
|
|
|
286
295
|
@property
|
|
287
296
|
def session(self) -> SessionT:
|
|
@@ -299,6 +308,8 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
299
308
|
scope = self._scope.get()
|
|
300
309
|
except LookupError:
|
|
301
310
|
raise MissingSessionError from None
|
|
311
|
+
if scope.hidden:
|
|
312
|
+
raise MissingSessionError(HIDDEN_BLOCK.format(what="session")) from None
|
|
302
313
|
if scope.session is None:
|
|
303
314
|
if scope.connection is None and scope.checkout is not None:
|
|
304
315
|
scope.session = self._lazy_session(scope.checkout)
|
|
@@ -506,6 +517,34 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
506
517
|
scope = self._scope.get(None)
|
|
507
518
|
return scope is not None and scope.session is not None
|
|
508
519
|
|
|
520
|
+
@contextmanager
|
|
521
|
+
def unbound(self) -> Iterator[None]:
|
|
522
|
+
"""Hide the block open around this one, for the code inside to open its own.
|
|
523
|
+
|
|
524
|
+
A test opens a transaction for the whole test, so code that reaches for
|
|
525
|
+
`session` without a block of its own borrows one and passes, where in
|
|
526
|
+
production it raises `MissingSessionError`. Wrap the call under test:
|
|
527
|
+
|
|
528
|
+
```python
|
|
529
|
+
@pytest.mark.db
|
|
530
|
+
def test_the_handler_opens_a_block() -> None:
|
|
531
|
+
with db.unbound():
|
|
532
|
+
handle(event)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
A block opened inside still joins the transaction around it, and rolls
|
|
536
|
+
back with it.
|
|
537
|
+
"""
|
|
538
|
+
scope = self._scope.get(None)
|
|
539
|
+
if scope is None:
|
|
540
|
+
yield
|
|
541
|
+
return
|
|
542
|
+
token = self._scope.set(replace(scope, session=None, hidden=True))
|
|
543
|
+
try:
|
|
544
|
+
yield
|
|
545
|
+
finally:
|
|
546
|
+
self._scope.reset(token)
|
|
547
|
+
|
|
509
548
|
@property
|
|
510
549
|
def engine(self) -> Any: # noqa: ANN401
|
|
511
550
|
"""The engine underneath, which the subclass makes."""
|
|
@@ -866,6 +905,14 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
866
905
|
if debugserver is not None:
|
|
867
906
|
send_recording(together, debugserver)
|
|
868
907
|
|
|
908
|
+
@contextmanager
|
|
909
|
+
def unbound(self) -> Iterator[None]:
|
|
910
|
+
"""Hide the block open on every database this registry holds."""
|
|
911
|
+
with ExitStack() as stack:
|
|
912
|
+
for alias in self.aliases:
|
|
913
|
+
stack.enter_context(BaseDatabase.unbound(self[alias]))
|
|
914
|
+
yield
|
|
915
|
+
|
|
869
916
|
def _recorded(
|
|
870
917
|
self, using: str | DatabaseT | Sequence[str | DatabaseT] | None
|
|
871
918
|
) -> tuple[Any, ...]:
|
|
@@ -85,6 +85,14 @@ class RetryNotSupportedError(SQLAKitError, TypeError):
|
|
|
85
85
|
super().__init__(message)
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
HIDDEN_BLOCK = (
|
|
89
|
+
"No {what} is bound: the block around this one is hidden, so the code "
|
|
90
|
+
"under it opens its own. `with db.transaction():` to write, "
|
|
91
|
+
"`with db.connect():` to read. A test hides its block with "
|
|
92
|
+
"`sqlakit_unbound`, `db(unbound=True)` or `db.unbound()`."
|
|
93
|
+
)
|
|
94
|
+
"""What the two errors above say under a hidden block, which a test opens."""
|
|
95
|
+
|
|
88
96
|
REGISTERED_DEFAULT = (
|
|
89
97
|
"This registry did not build the default database, it was registered, so "
|
|
90
98
|
"the settings it was built from live on it. Read them from `db['default']`."
|
|
@@ -19,13 +19,18 @@ up in, or the one they were given in person. A project with no model layer
|
|
|
19
19
|
defines `sqlakit_db` and `sqlakit_metadata` instead.
|
|
20
20
|
|
|
21
21
|
A test marked `db` runs in a transaction that rolls back, on every database.
|
|
22
|
-
`using` narrows that to the ones a test works on
|
|
22
|
+
`using` narrows that to the ones a test works on, and `unbound` hides the
|
|
23
|
+
test's own block from the code it calls, as production has none:
|
|
23
24
|
|
|
24
25
|
```python
|
|
25
26
|
@pytest.mark.db(using="warehouse")
|
|
26
27
|
@pytest.mark.db(using=["default", "warehouse"])
|
|
28
|
+
@pytest.mark.db(unbound=True)
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
`sqlakit_unbound = true` says the last one for the whole suite, and
|
|
32
|
+
`db(unbound=False)` gives one test its block back.
|
|
33
|
+
|
|
29
34
|
Every other test connects to nothing, and reaching for a session there raises
|
|
30
35
|
`MissingSessionError` rather than opening one.
|
|
31
36
|
"""
|
|
@@ -66,6 +71,12 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
|
|
66
71
|
type="bool",
|
|
67
72
|
default=False,
|
|
68
73
|
)
|
|
74
|
+
parser.addini(
|
|
75
|
+
"sqlakit_unbound",
|
|
76
|
+
"hide the test's own block, so the code it calls opens one as in production",
|
|
77
|
+
type="bool",
|
|
78
|
+
default=False,
|
|
79
|
+
)
|
|
69
80
|
parser.addini(
|
|
70
81
|
"sqlakit_skip_queries_from",
|
|
71
82
|
"the queries these files run stay out of the report: a factory, a helper",
|
|
@@ -276,6 +287,8 @@ def _sqlakit_transaction(
|
|
|
276
287
|
with ExitStack() as stack:
|
|
277
288
|
for block in _rolled_back(sqlakit_db, _asked_for(request)):
|
|
278
289
|
stack.enter_context(block)
|
|
290
|
+
if _hidden(request):
|
|
291
|
+
stack.enter_context(sqlakit_db.unbound())
|
|
279
292
|
with _reported(request, sqlakit_db):
|
|
280
293
|
yield
|
|
281
294
|
|
|
@@ -289,6 +302,8 @@ async def _sqlakit_async_transaction(
|
|
|
289
302
|
async with AsyncExitStack() as stack:
|
|
290
303
|
for block in _rolled_back(sqlakit_db, _asked_for(request)):
|
|
291
304
|
await stack.enter_async_context(block)
|
|
305
|
+
if _hidden(request):
|
|
306
|
+
stack.enter_context(sqlakit_db.unbound())
|
|
292
307
|
with _reported(request, sqlakit_db):
|
|
293
308
|
yield
|
|
294
309
|
|
|
@@ -385,6 +400,19 @@ def _asked_for(request: pytest.FixtureRequest) -> tuple[Any, ...]:
|
|
|
385
400
|
return tuple(using)
|
|
386
401
|
|
|
387
402
|
|
|
403
|
+
def _hidden(request: pytest.FixtureRequest) -> bool:
|
|
404
|
+
"""Whether the test's own block is hidden from the code it calls.
|
|
405
|
+
|
|
406
|
+
`sqlakit_unbound` says it for the suite, and `db(unbound=...)` on a test
|
|
407
|
+
says it for that one.
|
|
408
|
+
"""
|
|
409
|
+
marker = request.node.get_closest_marker(MARKER)
|
|
410
|
+
asked = None if marker is None else marker.kwargs.get("unbound")
|
|
411
|
+
if asked is None:
|
|
412
|
+
return bool(request.config.getini("sqlakit_unbound"))
|
|
413
|
+
return bool(asked)
|
|
414
|
+
|
|
415
|
+
|
|
388
416
|
def _rolled_back(db: Any, using: tuple[Any, ...]) -> list[Any]: # noqa: ANN401
|
|
389
417
|
"""Return the blocks that undo what a test writes.
|
|
390
418
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|