TypeDAL 4.5.0__tar.gz → 4.6.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.
Files changed (76) hide show
  1. typedal-4.6.0/.crush/.gitignore +1 -0
  2. typedal-4.6.0/.crush/crush.db-shm +0 -0
  3. typedal-4.6.0/.crush/crush.db-wal +0 -0
  4. typedal-4.6.0/.crush/logs/crush.log +19 -0
  5. {typedal-4.5.0 → typedal-4.6.0}/.github/workflows/su6.yml +1 -1
  6. {typedal-4.5.0 → typedal-4.6.0}/.readthedocs.yml +1 -1
  7. {typedal-4.5.0 → typedal-4.6.0}/CHANGELOG.md +6 -0
  8. {typedal-4.5.0 → typedal-4.6.0}/PKG-INFO +4 -3
  9. {typedal-4.5.0 → typedal-4.6.0}/docs/8_mixins.md +53 -0
  10. {typedal-4.5.0 → typedal-4.6.0}/pyproject.toml +4 -3
  11. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/__about__.py +1 -1
  12. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/caching.py +6 -8
  13. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/cli.py +4 -6
  14. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/core.py +14 -6
  15. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/define.py +2 -3
  16. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/fields.py +4 -4
  17. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/helpers.py +11 -15
  18. typedal-4.6.0/src/typedal/mixins.py +599 -0
  19. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/query_builder.py +4 -5
  20. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/relationships.py +64 -13
  21. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/rows.py +2 -3
  22. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/tables.py +4 -5
  23. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/types.py +19 -24
  24. typedal-4.6.0/tests/__init__.py +0 -0
  25. typedal-4.6.0/tests/test_mixins.py +468 -0
  26. {typedal-4.5.0 → typedal-4.6.0}/tests/test_orm.py +1 -7
  27. {typedal-4.5.0 → typedal-4.6.0}/tests/test_relationships.py +5 -1
  28. typedal-4.5.0/src/typedal/mixins.py +0 -250
  29. typedal-4.5.0/tests/test_mixins.py +0 -166
  30. /typedal-4.5.0/src/typedal/py.typed → /typedal-4.6.0/.crush/init +0 -0
  31. {typedal-4.5.0 → typedal-4.6.0}/.gitignore +0 -0
  32. {typedal-4.5.0 → typedal-4.6.0}/README.md +0 -0
  33. {typedal-4.5.0 → typedal-4.6.0}/coverage.svg +0 -0
  34. {typedal-4.5.0 → typedal-4.6.0}/docs/10_advanced_apis.md +0 -0
  35. {typedal-4.5.0 → typedal-4.6.0}/docs/1_getting_started.md +0 -0
  36. {typedal-4.5.0 → typedal-4.6.0}/docs/2_defining_tables.md +0 -0
  37. {typedal-4.5.0 → typedal-4.6.0}/docs/3_building_queries.md +0 -0
  38. {typedal-4.5.0 → typedal-4.6.0}/docs/4_relationships.md +0 -0
  39. {typedal-4.5.0 → typedal-4.6.0}/docs/5_py4web.md +0 -0
  40. {typedal-4.5.0 → typedal-4.6.0}/docs/6_migrations.md +0 -0
  41. {typedal-4.5.0 → typedal-4.6.0}/docs/7_configuration.md +0 -0
  42. {typedal-4.5.0 → typedal-4.6.0}/docs/9_memoization.md +0 -0
  43. {typedal-4.5.0 → typedal-4.6.0}/docs/css/code_blocks.css +0 -0
  44. {typedal-4.5.0 → typedal-4.6.0}/docs/index.md +0 -0
  45. {typedal-4.5.0 → typedal-4.6.0}/docs/requirements.txt +0 -0
  46. {typedal-4.5.0 → typedal-4.6.0}/example_new.py +0 -0
  47. {typedal-4.5.0 → typedal-4.6.0}/example_old.py +0 -0
  48. {typedal-4.5.0 → typedal-4.6.0}/mkdocs.yml +0 -0
  49. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/__init__.py +0 -0
  50. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/config.py +0 -0
  51. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/constants.py +0 -0
  52. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/for_py4web.py +0 -0
  53. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/for_web2py.py +0 -0
  54. /typedal-4.5.0/tests/__init__.py → /typedal-4.6.0/src/typedal/py.typed +0 -0
  55. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/serializers/as_json.py +0 -0
  56. {typedal-4.5.0 → typedal-4.6.0}/src/typedal/web2py_py4web_shared.py +0 -0
  57. {typedal-4.5.0 → typedal-4.6.0}/tasks.py +0 -0
  58. {typedal-4.5.0 → typedal-4.6.0}/tests/configs/simple.toml +0 -0
  59. {typedal-4.5.0 → typedal-4.6.0}/tests/configs/valid.env +0 -0
  60. {typedal-4.5.0 → typedal-4.6.0}/tests/configs/valid.toml +0 -0
  61. {typedal-4.5.0 → typedal-4.6.0}/tests/py314_tests.py +0 -0
  62. {typedal-4.5.0 → typedal-4.6.0}/tests/test_cli.py +0 -0
  63. {typedal-4.5.0 → typedal-4.6.0}/tests/test_config.py +0 -0
  64. {typedal-4.5.0 → typedal-4.6.0}/tests/test_docs_examples.py +0 -0
  65. {typedal-4.5.0 → typedal-4.6.0}/tests/test_helpers.py +0 -0
  66. {typedal-4.5.0 → typedal-4.6.0}/tests/test_json.py +0 -0
  67. {typedal-4.5.0 → typedal-4.6.0}/tests/test_main.py +0 -0
  68. {typedal-4.5.0 → typedal-4.6.0}/tests/test_mypy.py +0 -0
  69. {typedal-4.5.0 → typedal-4.6.0}/tests/test_py4web.py +0 -0
  70. {typedal-4.5.0 → typedal-4.6.0}/tests/test_query_builder.py +0 -0
  71. {typedal-4.5.0 → typedal-4.6.0}/tests/test_row.py +0 -0
  72. {typedal-4.5.0 → typedal-4.6.0}/tests/test_stats.py +0 -0
  73. {typedal-4.5.0 → typedal-4.6.0}/tests/test_table.py +0 -0
  74. {typedal-4.5.0 → typedal-4.6.0}/tests/test_web2py.py +0 -0
  75. {typedal-4.5.0 → typedal-4.6.0}/tests/test_xx_others.py +0 -0
  76. {typedal-4.5.0 → typedal-4.6.0}/tests/timings.py +0 -0
@@ -0,0 +1 @@
1
+ *
Binary file
Binary file
@@ -0,0 +1,19 @@
1
+ {"time":"2026-03-13T20:12:28.275088774+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":55},"msg":"Fetching providers from Catwalk"}
2
+ {"time":"2026-03-13T20:12:28.486572096+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":63},"msg":"Catwalk providers not modified"}
3
+ {"time":"2026-03-13T20:12:28.486667656+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/config.(*Config).configureProviders","file":"github.com/charmbracelet/crush/internal/config/load.go","line":296},"msg":"Skipping provider due to missing API key","provider":"anthropic"}
4
+ {"time":"2026-03-13T20:12:28.497862335+01:00","level":"INFO","msg":"OK 20250424200609_initial.sql (1.55ms)"}
5
+ {"time":"2026-03-13T20:12:28.498184106+01:00","level":"INFO","msg":"OK 20250515105448_add_summary_message_id.sql (300.3µs)"}
6
+ {"time":"2026-03-13T20:12:28.498510046+01:00","level":"INFO","msg":"OK 20250624000000_add_created_at_indexes.sql (298.08µs)"}
7
+ {"time":"2026-03-13T20:12:28.498811912+01:00","level":"INFO","msg":"OK 20250627000000_add_provider_to_messages.sql (289.51µs)"}
8
+ {"time":"2026-03-13T20:12:28.499108738+01:00","level":"INFO","msg":"OK 20250810000000_add_is_summary_message.sql (245.48µs)"}
9
+ {"time":"2026-03-13T20:12:28.499362065+01:00","level":"INFO","msg":"OK 20250812000000_add_todos_to_sessions.sql (241.95µs)"}
10
+ {"time":"2026-03-13T20:12:28.499726903+01:00","level":"INFO","msg":"OK 20260127000000_add_read_files_table.sql (351.05µs)"}
11
+ {"time":"2026-03-13T20:12:28.499731494+01:00","level":"INFO","msg":"goose: successfully migrated database to version: 20260127000000"}
12
+ {"time":"2026-03-13T20:12:28.50106094+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools/mcp.Initialize","file":"github.com/charmbracelet/crush/internal/agent/tools/mcp/init.go","line":167},"msg":"Initializing MCP clients"}
13
+ {"time":"2026-03-13T20:12:31.915239786+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":55},"msg":"Fetching providers from Catwalk"}
14
+ {"time":"2026-03-13T20:12:32.150078123+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":63},"msg":"Catwalk providers not modified"}
15
+ {"time":"2026-03-13T20:12:32.150303604+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/config.(*Config).configureProviders","file":"github.com/charmbracelet/crush/internal/config/load.go","line":296},"msg":"Skipping provider due to missing API key","provider":"anthropic"}
16
+ {"time":"2026-03-13T20:12:32.15507598+01:00","level":"INFO","msg":"goose: no migrations to run. current version: 20260127000000"}
17
+ {"time":"2026-03-13T20:12:32.161646966+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools/mcp.Initialize","file":"github.com/charmbracelet/crush/internal/agent/tools/mcp/init.go","line":167},"msg":"Initializing MCP clients"}
18
+ {"time":"2026-03-13T20:15:20.65020982+01:00","level":"ERROR","source":{"function":"github.com/charmbracelet/x/powernap/pkg/lsp.startServerProcess.func1","file":"github.com/charmbracelet/x/powernap@v0.1.3/pkg/lsp/client.go","line":650},"msg":"Language server stderr","command":"ty","output":"2026-03-13 20:15:20.650149482 INFO Version: 0.0.20\n"}
19
+ {"time":"2026-03-13T20:15:20.651255948+01:00","level":"ERROR","source":{"function":"github.com/charmbracelet/x/powernap/pkg/lsp.startServerProcess.func1","file":"github.com/charmbracelet/x/powernap@v0.1.3/pkg/lsp/client.go","line":650},"msg":"Language server stderr","command":"ty","output":"2026-03-13 20:15:20.651183033 ERROR Invalid client response: did not contain a result or error (method=workspace/configuration)\n"}
@@ -11,7 +11,7 @@ jobs:
11
11
  - uses: actions/checkout@v3
12
12
  - uses: actions/setup-python@v4
13
13
  with:
14
- python-version: '3.11'
14
+ python-version: '3.12'
15
15
  - uses: yezz123/setup-uv@v4
16
16
  with:
17
17
  uv-venv: ".venv"
@@ -3,7 +3,7 @@ version: 2
3
3
  build:
4
4
  os: ubuntu-22.04
5
5
  tools:
6
- python: "3.11"
6
+ python: "3.13"
7
7
 
8
8
  mkdocs:
9
9
  configuration: mkdocs.yml
@@ -2,6 +2,12 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.6.0 (2026-03-13)
6
+
7
+ ### Documentation
8
+
9
+ * Add section about PydanticMixin ([`d91c504`](https://github.com/trialandsuccess/TypeDAL/commit/d91c50485f7f7dd7a1e04ae93998475a05bb3742))
10
+
5
11
  ## v4.5.0 (2026-03-06)
6
12
 
7
13
  ### Feature
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.5.0
3
+ Version: 4.6.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
@@ -8,13 +8,12 @@ Project-URL: Source, https://github.com/trialandsuccess/TypeDAL
8
8
  Author-email: Robin van der Noord <contact@trialandsuccess.nl>
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Programming Language :: Python
11
- Classifier: Programming Language :: Python :: 3.11
12
11
  Classifier: Programming Language :: Python :: 3.12
13
12
  Classifier: Programming Language :: Python :: 3.13
14
13
  Classifier: Programming Language :: Python :: 3.14
15
14
  Classifier: Programming Language :: Python :: Implementation :: CPython
16
15
  Classifier: Programming Language :: Python :: Implementation :: PyPy
17
- Requires-Python: >=3.11
16
+ Requires-Python: >=3.12
18
17
  Requires-Dist: configurable-json<2
19
18
  Requires-Dist: configuraptor<3,>=2.0.1
20
19
  Requires-Dist: dill<1
@@ -26,6 +25,7 @@ Provides-Extra: all
26
25
  Requires-Dist: edwh-migrate[full]>=0.8.0; extra == 'all'
27
26
  Requires-Dist: py4web; extra == 'all'
28
27
  Requires-Dist: pydal2sql[all]>=1.2.0; extra == 'all'
28
+ Requires-Dist: pydantic<3; extra == 'all'
29
29
  Requires-Dist: questionary; extra == 'all'
30
30
  Requires-Dist: tabulate; extra == 'all'
31
31
  Requires-Dist: tomlkit; extra == 'all'
@@ -36,6 +36,7 @@ Requires-Dist: ewok; extra == 'dev'
36
36
  Requires-Dist: hatch; extra == 'dev'
37
37
  Requires-Dist: mkdocs; extra == 'dev'
38
38
  Requires-Dist: mkdocs-dracula-theme; extra == 'dev'
39
+ Requires-Dist: pydantic<3; extra == 'dev'
39
40
  Requires-Dist: pytest-mypy-testing; extra == 'dev'
40
41
  Requires-Dist: python-semantic-release<8; extra == 'dev'
41
42
  Requires-Dist: requests<2.32; extra == 'dev'
@@ -47,6 +47,58 @@ class MyTable(TypedTable, SlugMixin, slug_field="title"):
47
47
  # Now, whenever you insert a record into MyTable, the 'slug' field will be automatically generated based on the 'title' field.
48
48
  ```
49
49
 
50
+ ## Using `PydanticMixin`
51
+
52
+ The `PydanticMixin` enables seamless integration with Pydantic-based frameworks (like FastAPI) by adding schema generation
53
+ capabilities to your models. Without this mixin, you cannot return TypedTable instances directly as FastAPI responses
54
+ or use them with `pydantic.TypeAdapter`.
55
+
56
+ Add the mixin to enable `model_dump()` for serialization, including support for relationships and computed properties:
57
+
58
+ ```python
59
+ from typedal import TypedTable
60
+ from typedal.mixins import PydanticMixin
61
+
62
+
63
+ class Author(TypedTable, PydanticMixin):
64
+ name: str
65
+
66
+
67
+ class Book(TypedTable, PydanticMixin):
68
+ title: str
69
+ author: Author
70
+
71
+ @property
72
+ def display_title(self) -> str:
73
+ return f"{self.title} by {self.author.name}"
74
+
75
+
76
+ # After inserting records and joining relationships:
77
+ book = Book.where(id=1).join("author").first()
78
+
79
+ # model_dump() serializes the full object graph
80
+ data = book.model_dump()
81
+ # -> {"id": 1, "title": "...", "author": {"id": 1, "name": "..."}, "display_title": "..."}
82
+
83
+ # Use mode="json" for JSON-serializable output (dates as ISO strings, etc.)
84
+ data = book.model_dump(mode="json")
85
+ ```
86
+
87
+ > **Note:** When referencing other TypedTable models from a PydanticMixin class, those models must also include
88
+ > `PydanticMixin`. This ensures the entire object graph can be serialized consistently.
89
+
90
+ With this mixin, TypedTable instances work seamlessly as FastAPI response models:
91
+
92
+ ```python
93
+ from fastapi import FastAPI
94
+
95
+ app = FastAPI()
96
+
97
+ @app.get("/books/{book_id}")
98
+ def get_book(book_id: int) -> Book:
99
+ return Book.where(id=book_id).join("author").first()
100
+ ```
101
+
50
102
  ## Creating Custom Mixins
51
103
 
52
104
  To create your own mixins for additional functionality, follow these steps:
@@ -122,6 +174,7 @@ recent_articles = (
122
174
  By using these mixins, you can enhance the functionality of your models in a modular and reusable manner, saving you
123
175
  time and effort in your development process.
124
176
 
177
+
125
178
  ---
126
179
 
127
180
  Looking to cache expensive function results? Head to [9. Function Memoization](./9_memoization.md) to learn about `db.memoize()`.
@@ -10,7 +10,7 @@ name = "TypeDAL"
10
10
  dynamic = ["version"]
11
11
  description = 'Typing support for PyDAL'
12
12
  readme = "README.md"
13
- requires-python = ">=3.11"
13
+ requires-python = ">=3.12"
14
14
  license-expression = "MIT"
15
15
  keywords = []
16
16
  authors = [
@@ -19,7 +19,6 @@ authors = [
19
19
  classifiers = [
20
20
  "Development Status :: 4 - Beta",
21
21
  "Programming Language :: Python",
22
- "Programming Language :: Python :: 3.11",
23
22
  "Programming Language :: Python :: 3.12",
24
23
  "Programming Language :: Python :: 3.13",
25
24
  "Programming Language :: Python :: 3.14",
@@ -56,6 +55,7 @@ all = [
56
55
  "tabulate",
57
56
  "pydal2sql[all]>=1.2.0",
58
57
  "edwh-migrate[full]>=0.8.0",
58
+ "pydantic < 3",
59
59
  "questionary",
60
60
  "tomlkit",
61
61
  ]
@@ -70,6 +70,7 @@ dev = [
70
70
  "pytest-mypy-testing",
71
71
  "contextlib-chdir",
72
72
  "testcontainers",
73
+ "pydantic < 3",
73
74
  # depends on ->
74
75
  "requests<2.32",
75
76
  # mypy:
@@ -142,7 +143,7 @@ exclude_also = [
142
143
  ]
143
144
 
144
145
  [tool.mypy]
145
- python_version = "3.11"
146
+ python_version = "3.13"
146
147
 
147
148
  # `some: int = None` looks nicer than `some: int | None = None` and pycharm still understands it
148
149
  no_implicit_optional = false # I guess 'strict_optional' should be true, but disable this one because it's double!
@@ -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.5.0"
8
+ __version__ = "4.6.0"
@@ -203,10 +203,6 @@ def _remove_cache(s: Set, tablename: str) -> None:
203
203
  indeces = s.select("id").column("id")
204
204
  remove_cache(indeces, tablename)
205
205
 
206
-
207
- T_TypedTable = t.TypeVar("T_TypedTable", bound=TypedTable)
208
-
209
-
210
206
  def get_expire(
211
207
  expires_at: t.Optional[dt.datetime] = None,
212
208
  ttl: t.Optional[int | dt.timedelta] = None,
@@ -250,7 +246,7 @@ def _insert_cache_entry(
250
246
  db.commit()
251
247
 
252
248
 
253
- def save_to_cache(
249
+ def save_to_cache[T_TypedTable: TypedTable](
254
250
  instance: TypedRows[T_TypedTable],
255
251
  rows: Rows,
256
252
  expires_at: t.Optional[dt.datetime] = None,
@@ -418,8 +414,10 @@ def _expired_and_valid_query() -> tuple[str, str]:
418
414
  return expired_items, valid_items
419
415
 
420
416
 
421
- T = t.TypeVar("T")
422
- Stats = t.TypedDict("Stats", {"total": T, "valid": T, "expired": T})
417
+ class Stats[T](t.TypedDict):
418
+ total: T
419
+ valid: T
420
+ expired: T
423
421
 
424
422
  RowStats = t.TypedDict(
425
423
  "RowStats",
@@ -527,7 +525,7 @@ def calculate_stats(db: "TypeDAL") -> Stats[GenericStats]:
527
525
  }
528
526
 
529
527
 
530
- def memoize(
528
+ def memoize[T: t.Any](
531
529
  db: "TypeDAL",
532
530
  func: t.Callable[..., T],
533
531
  *args: TypedRows[t.Any] | TypedTable,
@@ -94,12 +94,10 @@ questionary_types: dict[typing.Hashable, Optional[AnyDict]] = {
94
94
  "fake_migrate": None, # only enable via config if required
95
95
  }
96
96
 
97
- T = typing.TypeVar("T")
98
-
99
97
  notfound = object()
100
98
 
101
99
 
102
- def _get_question(prop: str, annotation: typing.Type[T]) -> Optional[AnyDict]: # pragma: no cover
100
+ def _get_question[T](prop: str, annotation: typing.Type[T]) -> Optional[AnyDict]: # pragma: no cover
103
101
  question = questionary_types.get(prop, notfound)
104
102
  if question is notfound:
105
103
  # None means skip the question, notfound means use the type default!
@@ -111,7 +109,7 @@ def _get_question(prop: str, annotation: typing.Type[T]) -> Optional[AnyDict]:
111
109
  return question.copy() # type: ignore
112
110
 
113
111
 
114
- def get_question(prop: str, annotation: typing.Type[T], default: T | None) -> Optional[T]: # pragma: no cover
112
+ def get_question[T](prop: str, annotation: typing.Type[T], default: T | None) -> Optional[T]: # pragma: no cover
115
113
  """
116
114
  Generate a question based on a config property and prompt the user for it.
117
115
  """
@@ -449,7 +447,7 @@ def migrations_stub(
449
447
  return 0
450
448
 
451
449
 
452
- AnyNestedDict: typing.TypeAlias = dict[str, AnyDict]
450
+ type AnyNestedDict = dict[str, AnyDict]
453
451
 
454
452
 
455
453
  def tabulate_data(data: AnyNestedDict) -> None:
@@ -466,7 +464,7 @@ def tabulate_data(data: AnyNestedDict) -> None:
466
464
  print(tabulate(flattened_data, headers="keys"))
467
465
 
468
466
 
469
- FormatOptions: typing.TypeAlias = typing.Literal["plaintext", "json", "yaml", "toml"]
467
+ type FormatOptions = typing.Literal["plaintext", "json", "yaml", "toml"]
470
468
 
471
469
 
472
470
  def get_output_format(fmt: FormatOptions) -> typing.Callable[[AnyNestedDict], None]:
@@ -21,7 +21,7 @@ from .helpers import (
21
21
  sql_expression,
22
22
  to_snake,
23
23
  )
24
- from .types import CacheStatus, Field, T, Template # type: ignore
24
+ from .types import CacheStatus, Field, Template # noqa: F401
25
25
 
26
26
  try:
27
27
  # python 3.14+
@@ -246,7 +246,7 @@ class TypeDAL(pydal.DAL):
246
246
  self.try_define(_TypedalCache)
247
247
  self.try_define(_TypedalCacheDependency)
248
248
 
249
- def try_define(self, model: t.Type[T], verbose: bool = False) -> t.Type[T]:
249
+ def try_define[T: t.Any](self, model: t.Type[T], verbose: bool = False) -> t.Type[T]:
250
250
  """
251
251
  Try to define a model with migrate or fall back to fake migrate.
252
252
  """
@@ -270,7 +270,7 @@ class TypeDAL(pydal.DAL):
270
270
  }
271
271
 
272
272
  @t.overload
273
- def define(self, maybe_cls: None = None, **kwargs: t.Any) -> t.Callable[[t.Type[T]], t.Type[T]]:
273
+ def define[T: t.Any](self, maybe_cls: None = None, **kwargs: t.Any) -> t.Callable[[t.Type[T]], t.Type[T]]:
274
274
  """
275
275
  Typing Overload for define without a class.
276
276
 
@@ -279,7 +279,7 @@ class TypeDAL(pydal.DAL):
279
279
  """
280
280
 
281
281
  @t.overload
282
- def define(self, maybe_cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
282
+ def define[T: t.Any](self, maybe_cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
283
283
  """
284
284
  Typing Overload for define with a class.
285
285
 
@@ -287,7 +287,7 @@ class TypeDAL(pydal.DAL):
287
287
  class MyTable(TypedTable): ...
288
288
  """
289
289
 
290
- def define(
290
+ def define[T: t.Any](
291
291
  self,
292
292
  maybe_cls: t.Type[T] | None = None,
293
293
  **kwargs: t.Any,
@@ -379,6 +379,14 @@ class TypeDAL(pydal.DAL):
379
379
  # alias for backward-compatibility
380
380
  return self._builder.class_map
381
381
 
382
+ def _known_classes(self) -> dict[str, t.Type["TypedTable"]]:
383
+ """
384
+ Return currently defined TypedTable classes keyed by class name.
385
+
386
+ Useful when resolving forward references in annotations/relationships.
387
+ """
388
+ return {table.__name__: table for table in self._class_map.values()}
389
+
382
390
  @staticmethod
383
391
  def to_snake(camel: str) -> str:
384
392
  """
@@ -460,7 +468,7 @@ class TypeDAL(pydal.DAL):
460
468
  """
461
469
  return sql_expression(self, sql_fragment, *raw_args, output_type=output_type, **raw_kwargs)
462
470
 
463
- def memoize(
471
+ def memoize[T: t.Any](
464
472
  self,
465
473
  func: t.Callable[..., T],
466
474
  # should be TypedRows[TypedTable] or TypedTable but for some reason that breaks
@@ -29,7 +29,6 @@ from .relationships import Relationship, to_relationship
29
29
  from .tables import TypedTable
30
30
  from .types import (
31
31
  Field,
32
- T,
33
32
  T_annotation,
34
33
  Table,
35
34
  _Types,
@@ -53,7 +52,7 @@ class TableDefinitionBuilder:
53
52
  self.db = db
54
53
  self.class_map: dict[str, t.Type["TypedTable"]] = {}
55
54
 
56
- def define(self, cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
55
+ def define[T: t.Any](self, cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
57
56
  """Build and register a table from a TypedTable class."""
58
57
  full_dict = all_dict(cls)
59
58
  tablename = to_snake(cls.__name__)
@@ -133,7 +132,7 @@ class TableDefinitionBuilder:
133
132
  """Convert Python type annotation to pydal field type string."""
134
133
  ftype = t.cast(type, ftype_annotation) # cast from Type to type to make mypy happy)
135
134
 
136
- known_classes = {table.__name__: table for table in self.class_map.values()}
135
+ known_classes = self.db._known_classes()
137
136
 
138
137
  if isinstance(ftype, str):
139
138
  # extract type from string
@@ -24,8 +24,6 @@ from .types import (
24
24
  Query,
25
25
  T_annotation,
26
26
  T_MetaInstance,
27
- T_subclass,
28
- T_Value,
29
27
  Validator,
30
28
  )
31
29
 
@@ -37,7 +35,7 @@ if t.TYPE_CHECKING:
37
35
  ## general
38
36
 
39
37
 
40
- class TypedField(Expression, t.Generic[T_Value]): # pragma: no cover
38
+ class TypedField[T_Value](Expression): # pragma: no cover
41
39
  """
42
40
  Typed version of pydal.Field, which will be converted to a normal Field in the background.
43
41
  """
@@ -375,7 +373,9 @@ def UploadField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
375
373
  Upload = UploadField
376
374
 
377
375
 
378
- def ReferenceField(
376
+ def ReferenceField[
377
+ T_subclass: (TypedTable, Table)
378
+ ](
379
379
  other_table: str | t.Type[TypedTable] | TypedTable | Table | T_subclass,
380
380
  **kw: t.Unpack[FieldSettings],
381
381
  ) -> TypedField[int]:
@@ -15,7 +15,7 @@ from collections import ChainMap
15
15
 
16
16
  from pydal import DAL
17
17
 
18
- from .types import AnyDict, Expression, Field, Row, T, Table, Template # type: ignore
18
+ from .types import AnyDict, Expression, Field, Row, Table, Template
19
19
 
20
20
  try:
21
21
  import annotationlib
@@ -91,7 +91,7 @@ def all_annotations(cls: type, _except: t.Optional[t.Iterable[str]] = None) -> d
91
91
  return {k: v for k, v in _all.items() if k not in _except}
92
92
 
93
93
 
94
- def instanciate(cls: t.Type[T] | T, with_args: bool = False) -> T:
94
+ def instanciate[T: t.Any](cls: t.Type[T] | T, with_args: bool = False) -> T:
95
95
  """
96
96
  Create an instance of T (if it is a class).
97
97
 
@@ -173,10 +173,6 @@ def mktable(
173
173
  return output.getvalue()
174
174
 
175
175
 
176
- K = t.TypeVar("K")
177
- V = t.TypeVar("V")
178
-
179
-
180
176
  def looks_like(v: t.Any, _type: type[t.Any]) -> bool:
181
177
  """
182
178
  Returns true if v or v's class is of type _type, including if it is a generic.
@@ -189,7 +185,7 @@ def looks_like(v: t.Any, _type: type[t.Any]) -> bool:
189
185
  return isinstance(v, _type) or (isinstance(v, type) and issubclass(v, _type)) or origin_is_subclass(v, _type)
190
186
 
191
187
 
192
- def filter_out(mut_dict: dict[K, V], _type: type[T]) -> dict[K, T]:
188
+ def filter_out[K, V, T](mut_dict: dict[K, V], _type: type[T]) -> dict[K, T]:
193
189
  """
194
190
  Split a dictionary into things matching _type and the rest.
195
191
 
@@ -211,20 +207,20 @@ def unwrap_type(_type: type) -> type:
211
207
 
212
208
 
213
209
  @t.overload
214
- def extract_type_optional(annotation: T) -> tuple[T, bool]:
210
+ def extract_type_optional[T](annotation: T) -> tuple[T, bool]:
215
211
  """
216
212
  T -> T is not exactly right because you'll get the inner type, but mypy seems happy with this.
217
213
  """
218
214
 
219
215
 
220
216
  @t.overload
221
- def extract_type_optional(annotation: None) -> tuple[None, bool]:
217
+ def extract_type_optional[T](annotation: None) -> tuple[None, bool]:
222
218
  """
223
219
  None leads to None, False.
224
220
  """
225
221
 
226
222
 
227
- def extract_type_optional(annotation: T | None) -> tuple[T | None, bool]:
223
+ def extract_type_optional[T](annotation: T | None) -> tuple[T | None, bool]:
228
224
  """
229
225
  Given an annotation, extract the actual type and whether it is optional.
230
226
  """
@@ -256,13 +252,13 @@ class DummyQuery:
256
252
  Placeholder to &= and |= actual query parts.
257
253
  """
258
254
 
259
- def __or__(self, other: T) -> T:
255
+ def __or__[T](self, other: T) -> T:
260
256
  """
261
257
  For 'or': DummyQuery | Other == Other.
262
258
  """
263
259
  return other
264
260
 
265
- def __and__(self, other: T) -> T:
261
+ def __and__[T](self, other: T) -> T:
266
262
  """
267
263
  For 'and': DummyQuery & Other == Other.
268
264
  """
@@ -275,7 +271,7 @@ class DummyQuery:
275
271
  return False
276
272
 
277
273
 
278
- def as_lambda(value: T) -> t.Callable[..., T]:
274
+ def as_lambda[T](value: T) -> t.Callable[..., T]:
279
275
  """
280
276
  Wrap value in a callable.
281
277
  """
@@ -342,7 +338,7 @@ class classproperty:
342
338
  """
343
339
  self.fget = fget
344
340
 
345
- def __get__(self, obj: t.Any, owner: t.Type[T]) -> t.Any:
341
+ def __get__[T](self, obj: t.Any, owner: t.Type[T]) -> t.Any:
346
342
  """
347
343
  Retrieve the property value.
348
344
 
@@ -589,7 +585,7 @@ def normalize_table_keys(row: Row, pattern: re.Pattern[str] = re.compile(r"^([a-
589
585
  return Row(new_data)
590
586
 
591
587
 
592
- def default_representer(field: TypedField[T], value: T, table: t.Type[TypedTable]) -> str:
588
+ def default_representer[T: t.Any](field: TypedField[T], value: T, table: t.Type[TypedTable]) -> str:
593
589
  """
594
590
  Simply call field.represent on the value.
595
591
  """