TypeDAL 4.9.7__tar.gz → 4.9.8b2__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.
- {typedal-4.9.7 → typedal-4.9.8b2}/CHANGELOG.md +12 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/PKG-INFO +1 -1
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/__about__.py +1 -1
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/core.py +74 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/fields.py +1 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/conftest.py +6 -4
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_main.py +32 -1
- {typedal-4.9.7 → typedal-4.9.8b2}/.crush/.gitignore +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/.crush/crush.db-shm +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/.crush/crush.db-wal +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/.crush/init +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/.crush/logs/crush.log +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/.github/workflows/su6.yml +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/.gitignore +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/.readthedocs.yml +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/README.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/coverage.svg +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/10_advanced_apis.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/1_getting_started.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/2_defining_tables.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/3_building_queries.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/4_relationships.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/5_py4web.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/6_migrations.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/7_configuration.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/8_mixins.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/9_memoization.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/css/code_blocks.css +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/index.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/docs/requirements.txt +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/example_new.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/example_old.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/mkdocs.yml +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/pyproject.toml +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/__init__.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/caching.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/cli.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/config.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/constants.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/define.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/enum_helpers.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/for_py4web.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/for_web2py.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/helpers.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/mixins.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/py.typed +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/query_builder.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/relationships.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/rows.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/serializers/typescript.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/tables.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/types.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tasks.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/__init__.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/configs/simple.toml +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/configs/valid.env +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/configs/valid.toml +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/py314_tests.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_cli.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_config.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_docs_examples.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_helpers.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_json.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_mixins.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_mypy.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_orm.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_py4web.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_query_builder.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_relationships.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_row.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_stats.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_table.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_typescript.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_typing_mypy.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_typing_pyright.md +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_web2py.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_xx_others.py +0 -0
- {typedal-4.9.7 → typedal-4.9.8b2}/tests/timings.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.9.8-beta.2 (2026-07-14)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* **core:** Fully detach tables and fields when closing TypeDAL ([`469e0cb`](https://github.com/trialandsuccess/TypeDAL/commit/469e0cbc5b4f1584849040735ced0232adc18b65))
|
|
10
|
+
|
|
11
|
+
## v4.9.8-beta.1 (2026-07-14)
|
|
12
|
+
|
|
13
|
+
### Fix
|
|
14
|
+
|
|
15
|
+
* **core:** Purge dialect expressions when closing TypeDAL ([`d848cf7`](https://github.com/trialandsuccess/TypeDAL/commit/d848cf72ca0ecee24ba857d6b5e03d3cacce7693))
|
|
16
|
+
|
|
5
17
|
## v4.9.7 (2026-07-14)
|
|
6
18
|
|
|
7
19
|
### Fix
|
|
@@ -40,6 +40,53 @@ if t.TYPE_CHECKING:
|
|
|
40
40
|
from .types import AnyDict, DefineKwargs, Expression, Rows, Set, T_Query, Table
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def _expression_subclasses() -> t.Iterator[type]:
|
|
44
|
+
"""
|
|
45
|
+
Yield pydal.objects.Expression and every (nested) subclass currently loaded, e.g. Field and TypedField.
|
|
46
|
+
"""
|
|
47
|
+
seen = {pydal.objects.Expression}
|
|
48
|
+
stack = [pydal.objects.Expression]
|
|
49
|
+
while stack:
|
|
50
|
+
for subclass in stack.pop().__subclasses__():
|
|
51
|
+
if subclass not in seen:
|
|
52
|
+
seen.add(subclass)
|
|
53
|
+
stack.append(subclass)
|
|
54
|
+
|
|
55
|
+
return iter(seen)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _purge_dialect_expressions(adapter: t.Any) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Undo pydal's global dialect-expression registration for a closed adapter.
|
|
61
|
+
|
|
62
|
+
Some dialects (currently Postgres and Snowflake) register extra Expression methods
|
|
63
|
+
(e.g. `.dow`, `.doy`) by stashing a wrapper per name in the process-wide, class-level
|
|
64
|
+
`Expression._dialect_expressions_` dict. `Expression.__new__` then copies each wrapper
|
|
65
|
+
onto every instantiated Expression subclass (Field, TypedField, ...) as a bound method.
|
|
66
|
+
Both of those are permanent references living outside of any object graph tied to a
|
|
67
|
+
specific db, so they are not reference cycles: closing the adapter and even running
|
|
68
|
+
`gc.collect()` will not free it, and with it the entire db (tables, fields, models)
|
|
69
|
+
it belongs to, for the lifetime of the process, unless explicitly undone here.
|
|
70
|
+
"""
|
|
71
|
+
expressions = pydal.objects.Expression._dialect_expressions_
|
|
72
|
+
stale = [
|
|
73
|
+
name
|
|
74
|
+
for name, wrapper in expressions.items()
|
|
75
|
+
if getattr(getattr(wrapper, "dialect", None), "adapter", None) is adapter
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
if not stale:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
for name in stale:
|
|
82
|
+
del expressions[name]
|
|
83
|
+
|
|
84
|
+
for cls in _expression_subclasses():
|
|
85
|
+
for name in stale:
|
|
86
|
+
if name in cls.__dict__:
|
|
87
|
+
delattr(cls, name)
|
|
88
|
+
|
|
89
|
+
|
|
43
90
|
# note: these functions can not be moved to a different file,
|
|
44
91
|
# because then they will have different globals and it breaks!
|
|
45
92
|
|
|
@@ -282,6 +329,7 @@ class TypeDAL(_TypeDALBase):
|
|
|
282
329
|
|
|
283
330
|
def close(self) -> None:
|
|
284
331
|
"""Close the database connection and unbind all defined TypedTable models."""
|
|
332
|
+
adapter = self._adapter
|
|
285
333
|
try:
|
|
286
334
|
super().close()
|
|
287
335
|
finally:
|
|
@@ -289,6 +337,32 @@ class TypeDAL(_TypeDALBase):
|
|
|
289
337
|
model.unbind()
|
|
290
338
|
self._builder.class_map.clear()
|
|
291
339
|
|
|
340
|
+
if adapter is not None:
|
|
341
|
+
adapter.db = None
|
|
342
|
+
_purge_dialect_expressions(adapter)
|
|
343
|
+
|
|
344
|
+
for table_name in tuple(getattr(self, "tables", ())):
|
|
345
|
+
table = getattr(self, table_name, None)
|
|
346
|
+
if table is None:
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
if hasattr(table, "_db"):
|
|
350
|
+
table._db = None
|
|
351
|
+
|
|
352
|
+
for field_name in getattr(table, "fields", ()):
|
|
353
|
+
field: Field = getattr(table, field_name, None)
|
|
354
|
+
if field is None:
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
if hasattr(field, "_db"):
|
|
358
|
+
field._db = None
|
|
359
|
+
if hasattr(field, "db"):
|
|
360
|
+
field.db = None
|
|
361
|
+
if hasattr(field, "table"):
|
|
362
|
+
field.table = None
|
|
363
|
+
if hasattr(field, "_table"):
|
|
364
|
+
field._table = None
|
|
365
|
+
|
|
292
366
|
def try_define[T: t.Any](self, model: t.Type[T], verbose: bool = False) -> t.Type[T]:
|
|
293
367
|
"""
|
|
294
368
|
Try to define a model with migrate or fall back to fake migrate.
|
|
@@ -21,10 +21,12 @@ def psql(request):
|
|
|
21
21
|
request.addfinalizer(postgres.stop)
|
|
22
22
|
postgres.start()
|
|
23
23
|
|
|
24
|
-
|
|
25
24
|
@pytest.fixture
|
|
26
|
-
def
|
|
25
|
+
def dal_psql_uri(psql) -> str:
|
|
27
26
|
conn_str = postgres.get_connection_url()
|
|
28
|
-
|
|
27
|
+
return "postgres://" + conn_str.split("://")[-1]
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def dal_psql(dal_psql_uri: str):
|
|
29
31
|
with tempfile.TemporaryDirectory() as d:
|
|
30
|
-
yield TypeDAL(
|
|
32
|
+
yield TypeDAL(dal_psql_uri, attempts=1, migrate=True, enable_typedal_caching=False, folder=d)
|
|
@@ -10,7 +10,7 @@ from typing import ForwardRef
|
|
|
10
10
|
|
|
11
11
|
import pytest
|
|
12
12
|
|
|
13
|
-
from src.typedal import TypedRows
|
|
13
|
+
from src.typedal import TypeDAL, TypedRows
|
|
14
14
|
from src.typedal.__about__ import __version__
|
|
15
15
|
from src.typedal.enum_helpers import InvalidEnumValue
|
|
16
16
|
from src.typedal.fields import *
|
|
@@ -67,6 +67,37 @@ def test_database_with_auth_user_is_garbage_collected_after_close():
|
|
|
67
67
|
assert db_ref() is None
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def test_database_is_garbage_collected_after_close_postgres(dal_psql_uri, tmp_path):
|
|
71
|
+
# Postgres (and Snowflake) dialects register extra Expression methods (e.g. `.dow`) in the
|
|
72
|
+
# process-wide, class-level `Expression._dialect_expressions_` dict. That's a permanent
|
|
73
|
+
# reference living outside of any db-specific object graph, so unlike the sqlite case above,
|
|
74
|
+
# gc.collect() alone can't free it - close() has to explicitly undo the registration.
|
|
75
|
+
# (built directly from the conftest `postgres` container, not the `dal_psql` fixture: a
|
|
76
|
+
# suspended fixture generator would itself keep `db` alive for the duration of the test)
|
|
77
|
+
|
|
78
|
+
db = TypeDAL(dal_psql_uri, attempts=1, migrate=True, enable_typedal_caching=False, folder=str(tmp_path))
|
|
79
|
+
|
|
80
|
+
@db.define
|
|
81
|
+
class TemporaryPsqlTable(TypedTable):
|
|
82
|
+
value = TypedField(str)
|
|
83
|
+
|
|
84
|
+
db_ref = weakref.ref(db)
|
|
85
|
+
adapter_ref = weakref.ref(db._adapter)
|
|
86
|
+
|
|
87
|
+
db.close()
|
|
88
|
+
|
|
89
|
+
assert TemporaryPsqlTable._db is None
|
|
90
|
+
assert TemporaryPsqlTable._table is None
|
|
91
|
+
|
|
92
|
+
del db
|
|
93
|
+
gc.collect()
|
|
94
|
+
|
|
95
|
+
assert db_ref() is None
|
|
96
|
+
# the adapter itself is allowed to leak (pydal has no public API to fully undo the global
|
|
97
|
+
# dialect registration) as long as it no longer drags the rest of the db graph with it:
|
|
98
|
+
assert adapter_ref() is None or adapter_ref().db is None
|
|
99
|
+
|
|
100
|
+
|
|
70
101
|
def test_mixed_defines(capsys):
|
|
71
102
|
### DEFINE
|
|
72
103
|
|
|
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
|
|
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
|
|
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
|
|
File without changes
|