TypeDAL 4.8.0__tar.gz → 4.8.2__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.8.0 → typedal-4.8.2}/CHANGELOG.md +12 -0
  2. {typedal-4.8.0 → typedal-4.8.2}/PKG-INFO +3 -2
  3. typedal-4.8.2/coverage.svg +1 -0
  4. {typedal-4.8.0 → typedal-4.8.2}/pyproject.toml +9 -2
  5. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/__about__.py +1 -1
  6. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/core.py +32 -2
  7. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/fields.py +6 -6
  8. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/query_builder.py +2 -2
  9. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/relationships.py +40 -19
  10. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/tables.py +3 -2
  11. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/types.py +14 -9
  12. typedal-4.8.2/tests/test_typing_mypy.md +250 -0
  13. typedal-4.8.2/tests/test_typing_pyright.md +225 -0
  14. typedal-4.8.2/typing_again.py +35 -0
  15. typedal-4.8.0/coverage.svg +0 -1
  16. {typedal-4.8.0 → typedal-4.8.2}/.github/workflows/su6.yml +0 -0
  17. {typedal-4.8.0 → typedal-4.8.2}/.gitignore +0 -0
  18. {typedal-4.8.0 → typedal-4.8.2}/.readthedocs.yml +0 -0
  19. {typedal-4.8.0 → typedal-4.8.2}/README.md +0 -0
  20. {typedal-4.8.0 → typedal-4.8.2}/docs/10_advanced_apis.md +0 -0
  21. {typedal-4.8.0 → typedal-4.8.2}/docs/1_getting_started.md +0 -0
  22. {typedal-4.8.0 → typedal-4.8.2}/docs/2_defining_tables.md +0 -0
  23. {typedal-4.8.0 → typedal-4.8.2}/docs/3_building_queries.md +0 -0
  24. {typedal-4.8.0 → typedal-4.8.2}/docs/4_relationships.md +0 -0
  25. {typedal-4.8.0 → typedal-4.8.2}/docs/5_py4web.md +0 -0
  26. {typedal-4.8.0 → typedal-4.8.2}/docs/6_migrations.md +0 -0
  27. {typedal-4.8.0 → typedal-4.8.2}/docs/7_configuration.md +0 -0
  28. {typedal-4.8.0 → typedal-4.8.2}/docs/8_mixins.md +0 -0
  29. {typedal-4.8.0 → typedal-4.8.2}/docs/9_memoization.md +0 -0
  30. {typedal-4.8.0 → typedal-4.8.2}/docs/css/code_blocks.css +0 -0
  31. {typedal-4.8.0 → typedal-4.8.2}/docs/index.md +0 -0
  32. {typedal-4.8.0 → typedal-4.8.2}/docs/requirements.txt +0 -0
  33. {typedal-4.8.0 → typedal-4.8.2}/example_new.py +0 -0
  34. {typedal-4.8.0 → typedal-4.8.2}/example_old.py +0 -0
  35. {typedal-4.8.0 → typedal-4.8.2}/mkdocs.yml +0 -0
  36. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/__init__.py +0 -0
  37. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/caching.py +0 -0
  38. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/cli.py +0 -0
  39. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/config.py +0 -0
  40. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/constants.py +0 -0
  41. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/define.py +0 -0
  42. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/enum_helpers.py +0 -0
  43. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/for_py4web.py +0 -0
  44. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/for_web2py.py +0 -0
  45. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/helpers.py +0 -0
  46. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/mixins.py +0 -0
  47. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/py.typed +0 -0
  48. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/rows.py +0 -0
  49. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/serializers/as_json.py +0 -0
  50. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/serializers/typescript.py +0 -0
  51. {typedal-4.8.0 → typedal-4.8.2}/src/typedal/web2py_py4web_shared.py +0 -0
  52. {typedal-4.8.0 → typedal-4.8.2}/tasks.py +0 -0
  53. {typedal-4.8.0 → typedal-4.8.2}/tests/__init__.py +0 -0
  54. {typedal-4.8.0 → typedal-4.8.2}/tests/configs/simple.toml +0 -0
  55. {typedal-4.8.0 → typedal-4.8.2}/tests/configs/valid.env +0 -0
  56. {typedal-4.8.0 → typedal-4.8.2}/tests/configs/valid.toml +0 -0
  57. {typedal-4.8.0 → typedal-4.8.2}/tests/py314_tests.py +0 -0
  58. {typedal-4.8.0 → typedal-4.8.2}/tests/test_cli.py +0 -0
  59. {typedal-4.8.0 → typedal-4.8.2}/tests/test_config.py +0 -0
  60. {typedal-4.8.0 → typedal-4.8.2}/tests/test_docs_examples.py +0 -0
  61. {typedal-4.8.0 → typedal-4.8.2}/tests/test_helpers.py +0 -0
  62. {typedal-4.8.0 → typedal-4.8.2}/tests/test_json.py +0 -0
  63. {typedal-4.8.0 → typedal-4.8.2}/tests/test_main.py +0 -0
  64. {typedal-4.8.0 → typedal-4.8.2}/tests/test_mixins.py +0 -0
  65. {typedal-4.8.0 → typedal-4.8.2}/tests/test_mypy.py +0 -0
  66. {typedal-4.8.0 → typedal-4.8.2}/tests/test_orm.py +0 -0
  67. {typedal-4.8.0 → typedal-4.8.2}/tests/test_py4web.py +0 -0
  68. {typedal-4.8.0 → typedal-4.8.2}/tests/test_query_builder.py +0 -0
  69. {typedal-4.8.0 → typedal-4.8.2}/tests/test_relationships.py +0 -0
  70. {typedal-4.8.0 → typedal-4.8.2}/tests/test_row.py +0 -0
  71. {typedal-4.8.0 → typedal-4.8.2}/tests/test_stats.py +0 -0
  72. {typedal-4.8.0 → typedal-4.8.2}/tests/test_table.py +0 -0
  73. {typedal-4.8.0 → typedal-4.8.2}/tests/test_typescript.py +0 -0
  74. {typedal-4.8.0 → typedal-4.8.2}/tests/test_web2py.py +0 -0
  75. {typedal-4.8.0 → typedal-4.8.2}/tests/test_xx_others.py +0 -0
  76. {typedal-4.8.0 → typedal-4.8.2}/tests/timings.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.8.2 (2026-05-20)
6
+
7
+ ### Fix
8
+
9
+ * **typing:** Improved LSP support + more type testing ([#10](https://github.com/trialandsuccess/TypeDAL/issues/10)) ([`ff6fc2e`](https://github.com/trialandsuccess/TypeDAL/commit/ff6fc2e1706d5cc37719c87b8b8ee565dd31cda3))
10
+
11
+ ## v4.8.1 (2026-05-10)
12
+
13
+ ### Fix
14
+
15
+ * Extend from empty type-safe `DALProtocol` during type-checking to make some LSPs happier ([`d93a361`](https://github.com/trialandsuccess/TypeDAL/commit/d93a361e9caa6c9b94d6fbfa78644c8ff5e2bc23))
16
+
5
17
  ## v4.8.0 (2026-04-23)
6
18
 
7
19
  ### Feature
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.8.0
3
+ Version: 4.8.2
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
@@ -44,7 +44,8 @@ Requires-Dist: hatch; extra == 'dev'
44
44
  Requires-Dist: mkdocs; extra == 'dev'
45
45
  Requires-Dist: mkdocs-dracula-theme; extra == 'dev'
46
46
  Requires-Dist: pydantic<3; extra == 'dev'
47
- Requires-Dist: pytest-mypy-testing; extra == 'dev'
47
+ Requires-Dist: pyright<1.1.400; extra == 'dev'
48
+ Requires-Dist: pytest-typing; extra == 'dev'
48
49
  Requires-Dist: python-semantic-release<8; extra == 'dev'
49
50
  Requires-Dist: requests<2.32; extra == 'dev'
50
51
  Requires-Dist: su6[all]>=1.9.0; extra == 'dev'
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="122" height="20" role="img" aria-label="coverage: 100.00%"><title>coverage: 100.00%</title><filter id="blur"><feGaussianBlur in="SourceGraphic" stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="122" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="61" height="20" fill="#4b0"/><rect width="122" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="510">coverage</text><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="905" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="510">100.00%</text><text aria-hidden="true" x="905" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">100.00%</text><text x="905" y="140" transform="scale(.1)" fill="#fff" textLength="510">100.00%</text></g></svg>
@@ -91,7 +91,9 @@ dev = [
91
91
  # test:
92
92
  "su6[all]>=1.9.0",
93
93
  "python-semantic-release < 8",
94
- "pytest-mypy-testing",
94
+ # "pytest-mypy-testing",
95
+ "pytest-typing",
96
+ "pyright < 1.1.400",
95
97
  "contextlib-chdir",
96
98
  "testcontainers",
97
99
  "pydantic < 3",
@@ -263,11 +265,16 @@ add_ignore = [
263
265
  ## look nowhere for any code to 'build' since this is just used to manage (dev) dependencies
264
266
  #where = []
265
267
 
266
- [tool.pytest.ini_options]
268
+ [tool.pytest]
267
269
  pythonpath = [
268
270
  "src",
269
271
  ]
270
272
 
273
+ typing_checkers = [
274
+ "mypy",
275
+ "pyright",
276
+ ]
277
+
271
278
  [tool.typedal]
272
279
  # e.g.
273
280
  # lazy-policy = "forbid"
@@ -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.8.0"
8
+ __version__ = "4.8.2"
@@ -37,7 +37,7 @@ except ImportError: # pragma: no cover
37
37
  if t.TYPE_CHECKING:
38
38
  from .fields import TypedField
39
39
  from .query_builder import QueryBuilder
40
- from .types import AnyDict, Expression, Rows, T_Query, Table
40
+ from .types import AnyDict, Expression, Rows, Set, T_Query, Table
41
41
 
42
42
 
43
43
  # note: these functions can not be moved to a different file,
@@ -144,7 +144,37 @@ def resolve_annotation(ftype: str, namespace: dict[str, type] | None = None) ->
144
144
  return resolve_annotation_314(ftype, namespace=namespace)
145
145
 
146
146
 
147
- class TypeDAL(pydal.DAL):
147
+ if t.TYPE_CHECKING:
148
+
149
+ class _TypeDALBase:
150
+ # attributes accessed throughout the codebase
151
+ _adapter: t.Any
152
+ _migrate: t.Any
153
+ representers: t.Any
154
+
155
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: ...
156
+
157
+ def __call__(self, query: t.Any = None) -> "Set": ...
158
+
159
+ def commit(self) -> None: ...
160
+
161
+ def rollback(self) -> None: ...
162
+
163
+ def define_table(self, *args: t.Any, **kwargs: t.Any) -> "Table": ...
164
+
165
+ def has_representer(self, field_type: str) -> bool: ...
166
+
167
+ # pydal exposes dynamic table attributes like `db.my_table`.
168
+ # this keeps type checkers from flagging these as missing attributes.
169
+ def __getattr__(self, item: str) -> "Table": ...
170
+
171
+ else:
172
+
173
+ class _TypeDALBase(pydal.DAL):
174
+ pass
175
+
176
+
177
+ class TypeDAL(_TypeDALBase):
148
178
  """
149
179
  Drop-in replacement for pyDAL with layer to convert class-based table definitions to classical pydal define_tables.
150
180
  """
@@ -29,7 +29,7 @@ from .types import (
29
29
 
30
30
  if t.TYPE_CHECKING:
31
31
  # will be imported for real later:
32
- from .tables import TypedTable, _TypedTable
32
+ from .tables import TypedTable
33
33
 
34
34
 
35
35
  ## general
@@ -71,21 +71,21 @@ class TypedField[T_Value](Expression): # pragma: no cover
71
71
  # super().__init__()
72
72
 
73
73
  @t.overload
74
- def __get__(self, instance: T_MetaInstance, owner: t.Type[T_MetaInstance]) -> T_Value: # pragma: no cover
74
+ def __get__(self, instance: None, owner: "t.Type[t.Any]") -> "TypedField[T_Value]": # pragma: no cover
75
75
  """
76
- row.field -> (actual data).
76
+ Table.field -> Field.
77
77
  """
78
78
 
79
79
  @t.overload
80
- def __get__(self, instance: None, owner: "t.Type[_TypedTable]") -> "TypedField[T_Value]": # pragma: no cover
80
+ def __get__(self, instance: object, owner: "t.Type[t.Any]") -> T_Value: # pragma: no cover
81
81
  """
82
- Table.field -> Field.
82
+ row.field -> (actual data).
83
83
  """
84
84
 
85
85
  def __get__(
86
86
  self,
87
87
  instance: T_MetaInstance | None,
88
- owner: t.Type[T_MetaInstance],
88
+ owner: t.Type[t.Any],
89
89
  ) -> t.Union[T_Value, "TypedField[T_Value]"]:
90
90
  """
91
91
  Since this class is a Descriptor field, \
@@ -71,7 +71,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
71
71
  """
72
72
  self.model = model
73
73
  table = self._ensure_table_defined()
74
- default_query: Query = table.id > 0
74
+ default_query: Query = t.cast(Query, table.id > 0)
75
75
  self.query = add_query or default_query
76
76
  self.select_args = select_args or []
77
77
  self.select_kwargs = select_kwargs or {}
@@ -111,7 +111,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
111
111
  Querybuilder is truthy if it has t.Any conditions.
112
112
  """
113
113
  table = self._ensure_table_defined()
114
- default_query: Query = table.id > 0
114
+ default_query: Query = t.cast(Query, table.id > 0)
115
115
  return any(
116
116
  [
117
117
  self.query != default_query,
@@ -18,13 +18,17 @@ from .types import Condition, OnQuery, T_Field
18
18
 
19
19
  # default lazy policy is defined at the TypeDAL() instance settings level
20
20
 
21
+ To_Type = t.TypeVar("To_Type", bound="TypedTable")
22
+ _RelTable = t.TypeVar("_RelTable", bound="TypedTable")
23
+ _RelValue = t.TypeVar("_RelValue")
21
24
 
22
- class Relationship[To_Type: TypedTable]:
25
+
26
+ class Relationship[To_Type]:
23
27
  """
24
28
  Define a relationship to another table.
25
29
  """
26
30
 
27
- _type: t.Type[To_Type]
31
+ _type: t.Any
28
32
  table: t.Type["TypedTable"] | type | str # use get_table() to resolve later on
29
33
  condition: Condition
30
34
  condition_and: Condition
@@ -39,7 +43,7 @@ class Relationship[To_Type: TypedTable]:
39
43
 
40
44
  def __init__(
41
45
  self,
42
- _type: t.Type[To_Type],
46
+ _type: t.Any,
43
47
  condition: Condition = None,
44
48
  join: JOIN_OPTIONS = None,
45
49
  on: OnQuery = None,
@@ -56,7 +60,7 @@ class Relationship[To_Type: TypedTable]:
56
60
 
57
61
  resolved_type = resolve_relationship_type(_type, keep_unresolved=True)
58
62
  if resolved_type is not None:
59
- _type = t.cast(t.Type[To_Type], resolved_type)
63
+ _type = resolved_type
60
64
 
61
65
  self._type = _type
62
66
  self.condition = condition
@@ -201,11 +205,31 @@ class Relationship[To_Type: TypedTable]:
201
205
 
202
206
  return str(table)
203
207
 
208
+ @t.overload
209
+ def __get__(
210
+ self: "Relationship[list[_RelTable]]", instance: None, owner: t.Type["TypedTable"]
211
+ ) -> "Relationship[list[_RelTable]]": ...
212
+
213
+ @t.overload
214
+ def __get__(
215
+ self: "Relationship[list[_RelTable]]", instance: "TypedTable", owner: t.Type["TypedTable"]
216
+ ) -> list[_RelTable]: ...
217
+
218
+ @t.overload
219
+ def __get__(
220
+ self: "Relationship[_RelValue]", instance: None, owner: t.Type["TypedTable"]
221
+ ) -> "Relationship[_RelValue]": ...
222
+
223
+ @t.overload
224
+ def __get__(
225
+ self: "Relationship[_RelValue]", instance: "TypedTable", owner: t.Type["TypedTable"]
226
+ ) -> _RelValue: ...
227
+
204
228
  def __get__(
205
229
  self,
206
- instance: "TypedTable",
230
+ instance: "TypedTable" | None,
207
231
  owner: t.Type["TypedTable"],
208
- ) -> "t.Optional[list[t.Any]] | Relationship[To_Type]":
232
+ ) -> "Relationship[To_Type] | list[t.Any] | t.Any | None":
209
233
  """
210
234
  Relationship is a descriptor class, which can be returned from a class but not an instance.
211
235
 
@@ -285,14 +309,14 @@ class Ref[To_Type: TypedTable]:
285
309
 
286
310
 
287
311
  @t.overload
288
- def relationship[To_Type: TypedTable](
312
+ def relationship(
289
313
  _type: type[list[To_Type]],
290
314
  condition: Condition = None,
291
315
  join: JOIN_OPTIONS = None,
292
316
  on: OnQuery = None,
293
317
  lazy: LazyPolicy | None = None,
294
318
  explicit: bool = False,
295
- ) -> list[To_Type]:
319
+ ) -> "Relationship[list[To_Type]]":
296
320
  """
297
321
  Define a relationship that returns a list of related instances.
298
322
 
@@ -305,7 +329,7 @@ def relationship[To_Type: TypedTable](
305
329
 
306
330
 
307
331
  @t.overload
308
- def relationship[To_Type: TypedTable](
332
+ def relationship(
309
333
  _type: t.Type[To_Type] | str | t.Type[Ref[To_Type]],
310
334
  condition: Condition = None,
311
335
  *,
@@ -313,7 +337,7 @@ def relationship[To_Type: TypedTable](
313
337
  on: OnQuery = None,
314
338
  lazy: LazyPolicy | None = None,
315
339
  explicit: bool = False,
316
- ) -> To_Type:
340
+ ) -> "Relationship[To_Type]":
317
341
  """
318
342
  Define a relationship that returns a single related instance (never None with inner join).
319
343
 
@@ -327,14 +351,14 @@ def relationship[To_Type: TypedTable](
327
351
 
328
352
 
329
353
  @t.overload
330
- def relationship[To_Type: TypedTable](
354
+ def relationship(
331
355
  _type: t.Type[To_Type] | str | t.Type[Ref[To_Type]],
332
356
  condition: Condition = None,
333
357
  join: JOIN_OPTIONS = None,
334
358
  on: OnQuery = None,
335
359
  lazy: LazyPolicy | None = None,
336
360
  explicit: bool = False,
337
- ) -> To_Type | None:
361
+ ) -> "Relationship[To_Type | None]":
338
362
  """
339
363
  Define a relationship that returns a single optional related instance.
340
364
 
@@ -346,14 +370,14 @@ def relationship[To_Type: TypedTable](
346
370
  """
347
371
 
348
372
 
349
- def relationship[To_Type: TypedTable](
373
+ def relationship(
350
374
  _type: type[list[To_Type]] | t.Type[To_Type] | str | t.Type[Ref[To_Type]],
351
375
  condition: Condition = None,
352
376
  join: JOIN_OPTIONS = None,
353
377
  on: OnQuery = None,
354
378
  lazy: LazyPolicy | None = None,
355
379
  explicit: bool = False,
356
- ) -> list[To_Type] | To_Type | None:
380
+ ) -> "Relationship[list[To_Type]] | Relationship[To_Type] | Relationship[To_Type | None]":
357
381
  """
358
382
  Define a relationship to another table, when its id is not stored in the current table.
359
383
 
@@ -402,11 +426,8 @@ def relationship[To_Type: TypedTable](
402
426
  If you'd try to capture this in a single 'condition', pydal would create a cross join which is much less efficient.
403
427
  """
404
428
  return t.cast(
405
- # note: The descriptor `Relationship[To_Type]` is more correct, but pycharm doesn't really get that.
406
- # so for ease of use, just cast to the refered type for now!
407
- # e.g. x = relationship(Author) -> x: Author
408
- To_Type,
409
- Relationship(_type, condition, join, on, lazy=lazy, explicit=explicit), # type: ignore
429
+ Relationship[list[To_Type]] | Relationship[To_Type] | Relationship[To_Type | None],
430
+ Relationship(_type, condition, join, on, lazy=lazy, explicit=explicit),
410
431
  )
411
432
 
412
433
 
@@ -29,6 +29,7 @@ from .types import (
29
29
  OpRow,
30
30
  OrderBy,
31
31
  Query,
32
+ QueryLike,
32
33
  Reference,
33
34
  Row,
34
35
  SelectKwargs,
@@ -139,7 +140,7 @@ class TableMeta(type):
139
140
  Allow dict notation to get a column of this table (-> Field instance).
140
141
  """
141
142
  table = self._ensure_table_defined()
142
- return table[item]
143
+ return t.cast(Field, table[item])
143
144
 
144
145
  def __str__(self) -> str:
145
146
  """
@@ -483,7 +484,7 @@ class TableMeta(type):
483
484
  **kwargs,
484
485
  )
485
486
 
486
- def on(self, query: bool | Query) -> Expression:
487
+ def on(self, query: QueryLike) -> Expression:
487
488
  """
488
489
  Shadow Table.on.
489
490
 
@@ -11,10 +11,7 @@ import datetime as dt
11
11
  import types
12
12
  import typing as t
13
13
 
14
- import pydal.objects
15
-
16
14
  # Third-party
17
- from pydal.adapters.base import BaseAdapter
18
15
  from pydal.helpers.classes import OpRow as _OpRow
19
16
  from pydal.helpers.classes import Reference as _Reference
20
17
  from pydal.helpers.classes import SQLCustomType
@@ -63,6 +60,11 @@ class TableProtocol(t.Protocol): # pragma: no cover
63
60
  Tables have table[field] syntax.
64
61
  """
65
62
 
63
+ def on(self, query: "QueryLike") -> "Expression | _Expression":
64
+ """
65
+ pydal Table.on(query) helper used in join callbacks.
66
+ """
67
+
66
68
 
67
69
  class CacheFn(t.Protocol):
68
70
  """
@@ -70,7 +72,7 @@ class CacheFn(t.Protocol):
70
72
  """
71
73
 
72
74
  def __call__(
73
- self: BaseAdapter,
75
+ self, # : BaseAdapter
74
76
  sql: str = "",
75
77
  fields: t.Iterable[str] = (),
76
78
  attributes: t.Iterable[str] = (),
@@ -153,8 +155,8 @@ class Validator(_Validator):
153
155
  """Pydal Validator object. Make mypy happy."""
154
156
 
155
157
 
156
- class Table(_Table, TableProtocol):
157
- """Table with protocol support. Make mypy happy."""
158
+ class Table(_Table):
159
+ """Pydal Table object. Make mypy happy."""
158
160
 
159
161
 
160
162
  # ---------------------------------------------------------------------------
@@ -312,11 +314,14 @@ type T_Query = t.Union[
312
314
  type T_Field = t.Union["TypedField[t.Any]", "Table", t.Type["TypedTable"]]
313
315
 
314
316
  # table-ish parameter:
315
- type P_Table = t.Union[t.Type["TypedTable"], pydal.objects.Table]
317
+ # Use protocol typing so checkers know `.id` exists in relationship callbacks.
318
+ type P_Table = t.Union[t.Type["_TypedTable"], TableProtocol]
319
+
320
+ type QueryLike = Query | _Query | bool
316
321
 
317
- type Condition = t.Optional[t.Callable[[P_Table, P_Table], Query | bool]]
322
+ type Condition = t.Optional[t.Callable[[P_Table, P_Table], QueryLike]]
318
323
 
319
- type OnQuery = t.Optional[t.Callable[[P_Table, P_Table], list[Expression]]]
324
+ type OnQuery = t.Optional[t.Callable[[P_Table, P_Table], list[Expression | _Expression]]]
320
325
 
321
326
  type CacheModel = t.Callable[[str, CacheFn, int], Rows]
322
327
  type CacheTuple = tuple[CacheModel, int]
@@ -0,0 +1,250 @@
1
+ # Basics
2
+
3
+ note that mypy scopes classes like typedal.tables.TypedTable or test_snippet.WithBraces
4
+
5
+ ```python only=mypy
6
+ from typedal import TypeDAL, TypedTable, TypedField, relationship, Ref, Relationship
7
+
8
+ db = TypeDAL()
9
+
10
+ reveal_type(db) # revealed: typedal.core.TypeDAL
11
+
12
+ @db.define()
13
+ class WithBraces(TypedTable):
14
+ gid = TypedField(str)
15
+
16
+ with_braces = WithBraces.first_or_fail()
17
+
18
+ reveal_type(with_braces) # revealed: test_snippet.WithBraces
19
+
20
+ @db.define
21
+ class WithoutBraces(TypedTable):
22
+ with_braces = relationship(WithBraces, condition=lambda self, other: self.id == other.id, join="inner")
23
+ deferred = relationship(Ref["DeferredDefine"], on=lambda self, other: [other.on(other.id == self.id)], join="left")
24
+ multiple = relationship(list["WithoutBraces"], condition=lambda self, other: self.id != other.id)
25
+
26
+ without_braces = WithoutBraces.where().first()
27
+
28
+ reveal_type(without_braces) # revealed: test_snippet.WithoutBraces | None
29
+
30
+
31
+ class DeferredDefine(TypedTable):
32
+ ...
33
+
34
+ db.define(DeferredDefine)
35
+
36
+ for deferred_define in DeferredDefine.paginate(limit=1):
37
+ reveal_type(deferred_define) # revealed: test_snippet.DeferredDefine
38
+
39
+ new_row = WithBraces.insert()
40
+
41
+ reveal_type(new_row) # revealed: test_snippet.WithBraces
42
+ reveal_type(new_row.id) # revealed: int
43
+
44
+ reveal_type(WithBraces.gid) # revealed: typedal.fields.TypedField[str]
45
+ reveal_type(new_row.gid) # revealed: str
46
+
47
+ db.commit()
48
+
49
+ joined = WithoutBraces.join().first_or_fail()
50
+
51
+ reveal_type(WithoutBraces.with_braces) # revealed: typedal.relationships.Relationship[test_snippet.WithBraces]
52
+ reveal_type(WithoutBraces.deferred) # revealed: typedal.relationships.Relationship[test_snippet.DeferredDefine | None]
53
+ reveal_type(WithoutBraces.multiple) # revealed: typedal.relationships.Relationship[list[test_snippet.WithoutBraces]]
54
+
55
+ reveal_type(joined.with_braces) # revealed: test_snippet.WithBraces
56
+ reveal_type(joined.deferred) # revealed: test_snippet.DeferredDefine | None
57
+ reveal_type(joined.multiple) # revealed: list[test_snippet.WithoutBraces]
58
+
59
+ ```
60
+
61
+ # 1. Field Dual Behavior (Class vs Instance)
62
+
63
+ ```python only=mypy
64
+ from typedal import TypeDAL, TypedTable, TypedField
65
+
66
+ db = TypeDAL()
67
+
68
+ @db.define
69
+ class MyTable(TypedTable):
70
+ fancy = TypedField(str)
71
+
72
+ reveal_type(MyTable.fancy.lower()) # revealed: typedal.types.Expression
73
+ reveal_type(MyTable().fancy.lower()) # revealed: str
74
+ ```
75
+
76
+ # 2. Alias Roundtrip Typing
77
+
78
+ ```python only=mypy
79
+ from typedal import TypeDAL, TypedTable
80
+
81
+ db = TypeDAL()
82
+
83
+ @db.define
84
+ class MyTable(TypedTable):
85
+ ...
86
+
87
+ aliased_cls = MyTable.with_alias("---")
88
+ reveal_type(aliased_cls) # revealed: type[test_snippet.MyTable]
89
+
90
+ aliased_instance = aliased_cls()
91
+ reveal_type(aliased_instance) # revealed: test_snippet.MyTable
92
+ ```
93
+
94
+ # 3. Query Object Typing and Compatibility
95
+
96
+ ```python only=mypy
97
+ from typedal import TypeDAL, TypedTable
98
+
99
+ db = TypeDAL()
100
+
101
+ @db.define
102
+ class MyTable(TypedTable):
103
+ ...
104
+
105
+ my_query = MyTable.id > 3
106
+ reveal_type(my_query) # revealed: typedal.types.Query
107
+
108
+ query = MyTable.id == 3
109
+
110
+ reveal_type(query) # revealed: typedal.types.Query
111
+
112
+ new = MyTable.update(query)
113
+ reveal_type(new) # revealed: test_snippet.MyTable | None
114
+
115
+ MyTable.update_or_insert(MyTable)
116
+ MyTable.update_or_insert(my_query)
117
+ MyTable.update_or_insert(db.my_table.id > 3)
118
+ ```
119
+
120
+ # 4. TypedRows Inference Behavior
121
+
122
+ ```python only=mypy
123
+ from typedal import TypeDAL, TypedRows, TypedTable
124
+
125
+ db = TypeDAL()
126
+
127
+ @db.define
128
+ class MyTable(TypedTable):
129
+ ...
130
+
131
+ select1 = db(MyTable).select() # error: [var-annotated]
132
+ select2: TypedRows[MyTable] = db(MyTable).select()
133
+ select3 = MyTable.select().collect()
134
+
135
+ reveal_type(select1) # revealed: typedal.rows.TypedRows[Any]
136
+ reveal_type(select2) # revealed: typedal.rows.TypedRows[test_snippet.MyTable]
137
+ reveal_type(select3) # revealed: typedal.rows.TypedRows[test_snippet.MyTable]
138
+
139
+ reveal_type(select1.first()) # revealed: Any | None
140
+ reveal_type(select2.first()) # revealed: test_snippet.MyTable | None
141
+ reveal_type(select3.first()) # revealed: test_snippet.MyTable | None
142
+
143
+ for row in select2:
144
+ reveal_type(row) # revealed: test_snippet.MyTable
145
+
146
+ for row in MyTable.select():
147
+ reveal_type(row) # revealed: test_snippet.MyTable
148
+ ```
149
+
150
+ # 5. where().column(...) Overloads
151
+
152
+ ```python only=mypy
153
+ import typing
154
+ from typedal import TypeDAL, TypedField, TypedTable
155
+
156
+ db = TypeDAL()
157
+
158
+ @db.define
159
+ class MyTable(TypedTable):
160
+ normal: str
161
+ fancy = TypedField(str)
162
+
163
+ SomeField: typing.Any = ...
164
+
165
+ reveal_type(MyTable.where().column(SomeField)) # revealed: list[Any]
166
+ reveal_type(MyTable.where().column(MyTable.normal)) # revealed: list[str]
167
+ reveal_type(MyTable.where().column(MyTable.fancy)) # revealed: list[str]
168
+ ```
169
+
170
+ # 6. rows.render() Overloads
171
+
172
+ ```python only=mypy
173
+ from typedal import TypeDAL, TypedTable
174
+
175
+ db = TypeDAL()
176
+
177
+ @db.define
178
+ class MyTable(TypedTable):
179
+ ...
180
+
181
+ rows = MyTable.where().collect()
182
+ reveal_type(rows.render()) # revealed: typing.Generator[test_snippet.MyTable, None, None]
183
+ reveal_type(rows.render(1)) # revealed: test_snippet.MyTable
184
+ ```
185
+
186
+ # 7. Mixin-as-Table Type Argument
187
+
188
+ ```python only=mypy
189
+ from typedal import TypeDAL, TypedTable
190
+ from typedal.mixins import Mixin
191
+
192
+ db = TypeDAL()
193
+
194
+ class SearchMixin(Mixin):
195
+ ...
196
+
197
+ @db.define
198
+ class SearchableTable(TypedTable, SearchMixin):
199
+ title: str
200
+
201
+ def using_mixin(table: type[SearchMixin]) -> None:
202
+ reveal_type(table.where()) # revealed: typedal.query_builder.QueryBuilder[test_snippet.SearchMixin]
203
+
204
+ using_mixin(SearchableTable)
205
+ ```
206
+
207
+ # 8. Cache Tuple Protocol Checks
208
+
209
+ ```python only=mypy
210
+ import typing
211
+ from typedal.types import CacheFn, CacheTuple, Rows
212
+
213
+ def cache_model(key: str, fn: CacheFn, expire: int) -> Rows:
214
+ return fn()
215
+
216
+ cache_valid: CacheTuple = (cache_model, 3000)
217
+
218
+ def invalid_cache_model(key: str, fn: typing.Callable[..., list[str]], _: typing.Optional[int] = None) -> list[str]:
219
+ return fn()
220
+
221
+ cache_invalid: CacheTuple = (invalid_cache_model, 3000) # error: [assignment]
222
+ ```
223
+
224
+ # Parity with test_mypy.py
225
+
226
+ There are still a few tests missing in this file:
227
+
228
+ - Hook callback typing parity:
229
+ - `_after_insert` list type behavior
230
+ - accepted callbacks: `(Any, Reference)`, `(MyTable, Reference)`, `(OpRow, Reference)`
231
+ - rejected callback: `(str, Reference)` on both `.append(...)` and `MyTable.after_insert(...)`
232
+
233
+ - Instance update flow:
234
+ - `inst = MyTable(3)` reveal
235
+ - guarded `inst._update()` reveal
236
+ - `inst.update_record()` reveal
237
+
238
+ - Old-style table count paths:
239
+ - `db(db.old_style).count()`
240
+ - `db(old_style).count()`
241
+
242
+ - Raw DAL entrypoints:
243
+ - `db(MyTable.id > 0)`
244
+ - `db(db.old_style.id > 3)`
245
+ - `db(MyTable)`
246
+ - `db(db.old_style)`
247
+
248
+ - Field/type reveals not explicitly repeated in new sections:
249
+ - `MyTable.normal` and `MyTable().normal`
250
+ - `MyTable.options` and `MyTable().options`
@@ -0,0 +1,225 @@
1
+ # Basics
2
+
3
+ ```python only=pyright
4
+ from typedal import TypeDAL, TypedTable, TypedField, relationship, Ref, Relationship
5
+
6
+ db = TypeDAL()
7
+
8
+ reveal_type(db) # revealed: TypeDAL
9
+
10
+ @db.define()
11
+ class WithBraces(TypedTable):
12
+ gid = TypedField(str)
13
+
14
+ with_braces = WithBraces.first_or_fail()
15
+
16
+ reveal_type(with_braces) # revealed: WithBraces
17
+
18
+ @db.define
19
+ class WithoutBraces(TypedTable):
20
+ with_braces = relationship(WithBraces, condition=lambda self, other: self.id == other.id, join="inner")
21
+ deferred = relationship(Ref["DeferredDefine"], on=lambda self, other: [other.on(other.id == self.id)], join="left")
22
+ multiple = relationship(list["WithoutBraces"], condition=lambda self, other: self.id != other.id)
23
+
24
+ without_braces = WithoutBraces.where().first()
25
+
26
+ reveal_type(without_braces) # revealed: WithoutBraces | None
27
+
28
+
29
+ class DeferredDefine(TypedTable):
30
+ ...
31
+
32
+ db.define(DeferredDefine)
33
+
34
+ for deferred_define in DeferredDefine.paginate(limit=1):
35
+ reveal_type(deferred_define) # revealed: DeferredDefine
36
+
37
+ new_row = WithBraces.insert()
38
+
39
+ reveal_type(new_row) # revealed: WithBraces
40
+ reveal_type(new_row.id) # revealed: int
41
+
42
+ reveal_type(WithBraces.gid) # revealed: TypedField[str]
43
+ reveal_type(new_row.gid) # revealed: str
44
+
45
+ db.commit()
46
+
47
+ joined = WithoutBraces.join().first_or_fail()
48
+
49
+ reveal_type(WithoutBraces.with_braces) # revealed: Relationship[WithBraces]
50
+ reveal_type(WithoutBraces.deferred) # revealed: Relationship[DeferredDefine | None]
51
+ reveal_type(WithoutBraces.multiple) # revealed: Relationship[list[WithoutBraces]]
52
+
53
+ reveal_type(joined.with_braces) # revealed: WithBraces
54
+ reveal_type(joined.deferred) # revealed: DeferredDefine | None
55
+ reveal_type(joined.multiple) # revealed: list[WithoutBraces]
56
+
57
+ ```
58
+
59
+
60
+ # 1. Field Dual Behavior (Class vs Instance)
61
+
62
+ ```python only=pyright
63
+ from typedal import TypeDAL, TypedTable, TypedField
64
+
65
+ db = TypeDAL()
66
+
67
+ @db.define
68
+ class MyTable(TypedTable):
69
+ fancy = TypedField(str)
70
+
71
+ reveal_type(MyTable.fancy.lower()) # revealed: Expression
72
+ reveal_type(MyTable().fancy.lower()) # revealed: str
73
+ ```
74
+
75
+ # 2. Alias Roundtrip Typing
76
+
77
+ ```python only=pyright
78
+ from typedal import TypeDAL, TypedTable
79
+
80
+ db = TypeDAL()
81
+
82
+ @db.define
83
+ class MyTable(TypedTable):
84
+ ...
85
+
86
+ aliased_cls = MyTable.with_alias("---")
87
+ reveal_type(aliased_cls) # revealed: type[MyTable]
88
+
89
+ aliased_instance = aliased_cls()
90
+ reveal_type(aliased_instance) # revealed: MyTable
91
+ ```
92
+
93
+ # 3. Query Object Typing and Compatibility
94
+
95
+ ```python only=pyright
96
+ from typedal import TypeDAL, TypedTable
97
+
98
+ db = TypeDAL()
99
+
100
+ @db.define
101
+ class MyTable(TypedTable):
102
+ ...
103
+
104
+ my_query = MyTable.id > 3
105
+ reveal_type(my_query) # revealed: Query
106
+
107
+ query = MyTable.id == 3
108
+
109
+ reveal_type(query) # revealed: Query
110
+
111
+ new = MyTable.update(query)
112
+ reveal_type(new) # revealed: MyTable | None
113
+
114
+ MyTable.update_or_insert(MyTable)
115
+ MyTable.update_or_insert(my_query)
116
+ # MyTable.update_or_insert(db.my_table.id > 3) # <- not supported
117
+ ```
118
+
119
+ # 4. TypedRows Inference Behavior
120
+
121
+ ```python only=pyright
122
+ from typedal import TypeDAL, TypedRows, TypedTable
123
+
124
+ db = TypeDAL()
125
+
126
+ @db.define
127
+ class MyTable(TypedTable):
128
+ ...
129
+
130
+ select1 = db(MyTable).select()
131
+ select2: TypedRows[MyTable] = db(MyTable).select()
132
+ select3 = MyTable.select().collect()
133
+
134
+ reveal_type(select1) # revealed: TypedRows[Unknown]
135
+ reveal_type(select2) # revealed: TypedRows[MyTable]
136
+ reveal_type(select3) # revealed: TypedRows[MyTable]
137
+
138
+ reveal_type(select1.first()) # revealed: Unknown | None
139
+ reveal_type(select2.first()) # revealed: MyTable | None
140
+ reveal_type(select3.first()) # revealed: MyTable | None
141
+
142
+ for row in select2:
143
+ reveal_type(row) # revealed: MyTable
144
+
145
+ for row in MyTable.select():
146
+ reveal_type(row) # revealed: MyTable
147
+ ```
148
+
149
+ # 5. where().column(...) Overloads
150
+
151
+ ```python only=pyright
152
+ import typing
153
+ from typedal import TypeDAL, TypedField, TypedTable
154
+
155
+ db = TypeDAL()
156
+
157
+ @db.define
158
+ class MyTable(TypedTable):
159
+ normal: str
160
+ fancy = TypedField(str)
161
+
162
+ SomeField: typing.Any = ...
163
+
164
+ reveal_type(MyTable.where().column(SomeField)) # revealed: list[Any]
165
+ reveal_type(MyTable.where().column(MyTable.normal)) # revealed: list[str]
166
+ reveal_type(MyTable.where().column(MyTable.fancy)) # revealed: list[str]
167
+ ```
168
+
169
+ # 6. rows.render() Overloads
170
+
171
+ ```python only=pyright
172
+ from typedal import TypeDAL, TypedTable
173
+
174
+ db = TypeDAL()
175
+
176
+ @db.define
177
+ class MyTable(TypedTable):
178
+ ...
179
+
180
+ rows = MyTable.where().collect()
181
+ reveal_type(rows.render()) # revealed: Generator[MyTable, None, None]
182
+ reveal_type(rows.render(1)) # revealed: MyTable
183
+ ```
184
+
185
+ # 7. Mixin-as-Table Type Argument
186
+
187
+ ```python only=pyright
188
+ from typedal import TypeDAL, TypedTable
189
+ from typedal.mixins import Mixin
190
+
191
+ db = TypeDAL()
192
+
193
+ class SearchMixin(Mixin):
194
+ ...
195
+
196
+ @db.define
197
+ class SearchableTable(TypedTable, SearchMixin):
198
+ title: str
199
+
200
+ def using_mixin(table: type[SearchMixin]) -> None:
201
+ reveal_type(table.where()) # revealed: QueryBuilder[SearchMixin]
202
+
203
+ using_mixin(SearchableTable)
204
+ ```
205
+
206
+ # 8. Cache Tuple Protocol Checks
207
+
208
+ ```python only=pyright
209
+ import typing
210
+ from typedal.types import CacheFn, CacheTuple, Rows
211
+
212
+ def cache_model(key: str, fn: CacheFn, expire: int) -> Rows:
213
+ return fn()
214
+
215
+ cache_valid: CacheTuple = (cache_model, 3000)
216
+
217
+ def invalid_cache_model(key: str, fn: typing.Callable[..., list[str]], _: typing.Optional[int] = None) -> list[str]:
218
+ return fn()
219
+
220
+ cache_invalid: CacheTuple = (invalid_cache_model, 3000) # error: [reportAssignmentType]
221
+ ```
222
+
223
+ # Parity with test_mypy.py
224
+
225
+ See test_typing_mypy.md
@@ -0,0 +1,35 @@
1
+ from typedal import TypeDAL, TypedTable, TypedField, relationship, Relationship
2
+
3
+ from typing import reveal_type
4
+
5
+ db = TypeDAL()
6
+
7
+ reveal_type(db)
8
+
9
+ @db.define
10
+ class OtherTable(TypedTable):
11
+ ...
12
+
13
+ @db.define
14
+ class MyTable(TypedTable):
15
+ field = TypedField(str)
16
+
17
+ rel = relationship(OtherTable)
18
+ multiple = relationship(list[OtherTable])
19
+
20
+
21
+ row = MyTable.first_or_fail()
22
+
23
+ reveal_type(row) # expected: MyTable; pycharm: MyTable
24
+ reveal_type(MyTable.field) # expected: TypedField[str]; pycharm: TypedField[str]
25
+ reveal_type(row.field) # expected: str; pycharm: TypedField[str]
26
+
27
+ q = MyTable.field.belongs([""])
28
+
29
+ reveal_type(q) # expected: Query; pycharm: Query
30
+
31
+ reveal_type(MyTable.rel) # expected: Relationship[OtherTable]; pycharm: Relationship[OtherTable | None]
32
+ reveal_type(row.rel) # expected: OtherTable | None; pycharm: Relationship[OtherTable | None]
33
+
34
+ reveal_type(MyTable.multiple) # expected: Relationship[list[OtherTable]]; pycharm: Relationship[list[OtherTable]]
35
+ reveal_type(row.multiple) # expected: list[OtherTable]; pycharm: Relationship[list[OtherTable]]
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="122" height="20" role="img" aria-label="coverage: 100.00%"><title>coverage: 100.00%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="122" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="61" height="20" fill="#4c1"/><rect width="122" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="905" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">100.00%</text><text x="905" y="140" transform="scale(.1)" fill="#fff" textLength="510">100.00%</text></g></svg>
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