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.
- {typedal-4.8.0 → typedal-4.8.2}/CHANGELOG.md +12 -0
- {typedal-4.8.0 → typedal-4.8.2}/PKG-INFO +3 -2
- typedal-4.8.2/coverage.svg +1 -0
- {typedal-4.8.0 → typedal-4.8.2}/pyproject.toml +9 -2
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/__about__.py +1 -1
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/core.py +32 -2
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/fields.py +6 -6
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/query_builder.py +2 -2
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/relationships.py +40 -19
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/tables.py +3 -2
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/types.py +14 -9
- typedal-4.8.2/tests/test_typing_mypy.md +250 -0
- typedal-4.8.2/tests/test_typing_pyright.md +225 -0
- typedal-4.8.2/typing_again.py +35 -0
- typedal-4.8.0/coverage.svg +0 -1
- {typedal-4.8.0 → typedal-4.8.2}/.github/workflows/su6.yml +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/.gitignore +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/.readthedocs.yml +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/README.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/10_advanced_apis.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/1_getting_started.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/2_defining_tables.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/3_building_queries.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/4_relationships.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/5_py4web.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/6_migrations.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/7_configuration.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/8_mixins.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/9_memoization.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/css/code_blocks.css +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/index.md +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/docs/requirements.txt +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/example_new.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/example_old.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/mkdocs.yml +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/__init__.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/caching.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/cli.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/config.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/constants.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/define.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/enum_helpers.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/for_py4web.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/for_web2py.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/helpers.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/mixins.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/py.typed +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/rows.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/serializers/typescript.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tasks.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/__init__.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/configs/simple.toml +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/configs/valid.env +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/configs/valid.toml +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/py314_tests.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_cli.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_config.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_docs_examples.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_helpers.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_json.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_main.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_mixins.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_mypy.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_orm.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_py4web.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_query_builder.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_relationships.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_row.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_stats.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_table.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_typescript.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_web2py.py +0 -0
- {typedal-4.8.0 → typedal-4.8.2}/tests/test_xx_others.py +0 -0
- {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.
|
|
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:
|
|
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
|
|
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"
|
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
74
|
+
def __get__(self, instance: None, owner: "t.Type[t.Any]") -> "TypedField[T_Value]": # pragma: no cover
|
|
75
75
|
"""
|
|
76
|
-
|
|
76
|
+
Table.field -> Field.
|
|
77
77
|
"""
|
|
78
78
|
|
|
79
79
|
@t.overload
|
|
80
|
-
def __get__(self, instance:
|
|
80
|
+
def __get__(self, instance: object, owner: "t.Type[t.Any]") -> T_Value: # pragma: no cover
|
|
81
81
|
"""
|
|
82
|
-
|
|
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[
|
|
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
|
-
|
|
25
|
+
|
|
26
|
+
class Relationship[To_Type]:
|
|
23
27
|
"""
|
|
24
28
|
Define a relationship to another table.
|
|
25
29
|
"""
|
|
26
30
|
|
|
27
|
-
_type: t.
|
|
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.
|
|
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 =
|
|
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
|
-
) -> "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
406
|
-
|
|
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:
|
|
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
|
|
157
|
-
"""Table
|
|
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
|
-
|
|
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],
|
|
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]]
|
typedal-4.8.0/coverage.svg
DELETED
|
@@ -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
|
|
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
|