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.
Files changed (80) hide show
  1. {typedal-4.9.7 → typedal-4.9.8b2}/CHANGELOG.md +12 -0
  2. {typedal-4.9.7 → typedal-4.9.8b2}/PKG-INFO +1 -1
  3. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/__about__.py +1 -1
  4. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/core.py +74 -0
  5. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/fields.py +1 -0
  6. {typedal-4.9.7 → typedal-4.9.8b2}/tests/conftest.py +6 -4
  7. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_main.py +32 -1
  8. {typedal-4.9.7 → typedal-4.9.8b2}/.crush/.gitignore +0 -0
  9. {typedal-4.9.7 → typedal-4.9.8b2}/.crush/crush.db-shm +0 -0
  10. {typedal-4.9.7 → typedal-4.9.8b2}/.crush/crush.db-wal +0 -0
  11. {typedal-4.9.7 → typedal-4.9.8b2}/.crush/init +0 -0
  12. {typedal-4.9.7 → typedal-4.9.8b2}/.crush/logs/crush.log +0 -0
  13. {typedal-4.9.7 → typedal-4.9.8b2}/.github/workflows/su6.yml +0 -0
  14. {typedal-4.9.7 → typedal-4.9.8b2}/.gitignore +0 -0
  15. {typedal-4.9.7 → typedal-4.9.8b2}/.readthedocs.yml +0 -0
  16. {typedal-4.9.7 → typedal-4.9.8b2}/README.md +0 -0
  17. {typedal-4.9.7 → typedal-4.9.8b2}/coverage.svg +0 -0
  18. {typedal-4.9.7 → typedal-4.9.8b2}/docs/10_advanced_apis.md +0 -0
  19. {typedal-4.9.7 → typedal-4.9.8b2}/docs/1_getting_started.md +0 -0
  20. {typedal-4.9.7 → typedal-4.9.8b2}/docs/2_defining_tables.md +0 -0
  21. {typedal-4.9.7 → typedal-4.9.8b2}/docs/3_building_queries.md +0 -0
  22. {typedal-4.9.7 → typedal-4.9.8b2}/docs/4_relationships.md +0 -0
  23. {typedal-4.9.7 → typedal-4.9.8b2}/docs/5_py4web.md +0 -0
  24. {typedal-4.9.7 → typedal-4.9.8b2}/docs/6_migrations.md +0 -0
  25. {typedal-4.9.7 → typedal-4.9.8b2}/docs/7_configuration.md +0 -0
  26. {typedal-4.9.7 → typedal-4.9.8b2}/docs/8_mixins.md +0 -0
  27. {typedal-4.9.7 → typedal-4.9.8b2}/docs/9_memoization.md +0 -0
  28. {typedal-4.9.7 → typedal-4.9.8b2}/docs/css/code_blocks.css +0 -0
  29. {typedal-4.9.7 → typedal-4.9.8b2}/docs/index.md +0 -0
  30. {typedal-4.9.7 → typedal-4.9.8b2}/docs/requirements.txt +0 -0
  31. {typedal-4.9.7 → typedal-4.9.8b2}/example_new.py +0 -0
  32. {typedal-4.9.7 → typedal-4.9.8b2}/example_old.py +0 -0
  33. {typedal-4.9.7 → typedal-4.9.8b2}/mkdocs.yml +0 -0
  34. {typedal-4.9.7 → typedal-4.9.8b2}/pyproject.toml +0 -0
  35. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/__init__.py +0 -0
  36. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/caching.py +0 -0
  37. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/cli.py +0 -0
  38. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/config.py +0 -0
  39. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/constants.py +0 -0
  40. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/define.py +0 -0
  41. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/enum_helpers.py +0 -0
  42. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/for_py4web.py +0 -0
  43. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/for_web2py.py +0 -0
  44. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/helpers.py +0 -0
  45. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/mixins.py +0 -0
  46. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/py.typed +0 -0
  47. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/query_builder.py +0 -0
  48. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/relationships.py +0 -0
  49. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/rows.py +0 -0
  50. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/serializers/as_json.py +0 -0
  51. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/serializers/typescript.py +0 -0
  52. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/tables.py +0 -0
  53. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/types.py +0 -0
  54. {typedal-4.9.7 → typedal-4.9.8b2}/src/typedal/web2py_py4web_shared.py +0 -0
  55. {typedal-4.9.7 → typedal-4.9.8b2}/tasks.py +0 -0
  56. {typedal-4.9.7 → typedal-4.9.8b2}/tests/__init__.py +0 -0
  57. {typedal-4.9.7 → typedal-4.9.8b2}/tests/configs/simple.toml +0 -0
  58. {typedal-4.9.7 → typedal-4.9.8b2}/tests/configs/valid.env +0 -0
  59. {typedal-4.9.7 → typedal-4.9.8b2}/tests/configs/valid.toml +0 -0
  60. {typedal-4.9.7 → typedal-4.9.8b2}/tests/py314_tests.py +0 -0
  61. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_cli.py +0 -0
  62. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_config.py +0 -0
  63. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_docs_examples.py +0 -0
  64. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_helpers.py +0 -0
  65. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_json.py +0 -0
  66. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_mixins.py +0 -0
  67. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_mypy.py +0 -0
  68. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_orm.py +0 -0
  69. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_py4web.py +0 -0
  70. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_query_builder.py +0 -0
  71. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_relationships.py +0 -0
  72. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_row.py +0 -0
  73. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_stats.py +0 -0
  74. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_table.py +0 -0
  75. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_typescript.py +0 -0
  76. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_typing_mypy.md +0 -0
  77. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_typing_pyright.md +0 -0
  78. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_web2py.py +0 -0
  79. {typedal-4.9.7 → typedal-4.9.8b2}/tests/test_xx_others.py +0 -0
  80. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.9.7
3
+ Version: 4.9.8b2
4
4
  Summary: Typing support for PyDAL
5
5
  Project-URL: Documentation, https://typedal.readthedocs.io/
6
6
  Project-URL: Issues, https://github.com/trialandsuccess/TypeDAL/issues
@@ -5,4 +5,4 @@ This file contains the Version info for this package.
5
5
  # SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
6
6
  #
7
7
  # SPDX-License-Identifier: MIT
8
- __version__ = "4.9.7"
8
+ __version__ = "4.9.8-beta.2"
@@ -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.
@@ -206,6 +206,7 @@ class TypedField[T_Value](Expression): # pragma: no cover
206
206
  self.requires = []
207
207
 
208
208
  self._table = None
209
+ self._db = None
209
210
  self._field = None
210
211
 
211
212
  def __getattr__(self, key: str) -> t.Any:
@@ -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 dal_psql(psql):
25
+ def dal_psql_uri(psql) -> str:
27
26
  conn_str = postgres.get_connection_url()
28
- uri = "postgres://" + conn_str.split("://")[-1]
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(uri, attempts=1, migrate=True, enable_typedal_caching=False, folder=d)
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