TypeDAL 4.9.6__tar.gz → 4.9.8__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 (81) hide show
  1. {typedal-4.9.6 → typedal-4.9.8}/CHANGELOG.md +24 -0
  2. {typedal-4.9.6 → typedal-4.9.8}/PKG-INFO +1 -1
  3. {typedal-4.9.6 → typedal-4.9.8}/pyproject.toml +1 -2
  4. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/__about__.py +1 -1
  5. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/cli.py +1 -2
  6. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/core.py +88 -0
  7. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/fields.py +7 -0
  8. {typedal-4.9.6 → typedal-4.9.8}/tasks.py +1 -1
  9. {typedal-4.9.6 → typedal-4.9.8}/tests/conftest.py +6 -4
  10. {typedal-4.9.6 → typedal-4.9.8}/tests/test_main.py +96 -1
  11. {typedal-4.9.6 → typedal-4.9.8}/tests/test_mypy.py +4 -4
  12. typedal-4.9.8/tests/test_py4web.py +91 -0
  13. {typedal-4.9.6 → typedal-4.9.8}/tests/test_relationships.py +20 -19
  14. typedal-4.9.6/tests/test_py4web.py +0 -92
  15. {typedal-4.9.6 → typedal-4.9.8}/.crush/.gitignore +0 -0
  16. {typedal-4.9.6 → typedal-4.9.8}/.crush/crush.db-shm +0 -0
  17. {typedal-4.9.6 → typedal-4.9.8}/.crush/crush.db-wal +0 -0
  18. {typedal-4.9.6 → typedal-4.9.8}/.crush/init +0 -0
  19. {typedal-4.9.6 → typedal-4.9.8}/.crush/logs/crush.log +0 -0
  20. {typedal-4.9.6 → typedal-4.9.8}/.github/workflows/su6.yml +0 -0
  21. {typedal-4.9.6 → typedal-4.9.8}/.gitignore +0 -0
  22. {typedal-4.9.6 → typedal-4.9.8}/.readthedocs.yml +0 -0
  23. {typedal-4.9.6 → typedal-4.9.8}/README.md +0 -0
  24. {typedal-4.9.6 → typedal-4.9.8}/coverage.svg +0 -0
  25. {typedal-4.9.6 → typedal-4.9.8}/docs/10_advanced_apis.md +0 -0
  26. {typedal-4.9.6 → typedal-4.9.8}/docs/1_getting_started.md +0 -0
  27. {typedal-4.9.6 → typedal-4.9.8}/docs/2_defining_tables.md +0 -0
  28. {typedal-4.9.6 → typedal-4.9.8}/docs/3_building_queries.md +0 -0
  29. {typedal-4.9.6 → typedal-4.9.8}/docs/4_relationships.md +0 -0
  30. {typedal-4.9.6 → typedal-4.9.8}/docs/5_py4web.md +0 -0
  31. {typedal-4.9.6 → typedal-4.9.8}/docs/6_migrations.md +0 -0
  32. {typedal-4.9.6 → typedal-4.9.8}/docs/7_configuration.md +0 -0
  33. {typedal-4.9.6 → typedal-4.9.8}/docs/8_mixins.md +0 -0
  34. {typedal-4.9.6 → typedal-4.9.8}/docs/9_memoization.md +0 -0
  35. {typedal-4.9.6 → typedal-4.9.8}/docs/css/code_blocks.css +0 -0
  36. {typedal-4.9.6 → typedal-4.9.8}/docs/index.md +0 -0
  37. {typedal-4.9.6 → typedal-4.9.8}/docs/requirements.txt +0 -0
  38. {typedal-4.9.6 → typedal-4.9.8}/example_new.py +0 -0
  39. {typedal-4.9.6 → typedal-4.9.8}/example_old.py +0 -0
  40. {typedal-4.9.6 → typedal-4.9.8}/mkdocs.yml +0 -0
  41. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/__init__.py +0 -0
  42. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/caching.py +0 -0
  43. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/config.py +0 -0
  44. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/constants.py +0 -0
  45. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/define.py +0 -0
  46. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/enum_helpers.py +0 -0
  47. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/for_py4web.py +0 -0
  48. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/for_web2py.py +0 -0
  49. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/helpers.py +0 -0
  50. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/mixins.py +0 -0
  51. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/py.typed +0 -0
  52. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/query_builder.py +0 -0
  53. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/relationships.py +0 -0
  54. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/rows.py +0 -0
  55. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/serializers/as_json.py +0 -0
  56. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/serializers/typescript.py +0 -0
  57. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/tables.py +0 -0
  58. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/types.py +0 -0
  59. {typedal-4.9.6 → typedal-4.9.8}/src/typedal/web2py_py4web_shared.py +0 -0
  60. {typedal-4.9.6 → typedal-4.9.8}/tests/__init__.py +0 -0
  61. {typedal-4.9.6 → typedal-4.9.8}/tests/configs/simple.toml +0 -0
  62. {typedal-4.9.6 → typedal-4.9.8}/tests/configs/valid.env +0 -0
  63. {typedal-4.9.6 → typedal-4.9.8}/tests/configs/valid.toml +0 -0
  64. {typedal-4.9.6 → typedal-4.9.8}/tests/py314_tests.py +0 -0
  65. {typedal-4.9.6 → typedal-4.9.8}/tests/test_cli.py +0 -0
  66. {typedal-4.9.6 → typedal-4.9.8}/tests/test_config.py +0 -0
  67. {typedal-4.9.6 → typedal-4.9.8}/tests/test_docs_examples.py +0 -0
  68. {typedal-4.9.6 → typedal-4.9.8}/tests/test_helpers.py +0 -0
  69. {typedal-4.9.6 → typedal-4.9.8}/tests/test_json.py +0 -0
  70. {typedal-4.9.6 → typedal-4.9.8}/tests/test_mixins.py +0 -0
  71. {typedal-4.9.6 → typedal-4.9.8}/tests/test_orm.py +0 -0
  72. {typedal-4.9.6 → typedal-4.9.8}/tests/test_query_builder.py +0 -0
  73. {typedal-4.9.6 → typedal-4.9.8}/tests/test_row.py +0 -0
  74. {typedal-4.9.6 → typedal-4.9.8}/tests/test_stats.py +0 -0
  75. {typedal-4.9.6 → typedal-4.9.8}/tests/test_table.py +0 -0
  76. {typedal-4.9.6 → typedal-4.9.8}/tests/test_typescript.py +0 -0
  77. {typedal-4.9.6 → typedal-4.9.8}/tests/test_typing_mypy.md +0 -0
  78. {typedal-4.9.6 → typedal-4.9.8}/tests/test_typing_pyright.md +0 -0
  79. {typedal-4.9.6 → typedal-4.9.8}/tests/test_web2py.py +0 -0
  80. {typedal-4.9.6 → typedal-4.9.8}/tests/test_xx_others.py +0 -0
  81. {typedal-4.9.6 → typedal-4.9.8}/tests/timings.py +0 -0
@@ -2,6 +2,30 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.9.8 (2026-07-14)
6
+
7
+ ### Fix
8
+
9
+ * **core:** Clear table hooks and field validators on close ([`3a3fe7b`](https://github.com/trialandsuccess/TypeDAL/commit/3a3fe7b3387476a9ee50f85d492dea067f218069))
10
+
11
+ ## v4.9.8-beta.2 (2026-07-14)
12
+
13
+ ### Fix
14
+
15
+ * **core:** Fully detach tables and fields when closing TypeDAL ([`469e0cb`](https://github.com/trialandsuccess/TypeDAL/commit/469e0cbc5b4f1584849040735ced0232adc18b65))
16
+
17
+ ## v4.9.8-beta.1 (2026-07-14)
18
+
19
+ ### Fix
20
+
21
+ * **core:** Purge dialect expressions when closing TypeDAL ([`d848cf7`](https://github.com/trialandsuccess/TypeDAL/commit/d848cf72ca0ecee24ba857d6b5e03d3cacce7693))
22
+
23
+ ## v4.9.7 (2026-07-14)
24
+
25
+ ### Fix
26
+
27
+ * **fields:** Clear field requirements during unbind ([`7e054f9`](https://github.com/trialandsuccess/TypeDAL/commit/7e054f947358a3fbb491db0d777410433da43d44))
28
+
5
29
  ## v4.9.6 (2026-07-14)
6
30
 
7
31
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.9.6
3
+ Version: 4.9.8
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
@@ -193,8 +193,7 @@ exclude = ["venv", ".bak"]
193
193
  target-version = "py314"
194
194
  line-length = 120
195
195
 
196
- extend-exclude = ["*.bak/", "venv*/"]
197
-
196
+ extend-exclude = ["*.bak/", "venv*/", "tests/", "example_*.py"]
198
197
  [tool.ruff.lint]
199
198
 
200
199
  select = [
@@ -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.6"
8
+ __version__ = "4.9.8"
@@ -425,8 +425,7 @@ def fake_migrations(
425
425
 
426
426
  previously_migrated = (
427
427
  db(
428
- db.ewh_implemented_features.name.belongs(to_fake)
429
- & (db.ewh_implemented_features.installed == True), # noqa E712
428
+ db.ewh_implemented_features.name.belongs(to_fake) & (db.ewh_implemented_features.installed == True), # noqa E712
430
429
  )
431
430
  .select(db.ewh_implemented_features.name)
432
431
  .column("name")
@@ -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,46 @@ 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: Table | None = getattr(self, table_name, None)
346
+ if table is None: # pragma: no cover
347
+ continue
348
+
349
+ for hook_name in (
350
+ "_before_insert",
351
+ "_after_insert",
352
+ "_before_update",
353
+ "_after_update",
354
+ "_before_delete",
355
+ "_after_delete",
356
+ ):
357
+ hooks = getattr(table, hook_name, None)
358
+ if isinstance(hooks, list):
359
+ hooks.clear()
360
+
361
+ if hasattr(table, "_db"):
362
+ table._db = None
363
+
364
+ for field_name in getattr(table, "fields", ()):
365
+ field: Field | None = getattr(table, field_name, None)
366
+ if field is None: # pragma: no cover
367
+ continue
368
+
369
+ if hasattr(field, "_db"):
370
+ field._db = None
371
+ if hasattr(field, "db"):
372
+ field.db = None
373
+ if hasattr(field, "table"):
374
+ field.table = None
375
+ if hasattr(field, "_table"):
376
+ field._table = None
377
+ if hasattr(field, "requires"):
378
+ field.requires = []
379
+
292
380
  def try_define[T: t.Any](self, model: t.Type[T], verbose: bool = False) -> t.Type[T]:
293
381
  """
294
382
  Try to define a model with migrate or fall back to fake migrate.
@@ -199,7 +199,14 @@ class TypedField[T_Value](Expression): # pragma: no cover
199
199
 
200
200
  def unbind(self) -> None:
201
201
  """Remove references to the pydal objects created during ``bind``."""
202
+ if getattr(self, "requires", None):
203
+ if isinstance(self.requires, list):
204
+ self.requires.clear()
205
+ else:
206
+ self.requires = []
207
+
202
208
  self._table = None
209
+ self._db = None
203
210
  self._field = None
204
211
 
205
212
  def __getattr__(self, key: str) -> t.Any:
@@ -33,7 +33,7 @@ def generate_nav_entries() -> list[str]:
33
33
 
34
34
 
35
35
  @task
36
- def update_docs_nav(ctx: Context) -> None:
36
+ def update_docs_nav(_: Context) -> None:
37
37
  """Update mkdocs.yml nav section from actual markdown files to prevent sync issues."""
38
38
  mkdocs_file = Path(__file__).parent / "mkdocs.yml"
39
39
 
@@ -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)
@@ -3,17 +3,21 @@ import gc
3
3
  import re
4
4
  import sys
5
5
  import typing
6
+ import types
6
7
  import weakref
8
+ from contextlib import contextmanager
7
9
  from copy import copy
8
10
  from sqlite3 import IntegrityError
9
11
  from typing import ForwardRef
10
12
 
11
13
  import pytest
14
+ from pydal.validators import IS_NOT_IN_DB
12
15
 
13
- from src.typedal import TypedRows
16
+ from src.typedal import TypeDAL, TypedRows
14
17
  from src.typedal.__about__ import __version__
15
18
  from src.typedal.enum_helpers import InvalidEnumValue
16
19
  from src.typedal.fields import *
20
+ from src.typedal.web2py_py4web_shared import AuthUser
17
21
 
18
22
 
19
23
  def test_about():
@@ -47,6 +51,97 @@ def test_database_is_garbage_collected_after_close():
47
51
  assert db_ref() is None
48
52
 
49
53
 
54
+ def test_database_is_garbage_collected_after_context_manager():
55
+
56
+ @contextmanager
57
+ def typedal_context():
58
+ db = TypeDAL("sqlite:memory", enable_typedal_caching=False)
59
+ try:
60
+ yield db
61
+ db.commit()
62
+ except:
63
+ db.rollback()
64
+ finally:
65
+ db.close()
66
+
67
+ def in_scope():
68
+ with typedal_context() as db:
69
+ @db.define
70
+ class RelatedTable(TypedTable):
71
+ value = TypedField(str)
72
+
73
+ @db.define
74
+ class TemporaryTable(TypedTable):
75
+ related: RelatedTable
76
+ value = TypedField(str)
77
+
78
+ @classmethod
79
+ def __on_define__(cls, db):
80
+ cls.id.requires = IS_NOT_IN_DB(db, cls.id)
81
+ cls.after_insert(lambda _row, _id, db=db: None)
82
+
83
+ query = TemporaryTable.where(id=0)
84
+ assert not query.paginate(limit=1)
85
+
86
+ return weakref.ref(db)
87
+
88
+ db_ref = in_scope()
89
+
90
+ gc.collect()
91
+
92
+ assert db_ref() is None
93
+
94
+
95
+ def test_database_with_auth_user_is_garbage_collected_after_close():
96
+ db = TypeDAL("sqlite:memory", enable_typedal_caching=False)
97
+ db.define(AuthUser, redefine=True)
98
+
99
+ AuthUser.after_insert(lambda row, db=db: print(db))
100
+
101
+ db_ref = weakref.ref(db)
102
+
103
+ db.close()
104
+
105
+ assert AuthUser._db is None
106
+ assert AuthUser._table is None
107
+
108
+ del db
109
+ gc.collect()
110
+
111
+ assert db_ref() is None
112
+
113
+
114
+ def test_database_is_garbage_collected_after_close_postgres(dal_psql_uri, tmp_path):
115
+ # Postgres (and Snowflake) dialects register extra Expression methods (e.g. `.dow`) in the
116
+ # process-wide, class-level `Expression._dialect_expressions_` dict. That's a permanent
117
+ # reference living outside of any db-specific object graph, so unlike the sqlite case above,
118
+ # gc.collect() alone can't free it - close() has to explicitly undo the registration.
119
+ # (built directly from the conftest `postgres` container, not the `dal_psql` fixture: a
120
+ # suspended fixture generator would itself keep `db` alive for the duration of the test)
121
+
122
+ db = TypeDAL(dal_psql_uri, attempts=1, migrate=True, enable_typedal_caching=False, folder=str(tmp_path))
123
+
124
+ @db.define
125
+ class TemporaryPsqlTable(TypedTable):
126
+ value = TypedField(str)
127
+
128
+ db_ref = weakref.ref(db)
129
+ adapter_ref = weakref.ref(db._adapter)
130
+
131
+ db.close()
132
+
133
+ assert TemporaryPsqlTable._db is None
134
+ assert TemporaryPsqlTable._table is None
135
+
136
+ del db
137
+ gc.collect()
138
+
139
+ assert db_ref() is None
140
+ # the adapter itself is allowed to leak (pydal has no public API to fully undo the global
141
+ # dialect registration) as long as it no longer drags the rest of the db graph with it:
142
+ assert adapter_ref() is None or adapter_ref().db is None
143
+
144
+
50
145
  def test_mixed_defines(capsys):
51
146
  ### DEFINE
52
147
 
@@ -178,12 +178,12 @@ def mypy_test_mixin_typed_table_argument() -> None:
178
178
 
179
179
  @pytest.mark.mypy_testing
180
180
  def mypy_test_cachefn() -> None:
181
- def cache_model(key: str, fn: CacheFn, expire: int) -> Rows:
181
+ def cache_model(_key: str, fn: CacheFn, _expire: int) -> Rows:
182
182
  return fn()
183
183
 
184
- cache_valid: CacheTuple = (cache_model, 3000)
184
+ _cache_valid: CacheTuple = (cache_model, 3000)
185
185
 
186
- def invalid_cache_model(key: str, fn: typing.Callable[..., list[str]], _: int = None) -> list[str]:
186
+ def invalid_cache_model(_key: str, fn: typing.Callable[..., list[str]], _expire: int = None) -> list[str]:
187
187
  return fn()
188
188
 
189
- cache_invalid: CacheTuple = (invalid_cache_model, 3000) # E: [assignment]
189
+ _cache_invalid: CacheTuple = (invalid_cache_model, 3000) # E: [assignment]
@@ -0,0 +1,91 @@
1
+ import json
2
+ import tempfile
3
+
4
+ from contextlib_chdir import chdir
5
+ from pydal.validators import IS_EMAIL, IS_NOT_IN_DB
6
+
7
+ from src.typedal import TypedTable
8
+ from src.typedal.for_py4web import DAL, AuthUser, setup_py4web_tables
9
+ from src.typedal.serializers import as_json
10
+ from typedal.config import TypeDALConfig
11
+
12
+ db = DAL("sqlite:memory")
13
+
14
+
15
+ def test_P4W():
16
+ assert hasattr(DAL, "on_request")
17
+ assert hasattr(db, "on_request")
18
+
19
+
20
+ def test_serialize():
21
+ @db.define()
22
+ class JsonTable(TypedTable):
23
+ field: str
24
+
25
+ row = JsonTable.insert(field="Hey")
26
+
27
+ loaded_data = json.loads(as_json.encode(JsonTable))
28
+ assert loaded_data
29
+ assert isinstance(loaded_data, dict)
30
+ loaded_data = json.loads(as_json.encode(row))
31
+ assert isinstance(loaded_data, dict)
32
+ assert loaded_data
33
+
34
+ loaded_data = json.loads(as_json.encode(JsonTable.paginate(limit=1)))
35
+
36
+ assert loaded_data["pagination"]
37
+ assert loaded_data["data"]
38
+
39
+
40
+ def test_auth_user():
41
+ setup_py4web_tables(db)
42
+ requirements = AuthUser.email.requires
43
+
44
+ assert requirements
45
+ assert isinstance(requirements, list)
46
+ assert len(requirements) == 2
47
+
48
+ assert isinstance(requirements[0], IS_EMAIL)
49
+ assert isinstance(requirements[1], IS_NOT_IN_DB)
50
+
51
+
52
+ def test_py4web_dal_singleton():
53
+ with tempfile.TemporaryDirectory() as d, chdir(d):
54
+ # note: typedal caching is disabled here because otherwise the cache table might send logs to {d}/log.sql
55
+ # which can lead to problems in other tests :(
56
+ db_1a = DAL("sqlite:memory", enable_typedal_caching=False)
57
+ db_1b = DAL("sqlite:memory", enable_typedal_caching=False)
58
+
59
+ db_2a = DAL("sqlite://test_py4web_dal_singleton", folder=d, enable_typedal_caching=False)
60
+ db_2b = DAL("sqlite://test_py4web_dal_singleton", folder=d, enable_typedal_caching=False)
61
+
62
+ conf = {"database": "sqlite:memory", "dialect": "sqlite", "pyproject": "", "flag_location": f"{d}/flags"}
63
+ db_3a = DAL(config=TypeDALConfig.load(conf))
64
+ db_3b = DAL(config=TypeDALConfig.load(conf))
65
+
66
+ assert db_1a is db_1b
67
+ assert db_1a._uri == db_1b._uri
68
+ assert db_1a._db_uid == db_1b._db_uid
69
+
70
+ assert db_2a is db_2b
71
+ assert db_2a._uri == db_2b._uri
72
+ assert db_2a._db_uid == db_2b._db_uid
73
+
74
+ assert db_1a is not db_2a
75
+ assert db_1a._uri != db_2a._uri
76
+ assert db_1a._db_uid != db_2a._db_uid
77
+
78
+ assert db_1b is not db_2b
79
+ assert db_1b._uri != db_2b._uri
80
+ assert db_1b._db_uid != db_2b._db_uid
81
+
82
+ assert db_3a is db_3b
83
+ assert db_3a is not db_1a
84
+ assert db_3a is not db_2a
85
+
86
+ assert db_1a._uri == db_1b._uri
87
+ assert db_3a._uri == db_1a._uri
88
+ assert db_3a._uri != db_2a._uri
89
+
90
+ # reset singletons for later use:
91
+ DAL._clear()
@@ -129,8 +129,8 @@ def _setup_data():
129
129
 
130
130
  # no relationships:
131
131
  new_author = User.insert(name="Untagged Author", roles=[], main_role=writer, extra_roles=[])
132
- untagged1 = Article.insert(title="Untagged Article 1", author=new_author)
133
- untagged2 = Article.insert(title="Untagged Article 2", author=new_author)
132
+ _untagged1 = Article.insert(title="Untagged Article 1", author=new_author)
133
+ _untagged2 = Article.insert(title="Untagged Article 2", author=new_author)
134
134
 
135
135
  # articles
136
136
 
@@ -355,10 +355,9 @@ def test_reprs():
355
355
 
356
356
  assert "AND" not in repr(relation)
357
357
 
358
- relation = Article.join("author", condition_and=lambda article, author: author.name != "Hank").relationships[
358
+ relation = Article.join("author", condition_and=lambda _article, author: author.name != "Hank").relationships[
359
359
  "author"
360
360
  ]
361
-
362
361
  assert "AND" in repr(relation) and "Hank" in repr(relation)
363
362
 
364
363
 
@@ -436,7 +435,7 @@ def test_join_with_different_condition():
436
435
  role_with_users = Role.join(
437
436
  "users",
438
437
  method="inner",
439
- condition_and=lambda role, user: ~user.name.like("Reader%"),
438
+ condition_and=lambda _role, user: ~user.name.like("Reader%"),
440
439
  ).first()
441
440
 
442
441
  assert role_with_users.users
@@ -446,7 +445,7 @@ def test_join_with_different_condition():
446
445
  role_with_users = Role.join(
447
446
  "users",
448
447
  method="left",
449
- condition_and=lambda role, user: ~user.name.like("Reader%"),
448
+ condition_and=lambda _role, user: ~user.name.like("Reader%"),
450
449
  ).first()
451
450
 
452
451
  assert role_with_users.users
@@ -729,11 +728,11 @@ def test_memoize_nested_dependencies():
729
728
  assert len(users) > 0
730
729
 
731
730
  # First memoize - should be fresh
732
- result1, status = db.memoize(process_users, users)
731
+ _result1, status = db.memoize(process_users, users)
733
732
  assert status == "fresh"
734
733
 
735
734
  # Second call - should be cached
736
- result2, status = db.memoize(process_users, users)
735
+ _result2, status = db.memoize(process_users, users)
737
736
  assert status == "cached"
738
737
 
739
738
  # Update a Role that was accessed inside process_users
@@ -742,7 +741,7 @@ def test_memoize_nested_dependencies():
742
741
 
743
742
  # Third call - should be fresh (invalidated by Role change)
744
743
  # but currently will still be "cached" because we only tracked User deps
745
- result3, status = db.memoize(process_users, users)
744
+ _result3, status = db.memoize(process_users, users)
746
745
  assert status == "fresh"
747
746
 
748
747
 
@@ -755,25 +754,25 @@ def test_memoize_nested_dependencies2():
755
754
  # (which happens when using _determine_dependencies_auto)
756
755
  return list(User.join())
757
756
 
758
- bogus, status = db.memoize(something_slow)
757
+ _bogus, status = db.memoize(something_slow)
759
758
  assert status == "fresh"
760
759
 
761
760
  # no change
762
- bogus, status = db.memoize(something_slow)
761
+ _bogus, status = db.memoize(something_slow)
763
762
  assert status == "cached"
764
763
 
765
764
  # user change
766
765
  User.first().update_record(name="New Name :)")
767
- bogus, status = db.memoize(something_slow)
766
+ _bogus, status = db.memoize(something_slow)
768
767
  assert status == "fresh"
769
768
 
770
769
  # role change
771
770
  Role.first().update_record(name="New Role Name :)")
772
- bogus, status = db.memoize(something_slow)
771
+ _bogus, status = db.memoize(something_slow)
773
772
  assert status == "fresh"
774
773
 
775
774
  # no change
776
- bogus, status = db.memoize(something_slow)
775
+ _bogus, status = db.memoize(something_slow)
777
776
  assert status == "cached"
778
777
 
779
778
 
@@ -931,7 +930,8 @@ def test_accessing_raw_data():
931
930
 
932
931
  user = User.where(id=4).join("articles").first()
933
932
 
934
- # <User({"id": 4, "name": "Untagged Author", "roles": [], "gid": "967da807-eb13-46dc-a0f3-d5c04751edf4", "main_role": 2, "extra_roles": []}) + ['articles']>
933
+ # <User({"id": 4, "name": "Untagged Author", "roles": [],
934
+ # "gid": "967da807-eb13-46dc-a0f3-d5c04751edf4", "main_role": 2, "extra_roles": []}) + ['articles']>
935
935
  assert user._row
936
936
 
937
937
  # one row per user+article combination like how postgres returns it:
@@ -1031,9 +1031,9 @@ def test_nested_join_with_shared_foreign_key():
1031
1031
  tech_corp = Company.insert(name="Tech Corp")
1032
1032
 
1033
1033
  # Create multiple offices in the same city for the same company
1034
- office1 = Office.insert(address="123 Market St", city_id=san_francisco.id, company=tech_corp.id)
1035
- office2 = Office.insert(address="456 Mission St", city_id=san_francisco.id, company=tech_corp.id)
1036
- office3 = Office.insert(address="789 Howard St", city_id=san_francisco.id, company=tech_corp.id)
1034
+ _office1 = Office.insert(address="123 Market St", city_id=san_francisco.id, company=tech_corp.id)
1035
+ _office2 = Office.insert(address="456 Mission St", city_id=san_francisco.id, company=tech_corp.id)
1036
+ _office3 = Office.insert(address="789 Howard St", city_id=san_francisco.id, company=tech_corp.id)
1037
1037
 
1038
1038
  db.commit()
1039
1039
 
@@ -1072,7 +1072,8 @@ def test_nested_join_with_shared_foreign_key():
1072
1072
  f"Office {idx} ('{office.address}'): city relationship is None (BUG!)"
1073
1073
  )
1074
1074
  assert isinstance(office.city_alternative, City), (
1075
- f"Office {idx}: city ({office.city_alternative}) is not a City instance but a {type(office.city_alternative)}"
1075
+ f"Office {idx}: city ({office.city_alternative}) "
1076
+ f"is not a City instance but a {type(office.city_alternative)}"
1076
1077
  )
1077
1078
  assert office.city_alternative.name == "San Francisco", (
1078
1079
  f"Office {idx}: expected 'San Francisco', got '{office.city_alternative.name}'"
@@ -1,92 +0,0 @@
1
- import json
2
- import tempfile
3
-
4
- from contextlib_chdir import chdir
5
- from pydal.validators import IS_EMAIL, IS_NOT_IN_DB
6
-
7
- from src.typedal import TypedTable
8
- from src.typedal.for_py4web import DAL, AuthUser, setup_py4web_tables
9
- from src.typedal.serializers import as_json
10
- from typedal.config import TypeDALConfig
11
-
12
- db = DAL("sqlite:memory")
13
-
14
-
15
- def test_P4W():
16
- assert hasattr(DAL, "on_request")
17
- assert hasattr(db, "on_request")
18
-
19
-
20
- def test_serialize():
21
- @db.define()
22
- class JsonTable(TypedTable):
23
- field: str
24
-
25
- row = JsonTable.insert(field="Hey")
26
-
27
- l = json.loads(as_json.encode(JsonTable))
28
- assert l
29
- assert isinstance(l, dict)
30
- l = json.loads(as_json.encode(row))
31
- assert isinstance(l, dict)
32
- assert l
33
-
34
- l = json.loads(as_json.encode(JsonTable.paginate(limit=1)))
35
-
36
- assert l["pagination"]
37
- assert l["data"]
38
-
39
-
40
- def test_auth_user():
41
- setup_py4web_tables(db)
42
- requirements = AuthUser.email.requires
43
-
44
- assert requirements
45
- assert isinstance(requirements, list)
46
- assert len(requirements) == 2
47
-
48
- assert isinstance(requirements[0], IS_EMAIL)
49
- assert isinstance(requirements[1], IS_NOT_IN_DB)
50
-
51
-
52
- def test_py4web_dal_singleton():
53
- with tempfile.TemporaryDirectory() as d:
54
- with chdir(d):
55
- # note: typedal caching is disabled here because otherwise the cache table might send logs to {d}/log.sql
56
- # which can lead to problems in other tests :(
57
- db_1a = DAL("sqlite:memory", enable_typedal_caching=False)
58
- db_1b = DAL("sqlite:memory", enable_typedal_caching=False)
59
-
60
- db_2a = DAL("sqlite://test_py4web_dal_singleton", folder=d, enable_typedal_caching=False)
61
- db_2b = DAL("sqlite://test_py4web_dal_singleton", folder=d, enable_typedal_caching=False)
62
-
63
- conf = {"database": "sqlite:memory", "dialect": "sqlite", "pyproject": "", "flag_location": f"{d}/flags"}
64
- db_3a = DAL(config=TypeDALConfig.load(conf))
65
- db_3b = DAL(config=TypeDALConfig.load(conf))
66
-
67
- assert db_1a is db_1b
68
- assert db_1a._uri == db_1b._uri
69
- assert db_1a._db_uid == db_1b._db_uid
70
-
71
- assert db_2a is db_2b
72
- assert db_2a._uri == db_2b._uri
73
- assert db_2a._db_uid == db_2b._db_uid
74
-
75
- assert db_1a is not db_2a
76
- assert db_1a._uri != db_2a._uri
77
- assert db_1a._db_uid != db_2a._db_uid
78
-
79
- assert db_1b is not db_2b
80
- assert db_1b._uri != db_2b._uri
81
- assert db_1b._db_uid != db_2b._db_uid
82
-
83
- assert db_3a is db_3b
84
- assert db_3a is not db_1a
85
- assert db_3a is not db_2a
86
-
87
- assert db_1a._uri == db_1b._uri
88
- assert db_3a._uri == db_1a._uri
89
- assert db_3a._uri != db_2a._uri
90
-
91
- # reset singletons for later use:
92
- DAL._clear()
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