TypeDAL 3.17.3__py3-none-any.whl → 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

typedal/define.py ADDED
@@ -0,0 +1,188 @@
1
+ """
2
+ Seperates the table definition code from core DAL code.
3
+
4
+ Since otherwise helper methods would clutter up the TypeDAl class.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ import types
11
+ import typing as t
12
+ import warnings
13
+
14
+ import pydal
15
+
16
+ from .constants import BASIC_MAPPINGS
17
+ from .core import TypeDAL, evaluate_forward_reference, resolve_annotation
18
+ from .fields import TypedField, is_typed_field
19
+ from .helpers import (
20
+ all_annotations,
21
+ all_dict,
22
+ filter_out,
23
+ instanciate,
24
+ is_union,
25
+ origin_is_subclass,
26
+ to_snake,
27
+ )
28
+ from .relationships import Relationship, to_relationship
29
+ from .tables import TypedTable
30
+ from .types import (
31
+ Field,
32
+ T,
33
+ T_annotation,
34
+ Table,
35
+ _Types,
36
+ )
37
+
38
+ try:
39
+ # python 3.14+
40
+ from annotationlib import ForwardRef
41
+ except ImportError: # pragma: no cover
42
+ # python 3.13-
43
+ from typing import ForwardRef
44
+
45
+
46
+ class TableDefinitionBuilder:
47
+ """Handles the conversion of TypedTable classes to pydal tables."""
48
+
49
+ def __init__(self, db: "TypeDAL"):
50
+ """
51
+ Before, the `class_map` was a singleton on the pydal class; now it's per database.
52
+ """
53
+ self.db = db
54
+ self.class_map: dict[str, t.Type["TypedTable"]] = {}
55
+
56
+ def define(self, cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
57
+ """Build and register a table from a TypedTable class."""
58
+ full_dict = all_dict(cls)
59
+ tablename = to_snake(cls.__name__)
60
+ annotations = all_annotations(cls)
61
+ annotations |= {k: t.cast(type, v) for k, v in full_dict.items() if is_typed_field(v)}
62
+ annotations = {k: v for k, v in annotations.items() if not k.startswith("_")}
63
+
64
+ typedfields: dict[str, TypedField[t.Any]] = {
65
+ k: instanciate(v, True) for k, v in annotations.items() if is_typed_field(v)
66
+ }
67
+
68
+ relationships: dict[str, type[Relationship[t.Any]]] = filter_out(annotations, Relationship)
69
+ fields = {fname: self.to_field(fname, ftype) for fname, ftype in annotations.items()}
70
+
71
+ other_kwargs = kwargs | {
72
+ k: v for k, v in cls.__dict__.items() if k not in annotations and not k.startswith("_")
73
+ }
74
+
75
+ for key, field in typedfields.items():
76
+ clone = copy.copy(field)
77
+ setattr(cls, key, clone)
78
+ typedfields[key] = clone
79
+
80
+ relationships = filter_out(full_dict, Relationship) | relationships | filter_out(other_kwargs, Relationship)
81
+
82
+ reference_field_keys = [
83
+ k for k, v in fields.items() if str(v.type).split(" ")[0] in ("list:reference", "reference")
84
+ ]
85
+
86
+ relationships |= {
87
+ k: new_relationship
88
+ for k in reference_field_keys
89
+ if k not in relationships and (new_relationship := to_relationship(cls, k, annotations[k]))
90
+ }
91
+
92
+ cache_dependency = self.db._config.caching and kwargs.pop("cache_dependency", True)
93
+ table: Table = self.db.define_table(tablename, *fields.values(), **kwargs)
94
+
95
+ for name, typed_field in typedfields.items():
96
+ field = fields[name]
97
+ typed_field.bind(field, table)
98
+
99
+ if issubclass(cls, TypedTable):
100
+ cls.__set_internals__(
101
+ db=self.db,
102
+ table=table,
103
+ relationships=t.cast(dict[str, Relationship[t.Any]], relationships),
104
+ )
105
+ self.class_map[str(table)] = cls
106
+ self.class_map[table._rname] = cls
107
+ cls.__on_define__(self.db)
108
+ else:
109
+ warnings.warn("db.define used without inheriting TypedTable. This could lead to strange problems!")
110
+
111
+ if not tablename.startswith("typedal_") and cache_dependency:
112
+ from .caching import _remove_cache
113
+
114
+ table._before_update.append(lambda s, _: _remove_cache(s, tablename))
115
+ table._before_delete.append(lambda s: _remove_cache(s, tablename))
116
+
117
+ return cls
118
+
119
+ def to_field(self, fname: str, ftype: type, **kw: t.Any) -> Field:
120
+ """Convert annotation to pydal Field."""
121
+ fname = to_snake(fname)
122
+ if converted_type := self.annotation_to_pydal_fieldtype(ftype, kw):
123
+ return self.build_field(fname, converted_type, **kw)
124
+ else:
125
+ raise NotImplementedError(f"Unsupported type {ftype}/{type(ftype)}")
126
+
127
+ def annotation_to_pydal_fieldtype(
128
+ self,
129
+ ftype_annotation: T_annotation,
130
+ mut_kw: t.MutableMapping[str, t.Any],
131
+ ) -> t.Optional[str]:
132
+ """Convert Python type annotation to pydal field type string."""
133
+ ftype = t.cast(type, ftype_annotation) # cast from Type to type to make mypy happy)
134
+
135
+ if isinstance(ftype, str):
136
+ # extract type from string
137
+ ftype = resolve_annotation(ftype)
138
+
139
+ if isinstance(ftype, ForwardRef):
140
+ known_classes = {table.__name__: table for table in self.class_map.values()}
141
+
142
+ ftype = evaluate_forward_reference(ftype, namespace=known_classes)
143
+
144
+ if mapping := BASIC_MAPPINGS.get(ftype):
145
+ # basi types
146
+ return mapping
147
+ elif isinstance(ftype, pydal.objects.Table):
148
+ # db.table
149
+ return f"reference {ftype._tablename}"
150
+ elif issubclass(type(ftype), type) and issubclass(ftype, TypedTable):
151
+ # SomeTable
152
+ snakename = to_snake(ftype.__name__)
153
+ return f"reference {snakename}"
154
+ elif isinstance(ftype, TypedField):
155
+ # FieldType(type, ...)
156
+ return ftype._to_field(mut_kw, self)
157
+ elif origin_is_subclass(ftype, TypedField):
158
+ # TypedField[int]
159
+ return self.annotation_to_pydal_fieldtype(t.get_args(ftype)[0], mut_kw)
160
+ elif isinstance(ftype, types.GenericAlias) and t.get_origin(ftype) in (list, TypedField): # type: ignore
161
+ # list[str] -> str -> string -> list:string
162
+ _child_type = t.get_args(ftype)[0]
163
+ _child_type = self.annotation_to_pydal_fieldtype(_child_type, mut_kw)
164
+ return f"list:{_child_type}"
165
+ elif is_union(ftype):
166
+ # str | int -> UnionType
167
+ # typing.Union[str | int] -> typing._UnionGenericAlias
168
+
169
+ # Optional[type] == type | None
170
+
171
+ match t.get_args(ftype):
172
+ case (_child_type, _Types.NONETYPE) | (_Types.NONETYPE, _child_type):
173
+ # good union of Nullable
174
+
175
+ # if a field is optional, it is nullable:
176
+ mut_kw["notnull"] = False
177
+ return self.annotation_to_pydal_fieldtype(_child_type, mut_kw)
178
+ case _:
179
+ # two types is not supported by the db!
180
+ return None
181
+ else:
182
+ return None
183
+
184
+ @classmethod
185
+ def build_field(cls, name: str, field_type: str, **kw: t.Any) -> Field:
186
+ """Create a pydal Field with default kwargs."""
187
+ kw_combined = TypeDAL.default_kwargs | kw
188
+ return Field(name, field_type, **kw_combined)
typedal/fields.py CHANGED
@@ -2,27 +2,248 @@
2
2
  This file contains available Field types.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import ast
8
+ import contextlib
6
9
  import datetime as dt
7
10
  import decimal
8
- import typing
11
+ import types
12
+ import typing as t
9
13
  import uuid
10
14
 
15
+ import pydal
11
16
  from pydal.helpers.classes import SQLCustomType
12
17
  from pydal.objects import Table
13
- from typing_extensions import Unpack
14
18
 
15
- from .core import TypeDAL, TypedField, TypedTable
16
- from .types import FieldSettings
19
+ from .core import TypeDAL
20
+ from .types import (
21
+ Expression,
22
+ Field,
23
+ FieldSettings,
24
+ Query,
25
+ T_annotation,
26
+ T_MetaInstance,
27
+ T_subclass,
28
+ T_Value,
29
+ Validator,
30
+ )
17
31
 
18
- T = typing.TypeVar("T", bound=typing.Any)
32
+ if t.TYPE_CHECKING:
33
+ # will be imported for real later:
34
+ from .tables import TypedTable
19
35
 
20
36
 
21
37
  ## general
22
38
 
23
39
 
40
+ class TypedField(Expression, t.Generic[T_Value]): # pragma: no cover
41
+ """
42
+ Typed version of pydal.Field, which will be converted to a normal Field in the background.
43
+ """
44
+
45
+ # will be set by .bind on db.define
46
+ name = ""
47
+ _db: t.Optional[pydal.DAL] = None
48
+ _rname: t.Optional[str] = None
49
+ _table: t.Optional[Table] = None
50
+ _field: t.Optional[Field] = None
51
+
52
+ _type: T_annotation
53
+ kwargs: t.Any
54
+
55
+ requires: Validator | t.Iterable[Validator]
56
+
57
+ # NOTE: for the logic of converting a TypedField into a pydal Field, see TypeDAL._to_field
58
+
59
+ def __init__(
60
+ self,
61
+ _type: t.Type[T_Value] | types.UnionType = str, # type: ignore
62
+ /,
63
+ **settings: t.Unpack[FieldSettings],
64
+ ) -> None:
65
+ """
66
+ Typed version of pydal.Field, which will be converted to a normal Field in the background.
67
+
68
+ Provide the Python type for this field as the first positional argument
69
+ and any other settings to Field() as keyword parameters.
70
+ """
71
+ self._type = _type
72
+ self.kwargs = settings
73
+ # super().__init__()
74
+
75
+ @t.overload
76
+ def __get__(self, instance: T_MetaInstance, owner: t.Type[T_MetaInstance]) -> T_Value: # pragma: no cover
77
+ """
78
+ row.field -> (actual data).
79
+ """
80
+
81
+ @t.overload
82
+ def __get__(self, instance: None, owner: "t.Type[TypedTable]") -> "TypedField[T_Value]": # pragma: no cover
83
+ """
84
+ Table.field -> Field.
85
+ """
86
+
87
+ def __get__(
88
+ self,
89
+ instance: T_MetaInstance | None,
90
+ owner: t.Type[T_MetaInstance],
91
+ ) -> t.Union[T_Value, "TypedField[T_Value]"]:
92
+ """
93
+ Since this class is a Descriptor field, \
94
+ it returns something else depending on if it's called on a class or instance.
95
+
96
+ (this is mostly for mypy/typing)
97
+ """
98
+ if instance:
99
+ # this is only reached in a very specific case:
100
+ # an instance of the object was created with a specific set of fields selected (excluding the current one)
101
+ # in that case, no value was stored in the owner -> return None (since the field was not selected)
102
+ return t.cast(T_Value, None) # cast as T_Value so mypy understands it for selected fields
103
+ else:
104
+ # getting as class -> return actual field so pydal understands it when using in query etc.
105
+ return t.cast(TypedField[T_Value], self._field) # pretend it's still typed for IDE support
106
+
107
+ def __str__(self) -> str:
108
+ """
109
+ String representation of a Typed Field.
110
+
111
+ If `type` is set explicitly (e.g. TypedField(str, type="text")), that type is used: `TypedField.text`,
112
+ otherwise the type annotation is used (e.g. TypedField(str) -> TypedField.str)
113
+ """
114
+ return str(self._field) if self._field else ""
115
+
116
+ def __repr__(self) -> str:
117
+ """
118
+ More detailed string representation of a Typed Field.
119
+
120
+ Uses __str__ and adds the provided extra options (kwargs) in the representation.
121
+ """
122
+ string_value = self.__str__()
123
+
124
+ if "type" in self.kwargs:
125
+ # manual type in kwargs supplied
126
+ typename = self.kwargs["type"]
127
+ elif issubclass(type, type(self._type)):
128
+ # normal type, str.__name__ = 'str'
129
+ typename = getattr(self._type, "__name__", str(self._type))
130
+ elif t_args := t.get_args(self._type):
131
+ # list[str] -> 'str'
132
+ typename = t_args[0].__name__
133
+ else: # pragma: no cover
134
+ # fallback - something else, may not even happen, I'm not sure
135
+ typename = self._type
136
+
137
+ string_value = f"TypedField[{typename}].{string_value}" if string_value else f"TypedField[{typename}]"
138
+
139
+ kw = self.kwargs.copy()
140
+ kw.pop("type", None)
141
+ return f"<{string_value} with options {kw}>"
142
+
143
+ def _to_field(self, extra_kwargs: t.MutableMapping[str, t.Any], builder: TableDefinitionBuilder) -> t.Optional[str]:
144
+ """
145
+ Convert a Typed Field instance to a pydal.Field.
146
+
147
+ Actual logic in TypeDAL._to_field but this function creates the pydal type name and updates the kwarg settings.
148
+ """
149
+ other_kwargs = self.kwargs.copy()
150
+ extra_kwargs.update(other_kwargs) # <- modifies and overwrites the default kwargs with user-specified ones
151
+ return extra_kwargs.pop("type", False) or builder.annotation_to_pydal_fieldtype(
152
+ self._type,
153
+ extra_kwargs,
154
+ )
155
+
156
+ def bind(self, field: pydal.objects.Field, table: pydal.objects.Table) -> None:
157
+ """
158
+ Bind the right db/table/field info to this class, so queries can be made using `Class.field == ...`.
159
+ """
160
+ self._table = table
161
+ self._field = field
162
+
163
+ def __getattr__(self, key: str) -> t.Any:
164
+ """
165
+ If the regular getattribute does not work, try to get info from the related Field.
166
+ """
167
+ with contextlib.suppress(AttributeError):
168
+ return super().__getattribute__(key)
169
+
170
+ # try on actual field:
171
+ return getattr(self._field, key)
172
+
173
+ def __eq__(self, other: t.Any) -> Query:
174
+ """
175
+ Performing == on a Field will result in a Query.
176
+ """
177
+ return t.cast(Query, self._field == other)
178
+
179
+ def __ne__(self, other: t.Any) -> Query:
180
+ """
181
+ Performing != on a Field will result in a Query.
182
+ """
183
+ return t.cast(Query, self._field != other)
184
+
185
+ def __gt__(self, other: t.Any) -> Query:
186
+ """
187
+ Performing > on a Field will result in a Query.
188
+ """
189
+ return t.cast(Query, self._field > other)
190
+
191
+ def __lt__(self, other: t.Any) -> Query:
192
+ """
193
+ Performing < on a Field will result in a Query.
194
+ """
195
+ return t.cast(Query, self._field < other)
196
+
197
+ def __ge__(self, other: t.Any) -> Query:
198
+ """
199
+ Performing >= on a Field will result in a Query.
200
+ """
201
+ return t.cast(Query, self._field >= other)
202
+
203
+ def __le__(self, other: t.Any) -> Query:
204
+ """
205
+ Performing <= on a Field will result in a Query.
206
+ """
207
+ return t.cast(Query, self._field <= other)
208
+
209
+ def __hash__(self) -> int:
210
+ """
211
+ Shadow Field.__hash__.
212
+ """
213
+ return hash(self._field)
214
+
215
+ def __invert__(self) -> Expression:
216
+ """
217
+ Performing ~ on a Field will result in an Expression.
218
+ """
219
+ if not self._field: # pragma: no cover
220
+ raise ValueError("Unbound Field can not be inverted!")
221
+
222
+ return t.cast(Expression, ~self._field)
223
+
224
+ def lower(self) -> Expression:
225
+ """
226
+ For string-fields: compare lowercased values.
227
+ """
228
+ if not self._field: # pragma: no cover
229
+ raise ValueError("Unbound Field can not be lowered!")
230
+
231
+ return t.cast(Expression, self._field.lower())
232
+
233
+
234
+ def is_typed_field(cls: t.Any) -> t.TypeGuard["TypedField[t.Any]"]:
235
+ """
236
+ Is `cls` an instance or subclass of TypedField?
237
+
238
+ Deprecated
239
+ """
240
+ return isinstance(cls, TypedField) or (
241
+ isinstance(t.get_origin(cls), type) and issubclass(t.get_origin(cls), TypedField)
242
+ )
243
+
244
+
24
245
  ## specific
25
- def StringField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
246
+ def StringField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
26
247
  """
27
248
  Pydal type is string, Python type is str.
28
249
  """
@@ -33,7 +254,7 @@ def StringField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
33
254
  String = StringField
34
255
 
35
256
 
36
- def TextField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
257
+ def TextField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
37
258
  """
38
259
  Pydal type is text, Python type is str.
39
260
  """
@@ -44,7 +265,7 @@ def TextField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
44
265
  Text = TextField
45
266
 
46
267
 
47
- def BlobField(**kw: Unpack[FieldSettings]) -> TypedField[bytes]:
268
+ def BlobField(**kw: t.Unpack[FieldSettings]) -> TypedField[bytes]:
48
269
  """
49
270
  Pydal type is blob, Python type is bytes.
50
271
  """
@@ -55,7 +276,7 @@ def BlobField(**kw: Unpack[FieldSettings]) -> TypedField[bytes]:
55
276
  Blob = BlobField
56
277
 
57
278
 
58
- def BooleanField(**kw: Unpack[FieldSettings]) -> TypedField[bool]:
279
+ def BooleanField(**kw: t.Unpack[FieldSettings]) -> TypedField[bool]:
59
280
  """
60
281
  Pydal type is boolean, Python type is bool.
61
282
  """
@@ -66,7 +287,7 @@ def BooleanField(**kw: Unpack[FieldSettings]) -> TypedField[bool]:
66
287
  Boolean = BooleanField
67
288
 
68
289
 
69
- def IntegerField(**kw: Unpack[FieldSettings]) -> TypedField[int]:
290
+ def IntegerField(**kw: t.Unpack[FieldSettings]) -> TypedField[int]:
70
291
  """
71
292
  Pydal type is integer, Python type is int.
72
293
  """
@@ -77,7 +298,7 @@ def IntegerField(**kw: Unpack[FieldSettings]) -> TypedField[int]:
77
298
  Integer = IntegerField
78
299
 
79
300
 
80
- def DoubleField(**kw: Unpack[FieldSettings]) -> TypedField[float]:
301
+ def DoubleField(**kw: t.Unpack[FieldSettings]) -> TypedField[float]:
81
302
  """
82
303
  Pydal type is double, Python type is float.
83
304
  """
@@ -88,7 +309,7 @@ def DoubleField(**kw: Unpack[FieldSettings]) -> TypedField[float]:
88
309
  Double = DoubleField
89
310
 
90
311
 
91
- def DecimalField(n: int, m: int, **kw: Unpack[FieldSettings]) -> TypedField[decimal.Decimal]:
312
+ def DecimalField(n: int, m: int, **kw: t.Unpack[FieldSettings]) -> TypedField[decimal.Decimal]:
92
313
  """
93
314
  Pydal type is decimal, Python type is Decimal.
94
315
  """
@@ -99,7 +320,7 @@ def DecimalField(n: int, m: int, **kw: Unpack[FieldSettings]) -> TypedField[deci
99
320
  Decimal = DecimalField
100
321
 
101
322
 
102
- def DateField(**kw: Unpack[FieldSettings]) -> TypedField[dt.date]:
323
+ def DateField(**kw: t.Unpack[FieldSettings]) -> TypedField[dt.date]:
103
324
  """
104
325
  Pydal type is date, Python type is datetime.date.
105
326
  """
@@ -110,7 +331,7 @@ def DateField(**kw: Unpack[FieldSettings]) -> TypedField[dt.date]:
110
331
  Date = DateField
111
332
 
112
333
 
113
- def TimeField(**kw: Unpack[FieldSettings]) -> TypedField[dt.time]:
334
+ def TimeField(**kw: t.Unpack[FieldSettings]) -> TypedField[dt.time]:
114
335
  """
115
336
  Pydal type is time, Python type is datetime.time.
116
337
  """
@@ -121,7 +342,7 @@ def TimeField(**kw: Unpack[FieldSettings]) -> TypedField[dt.time]:
121
342
  Time = TimeField
122
343
 
123
344
 
124
- def DatetimeField(**kw: Unpack[FieldSettings]) -> TypedField[dt.datetime]:
345
+ def DatetimeField(**kw: t.Unpack[FieldSettings]) -> TypedField[dt.datetime]:
125
346
  """
126
347
  Pydal type is datetime, Python type is datetime.datetime.
127
348
  """
@@ -132,7 +353,7 @@ def DatetimeField(**kw: Unpack[FieldSettings]) -> TypedField[dt.datetime]:
132
353
  Datetime = DatetimeField
133
354
 
134
355
 
135
- def PasswordField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
356
+ def PasswordField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
136
357
  """
137
358
  Pydal type is password, Python type is str.
138
359
  """
@@ -143,7 +364,7 @@ def PasswordField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
143
364
  Password = PasswordField
144
365
 
145
366
 
146
- def UploadField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
367
+ def UploadField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
147
368
  """
148
369
  Pydal type is upload, Python type is str.
149
370
  """
@@ -153,11 +374,10 @@ def UploadField(**kw: Unpack[FieldSettings]) -> TypedField[str]:
153
374
 
154
375
  Upload = UploadField
155
376
 
156
- T_subclass = typing.TypeVar("T_subclass", TypedTable, Table)
157
-
158
377
 
159
378
  def ReferenceField(
160
- other_table: str | typing.Type[TypedTable] | TypedTable | Table | T_subclass, **kw: Unpack[FieldSettings]
379
+ other_table: str | t.Type[TypedTable] | TypedTable | Table | T_subclass,
380
+ **kw: t.Unpack[FieldSettings],
161
381
  ) -> TypedField[int]:
162
382
  """
163
383
  Pydal type is reference, Python type is int (id).
@@ -180,7 +400,7 @@ def ReferenceField(
180
400
  Reference = ReferenceField
181
401
 
182
402
 
183
- def ListStringField(**kw: Unpack[FieldSettings]) -> TypedField[list[str]]:
403
+ def ListStringField(**kw: t.Unpack[FieldSettings]) -> TypedField[list[str]]:
184
404
  """
185
405
  Pydal type is list:string, Python type is list of str.
186
406
  """
@@ -191,7 +411,7 @@ def ListStringField(**kw: Unpack[FieldSettings]) -> TypedField[list[str]]:
191
411
  ListString = ListStringField
192
412
 
193
413
 
194
- def ListIntegerField(**kw: Unpack[FieldSettings]) -> TypedField[list[int]]:
414
+ def ListIntegerField(**kw: t.Unpack[FieldSettings]) -> TypedField[list[int]]:
195
415
  """
196
416
  Pydal type is list:integer, Python type is list of int.
197
417
  """
@@ -202,7 +422,7 @@ def ListIntegerField(**kw: Unpack[FieldSettings]) -> TypedField[list[int]]:
202
422
  ListInteger = ListIntegerField
203
423
 
204
424
 
205
- def ListReferenceField(other_table: str, **kw: Unpack[FieldSettings]) -> TypedField[list[int]]:
425
+ def ListReferenceField(other_table: str, **kw: t.Unpack[FieldSettings]) -> TypedField[list[int]]:
206
426
  """
207
427
  Pydal type is list:reference, Python type is list of int (id).
208
428
  """
@@ -213,7 +433,7 @@ def ListReferenceField(other_table: str, **kw: Unpack[FieldSettings]) -> TypedFi
213
433
  ListReference = ListReferenceField
214
434
 
215
435
 
216
- def JSONField(**kw: Unpack[FieldSettings]) -> TypedField[object]:
436
+ def JSONField(**kw: t.Unpack[FieldSettings]) -> TypedField[object]:
217
437
  """
218
438
  Pydal type is json, Python type is object (can be anything JSON-encodable).
219
439
  """
@@ -221,7 +441,7 @@ def JSONField(**kw: Unpack[FieldSettings]) -> TypedField[object]:
221
441
  return TypedField(object, **kw)
222
442
 
223
443
 
224
- def BigintField(**kw: Unpack[FieldSettings]) -> TypedField[int]:
444
+ def BigintField(**kw: t.Unpack[FieldSettings]) -> TypedField[int]:
225
445
  """
226
446
  Pydal type is bigint, Python type is int.
227
447
  """
@@ -241,7 +461,7 @@ NativeTimestampField = SQLCustomType(
241
461
  )
242
462
 
243
463
 
244
- def TimestampField(**kw: Unpack[FieldSettings]) -> TypedField[dt.datetime]:
464
+ def TimestampField(**kw: t.Unpack[FieldSettings]) -> TypedField[dt.datetime]:
245
465
  """
246
466
  Database type is timestamp, Python type is datetime.
247
467
 
@@ -275,7 +495,7 @@ def safe_decode_native_point(value: str | None) -> tuple[float, ...]:
275
495
 
276
496
  try:
277
497
  parsed = ast.literal_eval(value)
278
- return typing.cast(tuple[float, ...], parsed)
498
+ return t.cast(tuple[float, ...], parsed)
279
499
  except ValueError: # pragma: no cover
280
500
  # should not happen when inserted with `safe_encode_native_point` but you never know
281
501
  return ()
@@ -328,7 +548,7 @@ NativePointField = SQLCustomType(
328
548
  )
329
549
 
330
550
 
331
- def PointField(**kw: Unpack[FieldSettings]) -> TypedField[tuple[float, float]]:
551
+ def PointField(**kw: t.Unpack[FieldSettings]) -> TypedField[tuple[float, float]]:
332
552
  """
333
553
  Database type is point, Python type is tuple[float, float].
334
554
  """
@@ -344,9 +564,14 @@ NativeUUIDField = SQLCustomType(
344
564
  )
345
565
 
346
566
 
347
- def UUIDField(**kw: Unpack[FieldSettings]) -> TypedField[uuid.UUID]:
567
+ def UUIDField(**kw: t.Unpack[FieldSettings]) -> TypedField[uuid.UUID]:
348
568
  """
349
569
  Database type is uuid, Python type is UUID.
350
570
  """
351
571
  kw["type"] = NativeUUIDField
352
572
  return TypedField(uuid.UUID, **kw)
573
+
574
+
575
+ # note: import at the end to prevent circular imports:
576
+ from .define import TableDefinitionBuilder # noqa: E402
577
+ from .tables import TypedTable # noqa: E402
typedal/for_web2py.py CHANGED
@@ -6,7 +6,7 @@ import datetime as dt
6
6
 
7
7
  from pydal.validators import IS_NOT_IN_DB
8
8
 
9
- from .core import TypeDAL, TypedField, TypedTable
9
+ from . import TypeDAL, TypedField, TypedTable
10
10
  from .fields import TextField
11
11
  from .web2py_py4web_shared import AuthUser
12
12