TypeDAL 3.12.2__tar.gz → 3.13.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.

Potentially problematic release.


This version of TypeDAL might be problematic. Click here for more details.

Files changed (58) hide show
  1. {typedal-3.12.2 → typedal-3.13.0}/CHANGELOG.md +6 -0
  2. {typedal-3.12.2 → typedal-3.13.0}/PKG-INFO +2 -2
  3. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/__about__.py +1 -1
  4. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/core.py +87 -0
  5. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/helpers.py +24 -0
  6. {typedal-3.12.2 → typedal-3.13.0}/tests/test_main.py +39 -0
  7. {typedal-3.12.2 → typedal-3.13.0}/.github/workflows/su6.yml +0 -0
  8. {typedal-3.12.2 → typedal-3.13.0}/.gitignore +0 -0
  9. {typedal-3.12.2 → typedal-3.13.0}/.readthedocs.yml +0 -0
  10. {typedal-3.12.2 → typedal-3.13.0}/README.md +0 -0
  11. {typedal-3.12.2 → typedal-3.13.0}/coverage.svg +0 -0
  12. {typedal-3.12.2 → typedal-3.13.0}/docs/1_getting_started.md +0 -0
  13. {typedal-3.12.2 → typedal-3.13.0}/docs/2_defining_tables.md +0 -0
  14. {typedal-3.12.2 → typedal-3.13.0}/docs/3_building_queries.md +0 -0
  15. {typedal-3.12.2 → typedal-3.13.0}/docs/4_relationships.md +0 -0
  16. {typedal-3.12.2 → typedal-3.13.0}/docs/5_py4web.md +0 -0
  17. {typedal-3.12.2 → typedal-3.13.0}/docs/6_migrations.md +0 -0
  18. {typedal-3.12.2 → typedal-3.13.0}/docs/7_mixins.md +0 -0
  19. {typedal-3.12.2 → typedal-3.13.0}/docs/css/code_blocks.css +0 -0
  20. {typedal-3.12.2 → typedal-3.13.0}/docs/index.md +0 -0
  21. {typedal-3.12.2 → typedal-3.13.0}/docs/requirements.txt +0 -0
  22. {typedal-3.12.2 → typedal-3.13.0}/example_new.py +0 -0
  23. {typedal-3.12.2 → typedal-3.13.0}/example_old.py +0 -0
  24. {typedal-3.12.2 → typedal-3.13.0}/mkdocs.yml +0 -0
  25. {typedal-3.12.2 → typedal-3.13.0}/pyproject.toml +0 -0
  26. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/__init__.py +0 -0
  27. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/caching.py +0 -0
  28. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/cli.py +0 -0
  29. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/config.py +0 -0
  30. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/fields.py +0 -0
  31. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/for_py4web.py +0 -0
  32. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/for_web2py.py +0 -0
  33. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/mixins.py +0 -0
  34. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/py.typed +0 -0
  35. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/serializers/as_json.py +0 -0
  36. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/types.py +0 -0
  37. {typedal-3.12.2 → typedal-3.13.0}/src/typedal/web2py_py4web_shared.py +0 -0
  38. {typedal-3.12.2 → typedal-3.13.0}/tests/__init__.py +0 -0
  39. {typedal-3.12.2 → typedal-3.13.0}/tests/configs/simple.toml +0 -0
  40. {typedal-3.12.2 → typedal-3.13.0}/tests/configs/valid.env +0 -0
  41. {typedal-3.12.2 → typedal-3.13.0}/tests/configs/valid.toml +0 -0
  42. {typedal-3.12.2 → typedal-3.13.0}/tests/test_cli.py +0 -0
  43. {typedal-3.12.2 → typedal-3.13.0}/tests/test_config.py +0 -0
  44. {typedal-3.12.2 → typedal-3.13.0}/tests/test_docs_examples.py +0 -0
  45. {typedal-3.12.2 → typedal-3.13.0}/tests/test_helpers.py +0 -0
  46. {typedal-3.12.2 → typedal-3.13.0}/tests/test_json.py +0 -0
  47. {typedal-3.12.2 → typedal-3.13.0}/tests/test_mixins.py +0 -0
  48. {typedal-3.12.2 → typedal-3.13.0}/tests/test_mypy.py +0 -0
  49. {typedal-3.12.2 → typedal-3.13.0}/tests/test_orm.py +0 -0
  50. {typedal-3.12.2 → typedal-3.13.0}/tests/test_py4web.py +0 -0
  51. {typedal-3.12.2 → typedal-3.13.0}/tests/test_query_builder.py +0 -0
  52. {typedal-3.12.2 → typedal-3.13.0}/tests/test_relationships.py +0 -0
  53. {typedal-3.12.2 → typedal-3.13.0}/tests/test_row.py +0 -0
  54. {typedal-3.12.2 → typedal-3.13.0}/tests/test_stats.py +0 -0
  55. {typedal-3.12.2 → typedal-3.13.0}/tests/test_table.py +0 -0
  56. {typedal-3.12.2 → typedal-3.13.0}/tests/test_web2py.py +0 -0
  57. {typedal-3.12.2 → typedal-3.13.0}/tests/test_xx_others.py +0 -0
  58. {typedal-3.12.2 → typedal-3.13.0}/tests/timings.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v3.13.0 (2025-04-28)
6
+
7
+ ### Feature
8
+
9
+ * Adding `_once` hooks ([`a69fbb3`](https://github.com/trialandsuccess/TypeDAL/commit/a69fbb361cdfec9352fca503206299c5bbb940d2))
10
+
5
11
  ## v3.12.2 (2025-04-25)
6
12
 
7
13
  ### Fix
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 3.12.2
3
+ Version: 3.13.0
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__ = "3.12.2"
8
+ __version__ = "3.13.0"
@@ -5,6 +5,7 @@ Core functionality of TypeDAL.
5
5
  import contextlib
6
6
  import csv
7
7
  import datetime as dt
8
+ import functools
8
9
  import inspect
9
10
  import json
10
11
  import math
@@ -33,6 +34,7 @@ from .helpers import (
33
34
  all_annotations,
34
35
  all_dict,
35
36
  as_lambda,
37
+ classproperty,
36
38
  extract_type_optional,
37
39
  filter_out,
38
40
  instanciate,
@@ -796,6 +798,10 @@ class TypeDAL(pydal.DAL): # type: ignore
796
798
  return to_snake(camel)
797
799
 
798
800
 
801
+ P = typing.ParamSpec("P")
802
+ R = typing.TypeVar("R")
803
+
804
+
799
805
  class TableMeta(type):
800
806
  """
801
807
  This metaclass contains functionality on table classes, that doesn't exist on its instances.
@@ -1196,6 +1202,19 @@ class TableMeta(type):
1196
1202
  return self.with_alias(key)
1197
1203
 
1198
1204
  # hooks:
1205
+ def _hook_once(
1206
+ cls: Type[T_MetaInstance], hooks: list[typing.Callable[P, R]], fn: typing.Callable[P, R]
1207
+ ) -> Type[T_MetaInstance]:
1208
+ @functools.wraps(fn)
1209
+ def wraps(*a: P.args, **kw: P.kwargs) -> R:
1210
+ try:
1211
+ return fn(*a, **kw)
1212
+ finally:
1213
+ hooks.remove(wraps)
1214
+
1215
+ hooks.append(wraps)
1216
+ return cls
1217
+
1199
1218
  def before_insert(
1200
1219
  cls: Type[T_MetaInstance],
1201
1220
  fn: typing.Callable[[T_MetaInstance], Optional[bool]] | typing.Callable[[OpRow], Optional[bool]],
@@ -1207,6 +1226,15 @@ class TableMeta(type):
1207
1226
  cls._before_insert.append(fn)
1208
1227
  return cls
1209
1228
 
1229
+ def before_insert_once(
1230
+ cls: Type[T_MetaInstance],
1231
+ fn: typing.Callable[[T_MetaInstance], Optional[bool]] | typing.Callable[[OpRow], Optional[bool]],
1232
+ ) -> Type[T_MetaInstance]:
1233
+ """
1234
+ Add a before insert hook that only fires once and then removes itself.
1235
+ """
1236
+ return cls._hook_once(cls._before_insert, fn) # type: ignore
1237
+
1210
1238
  def after_insert(
1211
1239
  cls: Type[T_MetaInstance],
1212
1240
  fn: (
@@ -1221,6 +1249,18 @@ class TableMeta(type):
1221
1249
  cls._after_insert.append(fn)
1222
1250
  return cls
1223
1251
 
1252
+ def after_insert_once(
1253
+ cls: Type[T_MetaInstance],
1254
+ fn: (
1255
+ typing.Callable[[T_MetaInstance, Reference], Optional[bool]]
1256
+ | typing.Callable[[OpRow, Reference], Optional[bool]]
1257
+ ),
1258
+ ) -> Type[T_MetaInstance]:
1259
+ """
1260
+ Add an after insert hook that only fires once and then removes itself.
1261
+ """
1262
+ return cls._hook_once(cls._after_insert, fn) # type: ignore
1263
+
1224
1264
  def before_update(
1225
1265
  cls: Type[T_MetaInstance],
1226
1266
  fn: typing.Callable[[Set, T_MetaInstance], Optional[bool]] | typing.Callable[[Set, OpRow], Optional[bool]],
@@ -1232,6 +1272,15 @@ class TableMeta(type):
1232
1272
  cls._before_update.append(fn)
1233
1273
  return cls
1234
1274
 
1275
+ def before_update_once(
1276
+ cls,
1277
+ fn: typing.Callable[[Set, T_MetaInstance], Optional[bool]] | typing.Callable[[Set, OpRow], Optional[bool]],
1278
+ ) -> Type[T_MetaInstance]:
1279
+ """
1280
+ Add a before update hook that only fires once and then removes itself.
1281
+ """
1282
+ return cls._hook_once(cls._before_update, fn) # type: ignore
1283
+
1235
1284
  def after_update(
1236
1285
  cls: Type[T_MetaInstance],
1237
1286
  fn: typing.Callable[[Set, T_MetaInstance], Optional[bool]] | typing.Callable[[Set, OpRow], Optional[bool]],
@@ -1243,6 +1292,15 @@ class TableMeta(type):
1243
1292
  cls._after_update.append(fn)
1244
1293
  return cls
1245
1294
 
1295
+ def after_update_once(
1296
+ cls: Type[T_MetaInstance],
1297
+ fn: typing.Callable[[Set, T_MetaInstance], Optional[bool]] | typing.Callable[[Set, OpRow], Optional[bool]],
1298
+ ) -> Type[T_MetaInstance]:
1299
+ """
1300
+ Add an after update hook that only fires once and then removes itself.
1301
+ """
1302
+ return cls._hook_once(cls._after_update, fn) # type: ignore
1303
+
1246
1304
  def before_delete(cls: Type[T_MetaInstance], fn: typing.Callable[[Set], Optional[bool]]) -> Type[T_MetaInstance]:
1247
1305
  """
1248
1306
  Add a before delete hook.
@@ -1251,6 +1309,15 @@ class TableMeta(type):
1251
1309
  cls._before_delete.append(fn)
1252
1310
  return cls
1253
1311
 
1312
+ def before_delete_once(
1313
+ cls: Type[T_MetaInstance],
1314
+ fn: typing.Callable[[Set], Optional[bool]],
1315
+ ) -> Type[T_MetaInstance]:
1316
+ """
1317
+ Add a before delete hook that only fires once and then removes itself.
1318
+ """
1319
+ return cls._hook_once(cls._before_delete, fn)
1320
+
1254
1321
  def after_delete(cls: Type[T_MetaInstance], fn: typing.Callable[[Set], Optional[bool]]) -> Type[T_MetaInstance]:
1255
1322
  """
1256
1323
  Add an after delete hook.
@@ -1259,6 +1326,15 @@ class TableMeta(type):
1259
1326
  cls._after_delete.append(fn)
1260
1327
  return cls
1261
1328
 
1329
+ def after_delete_once(
1330
+ cls: Type[T_MetaInstance],
1331
+ fn: typing.Callable[[Set], Optional[bool]],
1332
+ ) -> Type[T_MetaInstance]:
1333
+ """
1334
+ Add an after delete hook that only fires once and then removes itself.
1335
+ """
1336
+ return cls._hook_once(cls._after_delete, fn)
1337
+
1262
1338
 
1263
1339
  class TypedField(Expression, typing.Generic[T_Value]): # pragma: no cover
1264
1340
  """
@@ -1482,6 +1558,17 @@ class _TypedTable:
1482
1558
  where you need a reference to the current database, which may not exist yet when defining the model.
1483
1559
  """
1484
1560
 
1561
+ @classproperty
1562
+ def _hooks(cls) -> dict[str, list[typing.Callable[..., Optional[bool]]]]:
1563
+ return {
1564
+ "before_insert": cls._before_insert,
1565
+ "after_insert": cls._after_insert,
1566
+ "before_update": cls._before_update,
1567
+ "after_update": cls._after_update,
1568
+ "before_delete": cls._before_delete,
1569
+ "after_delete": cls._after_delete,
1570
+ }
1571
+
1485
1572
 
1486
1573
  class TypedTable(_TypedTable, metaclass=TableMeta):
1487
1574
  """
@@ -303,3 +303,27 @@ def get_field(field: "TypedField[typing.Any] | Field") -> "Field":
303
303
  "Field",
304
304
  field, # Table.field already is a Field, but cast to make sure the editor knows this too.
305
305
  )
306
+
307
+
308
+ class classproperty:
309
+ def __init__(self, fget: typing.Callable[..., typing.Any]) -> None:
310
+ """
311
+ Initialize the classproperty.
312
+
313
+ Args:
314
+ fget: A function that takes the class as an argument and returns a value.
315
+ """
316
+ self.fget = fget
317
+
318
+ def __get__(self, obj: typing.Any, owner: typing.Type[T]) -> typing.Any:
319
+ """
320
+ Retrieve the property value.
321
+
322
+ Args:
323
+ obj: The instance of the class (unused).
324
+ owner: The class that owns the property.
325
+
326
+ Returns:
327
+ The value returned by the function.
328
+ """
329
+ return self.fget(owner)
@@ -510,10 +510,49 @@ def test_hooks_duplicates():
510
510
  HookedTableV3.after_insert(increase_counter_v2) # other hash
511
511
 
512
512
  HookedTableV3.insert(name="Should increase counter twice")
513
+ assert counter == 3
514
+
515
+ for hook in HookedTableV3._hooks.values():
516
+ hook.clear()
513
517
 
518
+ HookedTableV3.insert(name="Should NOT increase counter")
514
519
  assert counter == 3
515
520
 
516
521
 
522
+ def test_hooks_once():
523
+ @db.define()
524
+ class HookedTableV4(TypedTable):
525
+ name: str
526
+
527
+ counter = 0
528
+
529
+ def increase_counter_v2(_, __=None):
530
+ nonlocal counter
531
+ counter += 1
532
+
533
+ HookedTableV4.before_insert_once(increase_counter_v2)
534
+ HookedTableV4.after_insert_once(increase_counter_v2)
535
+ HookedTableV4.before_update_once(increase_counter_v2)
536
+ HookedTableV4.after_update_once(increase_counter_v2)
537
+ HookedTableV4.before_delete_once(increase_counter_v2)
538
+ HookedTableV4.after_delete_once(increase_counter_v2)
539
+
540
+ assert counter == 0
541
+
542
+ HookedTableV4.insert(name="1")
543
+ assert counter == 2
544
+ row = HookedTableV4.insert(name="2")
545
+ assert counter == 2
546
+
547
+ row.update_record(name="3")
548
+ assert counter == 4
549
+ row.update_record(name="4")
550
+ assert counter == 4
551
+
552
+ row.delete_record()
553
+ assert counter == 6
554
+
555
+
517
556
  def test_try():
518
557
  class SomeTableToRetry(TypedTable):
519
558
  key: int
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