TypeDAL 5.0.5__tar.gz → 5.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {typedal-5.0.5 → typedal-5.1.0}/PKG-INFO +1 -1
  2. {typedal-5.0.5 → typedal-5.1.0}/pyproject.toml +1 -1
  3. {typedal-5.0.5 → typedal-5.1.0}/pyproject.toml.orig +1 -1
  4. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/asynchronous.py +1 -0
  5. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/caching.py +1 -2
  6. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/cli.py +2 -1
  7. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/config.py +1 -1
  8. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/core.py +11 -6
  9. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/define.py +5 -3
  10. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/fields.py +4 -4
  11. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/mixins.py +1 -0
  12. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/query_builder.py +64 -17
  13. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/relationships.py +9 -8
  14. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/rows.py +30 -7
  15. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/serializers/typescript.py +2 -1
  16. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/tables.py +43 -8
  17. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/types.py +40 -54
  18. typedal-5.1.0/src/typedal/warnings.py +5 -0
  19. {typedal-5.0.5 → typedal-5.1.0}/README.md +0 -0
  20. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/__about__.py +0 -0
  21. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/__init__.py +0 -0
  22. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/constants.py +0 -0
  23. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/enum_helpers.py +0 -0
  24. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/for_py4web.py +0 -0
  25. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/for_web2py.py +0 -0
  26. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/helpers.py +0 -0
  27. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/py.typed +0 -0
  28. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/serializers/as_json.py +0 -0
  29. {typedal-5.0.5 → typedal-5.1.0}/src/typedal/web2py_py4web_shared.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 5.0.5
3
+ Version: 5.1.0
4
4
  Summary: Typing support for PyDAL
5
5
  Keywords:
6
6
  Author: Robin van der Noord
@@ -6,7 +6,7 @@ build-backend = "uv_build"
6
6
  name = "TypeDAL"
7
7
  description = "Typing support for PyDAL"
8
8
  readme = "README.md"
9
- version = "5.0.5"
9
+ version = "5.1.0"
10
10
  requires-python = ">=3.12"
11
11
  license-expression = "MIT"
12
12
  keywords = []
@@ -6,7 +6,7 @@ build-backend = "uv_build"
6
6
  name = "TypeDAL"
7
7
  description = 'Typing support for PyDAL'
8
8
  readme = "README.md"
9
- version = "5.0.5"
9
+ version = "5.1.0"
10
10
  requires-python = ">=3.12"
11
11
  license-expression = "MIT"
12
12
  keywords = []
@@ -434,6 +434,7 @@ class BlockingAccessHandler(ExecutionHandler):
434
434
  f"the extra query. command: {command}",
435
435
  category=BlockingDatabaseAccessWarning,
436
436
  skip_file_prefixes=_SKIP_FRAMES,
437
+ stacklevel=2,
437
438
  )
438
439
 
439
440
 
@@ -9,13 +9,12 @@ import json
9
9
  import typing as t
10
10
 
11
11
  import dill # nosec
12
- from pydal.objects import Field, Rows, Set
13
12
 
14
13
  from .fields import TypedField
15
14
  from .helpers import throw
16
15
  from .rows import TypedRows
17
16
  from .tables import TypedTable
18
- from .types import CacheStatus, Query
17
+ from .types import CacheStatus, Field, Query, Rows, Set
19
18
 
20
19
  if t.TYPE_CHECKING:
21
20
  from .core import TypeDAL
@@ -30,6 +30,7 @@ except ImportError as e:
30
30
  "to fix this.",
31
31
  source=e,
32
32
  category=RuntimeWarning,
33
+ stacklevel=2,
33
34
  )
34
35
  exit(127) # command not found
35
36
 
@@ -226,7 +227,7 @@ def setup(
226
227
  data.pop("connection", None)
227
228
 
228
229
  # ignore any None:
229
- old_contents["tool"]["typedal"] = {k: v for k, v in data.items() if v is not None}
230
+ old_contents["tool"]["typedal"] = {k: v for k, v in data.items() if v is not None} # type: ignore
230
231
 
231
232
  with toml_path.open("w") as f:
232
233
  tomlkit.dump(old_contents, f)
@@ -159,7 +159,7 @@ def _load_toml(path: str | bool | Path | None = True) -> tuple[str, AnyDict]:
159
159
 
160
160
  return str(toml_path) or "", t.cast(AnyDict, data["tool"]["typedal"])
161
161
  except Exception as e:
162
- warnings.warn(f"Could not load typedal config toml: {e}", source=e)
162
+ warnings.warn(f"Could not load typedal config toml: {e}", source=e, stacklevel=2)
163
163
  return str(toml_path) or "", {}
164
164
 
165
165
 
@@ -51,10 +51,10 @@ if t.TYPE_CHECKING:
51
51
 
52
52
  def _expression_subclasses() -> t.Iterator[type]:
53
53
  """
54
- Yield pydal.objects.Expression and every (nested) subclass currently loaded, e.g. Field and TypedField.
54
+ Yield Expression and every (nested) subclass currently loaded, e.g. Field and TypedField.
55
55
  """
56
- seen = {pydal.objects.Expression}
57
- stack = [pydal.objects.Expression]
56
+ seen: set[type[Expression]] = {Expression}
57
+ stack: list[type[Expression]] = [Expression]
58
58
  while stack:
59
59
  for subclass in stack.pop().__subclasses__():
60
60
  if subclass not in seen:
@@ -77,7 +77,7 @@ def _purge_dialect_expressions(adapter: t.Any) -> None:
77
77
  `gc.collect()` will not free it, and with it the entire db (tables, fields, models)
78
78
  it belongs to, for the lifetime of the process, unless explicitly undone here.
79
79
  """
80
- expressions = pydal.objects.Expression._dialect_expressions_
80
+ expressions = Expression._dialect_expressions_
81
81
  stale = [
82
82
  name
83
83
  for name, wrapper in expressions.items()
@@ -476,7 +476,12 @@ class TypeDAL(_TypeDALBase):
476
476
  delattr(self, tablename)
477
477
 
478
478
  if verbose:
479
- warnings.warn(f"{model} could not be migrated, try faking", source=e, category=RuntimeWarning)
479
+ warnings.warn(
480
+ f"{model} could not be migrated, try faking",
481
+ source=e,
482
+ category=RuntimeWarning,
483
+ stacklevel=2,
484
+ )
480
485
 
481
486
  # try again:
482
487
  return self.define(model, migrate=self._migrate, fake_migrate=self._migrate, redefine=True)
@@ -544,7 +549,7 @@ class TypeDAL(_TypeDALBase):
544
549
 
545
550
  return wrapper
546
551
 
547
- def __call__(self, *_args: T_Query, **kwargs: t.Any) -> "TypedSet": # ty: ignore[invalid-method-override]
552
+ def __call__(self, *_args: T_Query, **kwargs: t.Any) -> "TypedSet":
548
553
  """
549
554
  A db instance can be called directly to perform a query.
550
555
 
@@ -12,7 +12,6 @@ import types
12
12
  import typing as t
13
13
  import warnings
14
14
 
15
- import pydal
16
15
  from pydal.validators import IS_IN_SET, ValidationError, Validator
17
16
 
18
17
  from .constants import BASIC_MAPPINGS
@@ -124,7 +123,10 @@ class TableDefinitionBuilder:
124
123
  self.class_map[table._rname] = cls # table_model - sql name
125
124
  cls.__on_define__(self.db)
126
125
  else:
127
- warnings.warn("db.define used without inheriting TypedTable. This could lead to strange problems!")
126
+ warnings.warn(
127
+ "db.define used without inheriting TypedTable. This could lead to strange problems!",
128
+ stacklevel=2,
129
+ )
128
130
 
129
131
  if not tablename.startswith("typedal_") and cache_dependency:
130
132
  from .caching import _remove_cache, remove_cache_for_table
@@ -163,7 +165,7 @@ class TableDefinitionBuilder:
163
165
  if mapping := BASIC_MAPPINGS.get(ftype):
164
166
  # basi types
165
167
  return mapping
166
- elif isinstance(ftype, pydal.objects.Table):
168
+ elif isinstance(ftype, Table):
167
169
  # db.table
168
170
  return f"reference {ftype._tablename}"
169
171
  elif issubclass(type(ftype), type) and issubclass(ftype, TypedTable):
@@ -14,7 +14,6 @@ import uuid
14
14
 
15
15
  import pydal
16
16
  from pydal.helpers.classes import SQLCustomType
17
- from pydal.objects import Table
18
17
 
19
18
  from .core import TypeDAL
20
19
  from .types import (
@@ -26,6 +25,7 @@ from .types import (
26
25
  Query,
27
26
  T_annotation,
28
27
  T_MetaInstance,
28
+ Table,
29
29
  Validator,
30
30
  )
31
31
 
@@ -190,12 +190,12 @@ class TypedField[T_Value](Expression): # pragma: no cover
190
190
  extra_kwargs,
191
191
  )
192
192
 
193
- def bind(self, field: pydal.objects.Field, table: pydal.objects.Table) -> None:
193
+ def bind(self, field: Field, table: Table) -> None:
194
194
  """
195
195
  Bind the right db/table/field info to this class, so queries can be made using `Class.field == ...`.
196
196
  """
197
197
  self._table = table
198
- self._field = t.cast(Field, field)
198
+ self._field = field
199
199
 
200
200
  def unbind(self) -> None:
201
201
  """Remove references to the pydal objects created during `bind`."""
@@ -280,7 +280,7 @@ class TypedField[T_Value](Expression): # pragma: no cover
280
280
  return t.cast(Expression, self._field.lower())
281
281
 
282
282
 
283
- def rname(field: TypedField[t.Any] | Field | pydal.objects.Field) -> str:
283
+ def rname(field: TypedField[t.Any] | Field) -> str:
284
284
  """
285
285
  Return the full rname (table and field).
286
286
  """
@@ -180,6 +180,7 @@ class SlugMixin(Mixin):
180
180
  warnings.warn(
181
181
  "The 'slug_suffix' option is deprecated, use 'slug_suffix_length' instead.",
182
182
  DeprecationWarning,
183
+ stacklevel=2,
183
184
  )
184
185
 
185
186
  slug_suffix = slug_suffix_length or kw.get("slug_suffix", 0)
@@ -8,9 +8,10 @@ import datetime as dt
8
8
  import math
9
9
  import time
10
10
  import typing as t
11
+ import warnings
11
12
  from collections import defaultdict
12
13
 
13
- import pydal.objects
14
+ from pydal.helpers.classes import SQLALL
14
15
 
15
16
  from .asynchronous import run_async
16
17
  from .constants import DEFAULT_JOIN_OPTION, JOIN_OPTIONS
@@ -38,15 +39,17 @@ from .types import (
38
39
  Query,
39
40
  Row,
40
41
  Rows,
42
+ Select,
41
43
  SelectKwargs,
42
44
  T_MetaInstance,
43
45
  Table,
44
46
  merge_permissions,
45
47
  require_permission,
46
48
  )
49
+ from .warnings import UnusedWindowWarning
47
50
 
48
51
 
49
- class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
52
+ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
50
53
  """
51
54
  Abstration on top of pydal's query system.
52
55
  """
@@ -290,12 +293,12 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
290
293
 
291
294
  subquery = t.cast(Query, DummyQuery())
292
295
  for query_part in queries_or_lambdas:
293
- if isinstance(query_part, (Field, pydal.objects.Field)) or is_typed_field(query_part):
294
- subquery |= t.cast(Query, query_part != None)
295
- elif isinstance(query_part, (pydal.objects.Query, Expression, pydal.objects.Expression)):
296
+ if isinstance(query_part, Field) or is_typed_field(query_part):
297
+ subquery |= query_part != None
298
+ elif isinstance(query_part, (Query, Expression)):
296
299
  subquery |= t.cast(Query, query_part)
297
300
  elif callable(query_part):
298
- if result := query_part(self.model): # ty: ignore[call-top-callable]
301
+ if result := query_part(self.model):
299
302
  subquery |= result
300
303
  elif isinstance(query_part, dict):
301
304
  subsubquery = DummyQuery()
@@ -401,7 +404,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
401
404
  if len(fields) != 1:
402
405
  raise ValueError("join(field, condition=...) can only be used with exactly one field!")
403
406
 
404
- if isinstance(condition, pydal.objects.Query):
407
+ if isinstance(condition, Query):
405
408
  condition = as_lambda(condition)
406
409
 
407
410
  field = fields[0]
@@ -418,8 +421,8 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
418
421
  if len(fields) != 1:
419
422
  raise ValueError("join(field, on=...) can only be used with exactly one field!")
420
423
 
421
- if isinstance(on, pydal.objects.Expression):
422
- on = t.cast(list[Expression], [on])
424
+ if isinstance(on, Expression):
425
+ on = [on]
423
426
 
424
427
  if isinstance(on, list):
425
428
  on = t.cast(OnQuery, as_lambda(on))
@@ -661,6 +664,17 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
661
664
 
662
665
  query, select_args, select_kwargs = self._before_query(metadata, add_id=add_id)
663
666
 
667
+ window_size = metadata.get("window_size", None)
668
+ is_iterating = metadata.get("iterating", False)
669
+
670
+ if window_size is not None and is_iterating is not True:
671
+ warnings.warn(
672
+ "A window size was configured, but collect() was called directly. "
673
+ "Use iteration over the query builder to fetch rows in windows.",
674
+ stacklevel=2,
675
+ category=UnusedWindowWarning,
676
+ )
677
+
664
678
  metadata["sql"] = db(query)._select(*select_args, **select_kwargs)
665
679
 
666
680
  if verbose: # pragma: no cover
@@ -856,7 +870,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
856
870
  expression_without_direction, _, direction = expression.rpartition(" ")
857
871
  return [expression_without_direction if direction.upper() in {"ASC", "DESC"} else orderby]
858
872
 
859
- if isinstance(orderby, pydal.objects.Field):
873
+ if isinstance(orderby, Field):
860
874
  return t.cast(list[OrderBy], [orderby])
861
875
 
862
876
  fields = []
@@ -968,8 +982,8 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
968
982
  if not isinstance(on, list):
969
983
  on = [on]
970
984
 
971
- on = [_ for _ in on if isinstance(_, pydal.objects.Expression)]
972
- left_joins.extend(on) # ty: ignore[invalid-argument-type]
985
+ on = [_ for _ in on if isinstance(_, Expression)]
986
+ left_joins.extend(on)
973
987
  elif method == "left":
974
988
  # Generate left join condition
975
989
  if not is_self_reference:
@@ -1032,7 +1046,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
1032
1046
  if pre_alias != post_alias:
1033
1047
  updated_args = []
1034
1048
  for arg in select_args:
1035
- if isinstance(arg, pydal.objects.SQLALL):
1049
+ if isinstance(arg, SQLALL):
1036
1050
  updated_args.extend(str(field).replace(f"{pre_alias}.", f"{post_alias}.") for field in arg._table)
1037
1051
  else:
1038
1052
  updated_args.append(str(arg).replace(f"{pre_alias}.", f"{post_alias}."))
@@ -1221,13 +1235,40 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
1221
1235
  """
1222
1236
  You can start iterating a Query Builder object before calling collect, for ease of use.
1223
1237
  """
1224
- yield from self.collect()
1238
+ builder = self._extend(metadata={"iterating": True})
1239
+ window_size = self.metadata.get("window_size", None)
1240
+
1241
+ if not window_size:
1242
+ yield from builder.collect()
1243
+ return
1244
+
1245
+ for chunk in builder.chunk(window_size):
1246
+ yield from chunk
1247
+
1248
+ def __await__(self):
1249
+ return self.collect_async().__await__()
1250
+
1251
+ async def __aiter__(self):
1252
+ builder = self._extend(metadata={"iterating": True})
1253
+
1254
+ window_size = self.metadata.get("window_size", None)
1255
+
1256
+ if window_size:
1257
+ async for chunk in builder.chunk_async(window_size):
1258
+ for row in chunk:
1259
+ yield row
1260
+ else:
1261
+ rows = await builder.collect_async()
1262
+ for row in rows:
1263
+ yield row
1264
+
1265
+ def window(self, window_size: int) -> QueryBuilder[T_MetaInstance]:
1266
+ return self._extend(metadata={"window_size": window_size})
1225
1267
 
1226
1268
  def __count(
1227
1269
  self,
1228
1270
  db: TypeDAL,
1229
1271
  distinct: t.Optional[bool] = None,
1230
- *,
1231
1272
  include_left_for_distinct: bool = True,
1232
1273
  ) -> Query:
1233
1274
  # internal, shared logic between .count and ._count
@@ -1350,7 +1391,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
1350
1391
  pass
1351
1392
  ```
1352
1393
  """
1353
- require_permission(self._permissions, "read")
1394
+ # require_permission checked in .collect()
1395
+ if "limitby" in self.select_kwargs:
1396
+ raise ValueError("chunk() cannot be combined with an existing limitby")
1397
+
1354
1398
  page = 1
1355
1399
 
1356
1400
  while rows := self.__paginate(chunk_size, page).collect():
@@ -1505,7 +1549,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable](pydal.objects.Select):
1505
1549
  writer can be visible halfway through the iteration; wrap it in `db.session()` if you
1506
1550
  need every page to come from the same snapshot.
1507
1551
  """
1508
- require_permission(self._permissions, "read")
1552
+ # require_permission checked in .collect()
1553
+ if "limitby" in self.select_kwargs:
1554
+ raise ValueError("chunk_async() cannot be combined with an existing limitby")
1555
+
1509
1556
  db = self._get_db()
1510
1557
  page = 1
1511
1558
 
@@ -6,14 +6,12 @@ import inspect
6
6
  import typing as t
7
7
  import warnings
8
8
 
9
- import pydal.objects
10
-
11
9
  from .config import LazyPolicy
12
10
  from .constants import JOIN_OPTIONS
13
11
  from .core import ForwardRef, TypeDAL, evaluate_forward_reference
14
12
  from .fields import TypedField
15
13
  from .helpers import extract_type_optional, looks_like, unwrap_type
16
- from .types import Condition, OnQuery, T_Field
14
+ from .types import Condition, OnQuery, T_Field, Table
17
15
 
18
16
  # default lazy policy is defined at the TypeDAL() instance settings level
19
17
 
@@ -112,7 +110,7 @@ class Relationship[To_Type]:
112
110
 
113
111
  @staticmethod
114
112
  def _error_duplicate_condition(condition: Condition, on: OnQuery) -> t.Never:
115
- warnings.warn(f"Relation | Both specified! {condition=} {on=}")
113
+ warnings.warn(f"Relation | Both specified! {condition=} {on=}", stacklevel=2)
116
114
  raise ValueError("Please specify either a condition or an 'on' statement for this relationship!")
117
115
 
118
116
  def __repr__(self) -> str:
@@ -199,7 +197,7 @@ class Relationship[To_Type]:
199
197
  if isinstance(self.table, str):
200
198
  return self.table
201
199
 
202
- if isinstance(self.table, pydal.objects.Table):
200
+ if isinstance(self.table, Table):
203
201
  return str(self.table)
204
202
 
205
203
  # else: typed table
@@ -280,6 +278,7 @@ class Relationship[To_Type]:
280
278
  f"Trying to access relationship '{self.name}' without joining. "
281
279
  f"Did you forget to use .join('{self.name}')? Returning empty value.",
282
280
  category=RuntimeWarning,
281
+ stacklevel=2,
283
282
  )
284
283
  return fallback_value
285
284
 
@@ -288,7 +287,7 @@ class Relationship[To_Type]:
288
287
  resolved_table = self.get_table(instance._db)
289
288
 
290
289
  builder = owner.where(id=instance.id).join(self.name)
291
- if issubclass(resolved_table, TypedTable) or isinstance(resolved_table, pydal.objects.Table):
290
+ if issubclass(resolved_table, TypedTable) or isinstance(resolved_table, Table):
292
291
  # is a table so we can select ALL and ignore non-required fields of parent row:
293
292
  builder = builder.select(owner.id, resolved_table.ALL)
294
293
 
@@ -298,6 +297,7 @@ class Relationship[To_Type]:
298
297
  "This performs an extra database query. "
299
298
  f"Consider using .join('{self.name}') for better performance.",
300
299
  category=RuntimeWarning,
300
+ stacklevel=2,
301
301
  )
302
302
 
303
303
  return builder.first()[self.name] # type: ignore
@@ -306,6 +306,7 @@ class Relationship[To_Type]:
306
306
  f"Failed to lazy load relationship '{self.name}': {e}",
307
307
  category=RuntimeWarning,
308
308
  source=e,
309
+ stacklevel=2,
309
310
  )
310
311
 
311
312
  return fallback_value
@@ -503,12 +504,12 @@ def to_relationship(
503
504
  try:
504
505
  condition = _generate_relationship_condition(cls, key, field)
505
506
  except Exception as e: # pragma: no cover
506
- warnings.warn("Could not generate Relationship condition", source=e)
507
+ warnings.warn("Could not generate Relationship condition", source=e, stacklevel=2)
507
508
  condition = None
508
509
 
509
510
  if not condition: # pragma: no cover
510
511
  # something went wrong, not a valid relationship
511
- warnings.warn(f"Invalid relationship for {cls.__name__}.{key}: {field}")
512
+ warnings.warn(f"Invalid relationship for {cls.__name__}.{key}: {field}", stacklevel=2)
512
513
  return None
513
514
 
514
515
  join = "left" if optional or t.get_origin(field) is list else "inner"
@@ -8,8 +8,6 @@ import csv
8
8
  import json
9
9
  import typing as t
10
10
 
11
- import pydal.objects
12
-
13
11
  from .asynchronous import run_async
14
12
  from .core import TypeDAL
15
13
  from .helpers import mktable
@@ -25,6 +23,7 @@ from .types import (
25
23
  Query,
26
24
  Row,
27
25
  Rows,
26
+ Set,
28
27
  T_MetaInstance,
29
28
  )
30
29
 
@@ -199,18 +198,42 @@ class TypedRows(t.Collection[T_MetaInstance], Rows):
199
198
 
200
199
  return mktable(data, headers)
201
200
 
202
- def group_by_value[T: t.Any, T_MetaInstance: _TypedTable]( # ty: ignore[shadowed-type-variable]
201
+ @t.overload
202
+ def group_by_value[T: t.Any](
203
+ self,
204
+ *fields: "TypedField[T] | str | Field",
205
+ one_result: t.Literal[True],
206
+ **kwargs: t.Any,
207
+ ) -> dict[T, T_MetaInstance]: ...
208
+
209
+ @t.overload
210
+ def group_by_value[T: t.Any](
211
+ self,
212
+ *fields: "TypedField[T] | str | Field",
213
+ one_result: t.Literal[False] = False,
214
+ **kwargs: t.Any,
215
+ ) -> dict[T, list[T_MetaInstance]]: ...
216
+
217
+ @t.overload
218
+ def group_by_value[T: t.Any](
219
+ self,
220
+ *fields: "TypedField[T] | str | Field",
221
+ one_result: bool,
222
+ **kwargs: t.Any,
223
+ ) -> dict[T, T_MetaInstance] | dict[T, list[T_MetaInstance]]: ...
224
+
225
+ def group_by_value[T: t.Any](
203
226
  self,
204
- *fields: "str | Field | TypedField[T]",
227
+ *fields: "TypedField[T] | str | Field",
205
228
  one_result: bool = False,
206
229
  **kwargs: t.Any,
207
- ) -> dict[T, list[T_MetaInstance]]:
230
+ ) -> dict[T, T_MetaInstance] | dict[T, list[T_MetaInstance]]:
208
231
  """
209
232
  Group the rows by a specific field (which will be the dict key).
210
233
  """
211
234
  kwargs["one_result"] = one_result
212
235
  result = super().group_by_value(*fields, **kwargs)
213
- return t.cast(dict[T, list[T_MetaInstance]], result)
236
+ return t.cast(dict[T, T_MetaInstance] | dict[T, list[T_MetaInstance]], result)
214
237
 
215
238
  def as_csv(self) -> str:
216
239
  """
@@ -533,7 +556,7 @@ class PaginatedRows(TypedRows[T_MetaInstance]):
533
556
  return {"data": super().as_dict(), "pagination": self.pagination}
534
557
 
535
558
 
536
- class TypedSet(pydal.objects.Set): # pragma: no cover
559
+ class TypedSet(Set): # pragma: no cover
537
560
  """
538
561
  Used to make pydal Set more typed.
539
562
 
@@ -31,7 +31,7 @@ class TypedDictRegistry(Singleton):
31
31
  def __init__(self) -> None:
32
32
  """Initialize the singleton registry and optional shared typtyp world."""
33
33
  self._types: dict[type, type[dict[str, t.Any]]] = {}
34
- self._world = typtyp.World() if typtyp else None
34
+ self._world = typtyp.World() if typtyp else None # type: ignore
35
35
  self._names: set[str] = set()
36
36
 
37
37
  @property
@@ -78,6 +78,7 @@ class TypedDictRegistry(Singleton):
78
78
  if world is None: # pragma: ignore
79
79
  warnings.warn(
80
80
  f"`{caller_name}` can not be used without the typescript extra. Please install `typedal[typescript]`",
81
+ stacklevel=2,
81
82
  )
82
83
  return ""
83
84
 
@@ -13,9 +13,9 @@ import types
13
13
  import typing as t
14
14
  import uuid
15
15
 
16
- import pydal.objects
16
+ import pydal
17
17
  from pydal._globals import DEFAULT
18
- from pydal.helpers.classes import SQLCallableList
18
+ from pydal.helpers.classes import SQLALL, SQLCallableList
19
19
 
20
20
  from .asynchronous import run_async
21
21
  from .constants import JOIN_OPTIONS
@@ -50,7 +50,7 @@ if t.TYPE_CHECKING:
50
50
 
51
51
 
52
52
  def reorder_fields(
53
- table: pydal.objects.Table,
53
+ table: Table,
54
54
  fields: t.Iterable[str | TypedField[t.Any] | Field],
55
55
  keep_others: bool = True,
56
56
  ) -> None:
@@ -65,7 +65,7 @@ def reorder_fields(
65
65
  - False: remove other fields (only keep what's specified).
66
66
  """
67
67
  # Normalize input to field names
68
- desired = [f.name if isinstance(f, (TypedField, Field, pydal.objects.Field)) else str(f) for f in fields]
68
+ desired = [f.name if isinstance(f, (TypedField, Field)) else str(f) for f in fields]
69
69
 
70
70
  new_order = [f for f in desired if f in table._fields]
71
71
 
@@ -105,6 +105,8 @@ class TableMeta(type):
105
105
  _table: Table | None = None
106
106
  _relationships: dict[str, Relationship[t.Any]] | None = None
107
107
  _permissions: Permissions | None = None
108
+ _singular: str
109
+ _plural: str
108
110
 
109
111
  #########################
110
112
  # TypeDAL custom logic: #
@@ -136,6 +138,23 @@ class TableMeta(type):
136
138
  self._relationships = None
137
139
  self._permissions = None
138
140
 
141
+ def __getattribute__(self, col: str) -> t.Any:
142
+ """
143
+ Resolve bound table fields before metaclass convenience methods.
144
+
145
+ Annotation-only fields are not stored in the model class dictionary, so
146
+ normal class lookup would otherwise return a same-named method defined
147
+ on this metaclass. For example, a model may have a 'window' column
148
+ while this metaclass also provides 'window()'. The column takes
149
+ precedence on the model class; use 'QueryBuilder(Model).window()' to
150
+ call the query helper in that case.
151
+ """
152
+ table = type.__getattribute__(self, "_table")
153
+ if table is not None and col in table._fields:
154
+ return table[col]
155
+
156
+ return type.__getattribute__(self, col)
157
+
139
158
  def __getattr__(self, col: str) -> t.Optional[Field]:
140
159
  """
141
160
  Magic method used by TypedTableMeta to get a database field with dot notation on a class.
@@ -149,6 +168,14 @@ class TableMeta(type):
149
168
 
150
169
  return None
151
170
 
171
+ def __setattr__(self, name: str, value: t.Any) -> None:
172
+ """Keep PyDAL's table label metadata in sync with its model class."""
173
+ if name.startswith("_") and self._table is not None:
174
+ # include base table for internal stuff such as _singular/_plural:
175
+ setattr(self._table, name, value)
176
+
177
+ super().__setattr__(name, value)
178
+
152
179
  def _ensure_table_defined(self) -> Table:
153
180
  if not self._table:
154
181
  raise EnvironmentError("@define or db.define is not called on this class yet!")
@@ -182,7 +209,7 @@ class TableMeta(type):
182
209
  else:
183
210
  return f"<unbound table {self.__name__}>"
184
211
 
185
- def from_row(self: t.Type[T_MetaInstance], row: pydal.objects.Row) -> T_MetaInstance:
212
+ def from_row(self: t.Type[T_MetaInstance], row: Row) -> T_MetaInstance:
186
213
  """
187
214
  Create a model instance from a pydal row.
188
215
  """
@@ -455,6 +482,12 @@ class TableMeta(type):
455
482
  """
456
483
  return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method, condition_and=condition_and)
457
484
 
485
+ def window(self: t.Type[T_MetaInstance], window_size: int) -> "QueryBuilder[T_MetaInstance]":
486
+ """
487
+ See QueryBuilder.window!
488
+ """
489
+ return QueryBuilder(self).window(window_size)
490
+
458
491
  def collect(self: t.Type[T_MetaInstance], verbose: bool = False) -> "TypedRows[T_MetaInstance]":
459
492
  """
460
493
  See QueryBuilder.collect!
@@ -642,7 +675,7 @@ class TableMeta(type):
642
675
  return await QueryBuilder(self).collect_into_async(into=into, verbose=verbose, init=init)
643
676
 
644
677
  @property
645
- def ALL(cls) -> pydal.objects.SQLALL:
678
+ def ALL(cls) -> SQLALL:
646
679
  """
647
680
  Select all fields for this table.
648
681
  """
@@ -931,6 +964,8 @@ class _TypedTable(metaclass=TableMeta):
931
964
  _after_delete: list[t.Callable[[Set], t.Optional[bool]]]
932
965
  _rows: tuple[Row, ...]
933
966
  _with: list[str]
967
+ _singular: t.ClassVar[str]
968
+ _plural: t.ClassVar[str]
934
969
 
935
970
  @classmethod
936
971
  def __on_define__(cls, db: TypeDAL) -> None:
@@ -1131,7 +1166,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1131
1166
 
1132
1167
  def __new__(
1133
1168
  cls,
1134
- row_or_id: t.Union[Row, Query, pydal.objects.Set, int, str, "TypedTable", None] = None,
1169
+ row_or_id: t.Union[Row, Query, Set, int, str, "TypedTable", None] = None,
1135
1170
  **filters: t.Any,
1136
1171
  ) -> t.Self:
1137
1172
  """
@@ -1149,7 +1184,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1149
1184
  # existing typed table instance!
1150
1185
  return t.cast(t.Self, row_or_id)
1151
1186
 
1152
- elif isinstance(row_or_id, pydal.objects.Row):
1187
+ elif isinstance(row_or_id, Row):
1153
1188
  row = row_or_id
1154
1189
  elif row_or_id is not None:
1155
1190
  row = table(row_or_id, **filters)
@@ -20,6 +20,7 @@ from pydal.objects import Field as _Field
20
20
  from pydal.objects import Query as _Query
21
21
  from pydal.objects import Row as _Row
22
22
  from pydal.objects import Rows as _Rows
23
+ from pydal.objects import Select as _Select
23
24
  from pydal.objects import Set as _Set
24
25
  from pydal.objects import Table as _Table
25
26
  from pydal.validators import Validator as _Validator
@@ -147,20 +148,26 @@ class FileSystemLike(t.Protocol): # pragma: no cover
147
148
  # pydal Wrappers (to help mypy understand these classes)
148
149
  # ---------------------------------------------------------------------------
149
150
 
151
+ # same for typing and runtime:
152
+ Row: t.TypeAlias = _Row
153
+ Validator: t.TypeAlias = _Validator
154
+ Table: t.TypeAlias = _Table
155
+ Reference: t.TypeAlias = _Reference
150
156
 
151
- class Query(_Query):
152
- """Pydal Query object. Makes mypy happy."""
153
-
154
-
155
- class Expression(_Expression):
156
- """Pydal Expression object. Make mypy happy."""
157
+ # has to be (fake) subclass during type-checking:
158
+ if t.TYPE_CHECKING:
157
159
 
160
+ class Query(_Query):
161
+ """Pydal Query object. Makes mypy happy."""
158
162
 
159
- class Set(_Set):
160
- """Pydal Set object. Make mypy happy."""
163
+ class Expression(_Expression):
164
+ """Pydal Expression object. Makes mypy happy."""
161
165
 
166
+ class Set(_Set):
167
+ """Pydal Set object. Makes mypy happy."""
162
168
 
163
- if t.TYPE_CHECKING:
169
+ class Select(_Select):
170
+ """Pydal Select object. Makes mypy happy."""
164
171
 
165
172
  class OpRow:
166
173
  """
@@ -188,57 +195,34 @@ if t.TYPE_CHECKING:
188
195
  def values(self) -> t.Iterable[t.Any]:
189
196
  """Dictionary-like value iteration."""
190
197
 
191
- else:
192
-
193
- class OpRow(_OpRow):
194
- """Runtime OpRow, using pydal's version."""
195
-
196
-
197
- class Reference(_Reference):
198
- """Pydal Reference object. Make mypy happy."""
199
-
200
-
201
- class Field(_Field):
202
- """Pydal Field object. Make mypy happy."""
203
-
204
- _rname: str
198
+ class Field(_Field):
199
+ """Pydal Field object. Make mypy happy."""
205
200
 
206
- def __eq__(self, other: t.Any) -> Query: # ty: ignore[invalid-method-override]
207
- """Comparing fields produces a PyDAL query."""
208
- return t.cast(Query, super().__eq__(other))
201
+ _rname: str
209
202
 
210
- def __ne__(self, other: t.Any) -> Query: # ty: ignore[invalid-method-override]
211
- """Comparing fields produces a PyDAL query."""
212
- return t.cast(Query, super().__ne__(other))
203
+ def __eq__(self, other: t.Any) -> Query: # ty: ignore[invalid-method-override]
204
+ """Comparing fields produces a PyDAL query."""
205
+ return t.cast(Query, super().__eq__(other))
213
206
 
214
- def __hash__(self) -> int:
215
- """Keep fields hashable after overriding equality."""
216
- return super().__hash__()
207
+ def __ne__(self, other: t.Any) -> Query: # ty: ignore[invalid-method-override]
208
+ """Comparing fields produces a PyDAL query."""
209
+ return t.cast(Query, super().__ne__(other))
217
210
 
211
+ def __hash__(self) -> int:
212
+ """Keep fields hashable after overriding equality."""
213
+ return super().__hash__()
218
214
 
219
- class Rows(_Rows):
220
- """Pydal Rows object. Make mypy happy."""
215
+ class Rows(_Rows):
216
+ def column(self, column: t.Any = None) -> list[t.Any]: ...
221
217
 
222
- def column(self, column: t.Any = None) -> list[t.Any]:
223
- """
224
- Get a list of all values in a specific column.
225
-
226
- Example:
227
- rows.column('name') -> ['Name 1', 'Name 2', ...]
228
- """
229
- return [r[str(column) if column else self.colnames[0]] for r in self]
230
-
231
-
232
- class Row(_Row):
233
- """Pydal Row object. Make mypy happy."""
234
-
235
-
236
- class Validator(_Validator):
237
- """Pydal Validator object. Make mypy happy."""
238
-
239
-
240
- class Table(_Table):
241
- """Pydal Table object. Make mypy happy."""
218
+ else:
219
+ Query: t.TypeAlias = _Query
220
+ Expression: t.TypeAlias = _Expression
221
+ Set: t.TypeAlias = _Set
222
+ Select: t.TypeAlias = _Select
223
+ Rows: t.TypeAlias = _Rows
224
+ OpRow: t.TypeAlias = _OpRow
225
+ Field: t.TypeAlias = _Field
242
226
 
243
227
 
244
228
  # ---------------------------------------------------------------------------
@@ -329,6 +313,8 @@ class Metadata(t.TypedDict):
329
313
  select_duration: t.NotRequired[float]
330
314
  relationships: t.NotRequired[set[str]]
331
315
  sql: t.NotRequired[str]
316
+ window_size: t.NotRequired[int]
317
+ iterating: t.NotRequired[bool]
332
318
 
333
319
 
334
320
  class FieldSettings(t.TypedDict, total=False):
@@ -0,0 +1,5 @@
1
+ """Warning types emitted by TypeDAL."""
2
+
3
+
4
+ class UnusedWindowWarning(RuntimeWarning):
5
+ """Warn when a window size is configured but the query is collected directly."""
File without changes
File without changes
File without changes
File without changes