TypeDAL 5.1.0__tar.gz → 5.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {typedal-5.1.0 → typedal-5.1.2}/PKG-INFO +1 -1
  2. {typedal-5.1.0 → typedal-5.1.2}/pyproject.toml +3 -3
  3. {typedal-5.1.0 → typedal-5.1.2}/pyproject.toml.orig +3 -3
  4. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/cli.py +1 -1
  5. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/core.py +15 -13
  6. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/query_builder.py +42 -7
  7. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/relationships.py +4 -2
  8. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/rows.py +2 -3
  9. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/serializers/typescript.py +1 -1
  10. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/tables.py +1 -1
  11. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/warnings.py +4 -0
  12. {typedal-5.1.0 → typedal-5.1.2}/README.md +0 -0
  13. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/__about__.py +0 -0
  14. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/__init__.py +0 -0
  15. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/asynchronous.py +0 -0
  16. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/caching.py +0 -0
  17. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/config.py +0 -0
  18. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/constants.py +0 -0
  19. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/define.py +0 -0
  20. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/enum_helpers.py +0 -0
  21. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/fields.py +0 -0
  22. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/for_py4web.py +0 -0
  23. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/for_web2py.py +0 -0
  24. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/helpers.py +0 -0
  25. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/mixins.py +0 -0
  26. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/py.typed +0 -0
  27. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/serializers/as_json.py +0 -0
  28. {typedal-5.1.0 → typedal-5.1.2}/src/typedal/types.py +0 -0
  29. {typedal-5.1.0 → typedal-5.1.2}/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.1.0
3
+ Version: 5.1.2
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.1.0"
9
+ version = "5.1.2"
10
10
  requires-python = ">=3.12"
11
11
  license-expression = "MIT"
12
12
  keywords = []
@@ -164,7 +164,7 @@ badge = true
164
164
  mypy = "--disable-error-code misc"
165
165
 
166
166
  [tool.black]
167
- target-version = ["py314"]
167
+ target-version = ["py312"]
168
168
  line-length = 120
169
169
  extend-exclude = '''
170
170
  # A regex preceded with ^/ will apply only to files and directories
@@ -199,7 +199,7 @@ exclude = [
199
199
  ]
200
200
 
201
201
  [tool.ruff]
202
- target-version = "py314"
202
+ target-version = "py312"
203
203
  line-length = 120
204
204
  extend-exclude = [
205
205
  "*.bak/",
@@ -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.1.0"
9
+ version = "5.1.2"
10
10
  requires-python = ">=3.12"
11
11
  license-expression = "MIT"
12
12
  keywords = []
@@ -181,7 +181,7 @@ badge = true
181
181
  mypy = "--disable-error-code misc"
182
182
 
183
183
  [tool.black]
184
- target-version = ["py314"]
184
+ target-version = ["py312"]
185
185
  line-length = 120
186
186
  # 'extend-exclude' excludes files or directories in addition to the defaults
187
187
  extend-exclude = '''
@@ -229,7 +229,7 @@ strict = true
229
229
  exclude = ["venv", ".bak"]
230
230
 
231
231
  [tool.ruff]
232
- target-version = "py314"
232
+ target-version = "py312"
233
233
  line-length = 120
234
234
 
235
235
  extend-exclude = ["*.bak/", "venv*/", "tests/", "example_*.py"]
@@ -227,7 +227,7 @@ def setup(
227
227
  data.pop("connection", None)
228
228
 
229
229
  # ignore any None:
230
- old_contents["tool"]["typedal"] = {k: v for k, v in data.items() if v is not None} # type: ignore
230
+ old_contents["tool"]["typedal"] = {k: v for k, v in data.items() if v is not None}
231
231
 
232
232
  with toml_path.open("w") as f:
233
233
  tomlkit.dump(old_contents, f)
@@ -34,7 +34,7 @@ from .helpers import (
34
34
  from .serializers.typescript import TypedDictRegistry
35
35
 
36
36
  # noinspection PyUnusedImports
37
- from .types import CacheStatus, Field, Template
37
+ from .types import CacheStatus, Expression, Field, Template
38
38
 
39
39
  try:
40
40
  # python 3.14+
@@ -46,10 +46,10 @@ except ImportError: # pragma: no cover
46
46
  if t.TYPE_CHECKING:
47
47
  from .fields import TypedField
48
48
  from .query_builder import QueryBuilder
49
- from .types import AnyDict, DefineKwargs, Expression, Rows, Set, T_Query, Table
49
+ from .types import AnyDict, DefineKwargs, Rows, Set, T_Query, Table
50
50
 
51
51
 
52
- def _expression_subclasses() -> t.Iterator[type]:
52
+ def _expression_subclasses() -> t.Iterator[type[Expression]]:
53
53
  """
54
54
  Yield Expression and every (nested) subclass currently loaded, e.g. Field and TypedField.
55
55
  """
@@ -100,7 +100,7 @@ def _purge_dialect_expressions(adapter: t.Any) -> None:
100
100
  # because then they will have different globals and it breaks!
101
101
 
102
102
 
103
- def evaluate_forward_reference_312(fw_ref: ForwardRef, namespace: dict[str, type]) -> type: # pragma: no cover
103
+ def evaluate_forward_reference_312(fw_ref: ForwardRef, namespace: t.Mapping[str, type]) -> type: # pragma: no cover
104
104
  """
105
105
  Extract the original type from a forward reference string.
106
106
 
@@ -110,13 +110,13 @@ def evaluate_forward_reference_312(fw_ref: ForwardRef, namespace: dict[str, type
110
110
  type,
111
111
  fw_ref._evaluate( # ty: ignore[deprecated]
112
112
  localns=locals(),
113
- globalns=globals() | namespace,
113
+ globalns=globals() | dict(namespace),
114
114
  recursive_guard=frozenset(),
115
115
  ),
116
116
  )
117
117
 
118
118
 
119
- def evaluate_forward_reference_313(fw_ref: ForwardRef, namespace: dict[str, type]) -> type: # pragma: no cover
119
+ def evaluate_forward_reference_313(fw_ref: ForwardRef, namespace: t.Mapping[str, type]) -> type: # pragma: no cover
120
120
  """
121
121
  Extract the original type from a forward reference string.
122
122
 
@@ -126,14 +126,14 @@ def evaluate_forward_reference_313(fw_ref: ForwardRef, namespace: dict[str, type
126
126
  type,
127
127
  fw_ref._evaluate( # ty: ignore[deprecated]
128
128
  localns=locals(),
129
- globalns=globals() | namespace,
129
+ globalns=globals() | dict(namespace),
130
130
  recursive_guard=frozenset(),
131
131
  type_params=(), # suggested since 3.13 (warning) and not supported before. Mandatory after 1.15!
132
132
  ),
133
133
  )
134
134
 
135
135
 
136
- def evaluate_forward_reference_314(fw_ref: ForwardRef, namespace: dict[str, type]) -> type: # pragma: no cover
136
+ def evaluate_forward_reference_314(fw_ref: ForwardRef, namespace: t.Mapping[str, type]) -> type: # pragma: no cover
137
137
  """
138
138
  Extract the original type from a forward reference string.
139
139
 
@@ -143,7 +143,7 @@ def evaluate_forward_reference_314(fw_ref: ForwardRef, namespace: dict[str, type
143
143
  type,
144
144
  fw_ref.evaluate(
145
145
  locals=locals(),
146
- globals=globals() | namespace,
146
+ globals=globals() | dict(namespace),
147
147
  type_params=(),
148
148
  ),
149
149
  )
@@ -151,7 +151,7 @@ def evaluate_forward_reference_314(fw_ref: ForwardRef, namespace: dict[str, type
151
151
 
152
152
  def evaluate_forward_reference(
153
153
  fw_ref: ForwardRef,
154
- namespace: dict[str, type] | None = None,
154
+ namespace: t.Mapping[str, type] | None = None,
155
155
  ) -> type: # pragma: no cover
156
156
  """
157
157
  Extract the original type from a forward reference string.
@@ -166,7 +166,7 @@ def evaluate_forward_reference(
166
166
  return evaluate_forward_reference_314(fw_ref, namespace=namespace or {})
167
167
 
168
168
 
169
- def resolve_annotation_313(ftype: str, namespace: dict[str, type] | None = None) -> type: # pragma: no cover
169
+ def resolve_annotation_313(ftype: str, namespace: t.Mapping[str, type] | None = None) -> type: # pragma: no cover
170
170
  """
171
171
  Resolve an annotation that's in string representation.
172
172
 
@@ -176,7 +176,7 @@ def resolve_annotation_313(ftype: str, namespace: dict[str, type] | None = None)
176
176
  return evaluate_forward_reference(fw_ref, namespace=namespace)
177
177
 
178
178
 
179
- def resolve_annotation_314(ftype: str, namespace: dict[str, type] | None = None) -> type: # pragma: no cover
179
+ def resolve_annotation_314(ftype: str, namespace: t.Mapping[str, type] | None = None) -> type: # pragma: no cover
180
180
  """
181
181
  Resolve an annotation that's in string representation.
182
182
 
@@ -186,7 +186,7 @@ def resolve_annotation_314(ftype: str, namespace: dict[str, type] | None = None)
186
186
  return evaluate_forward_reference(fw_ref, namespace=namespace)
187
187
 
188
188
 
189
- def resolve_annotation(ftype: str, namespace: dict[str, type] | None = None) -> type: # pragma: no cover
189
+ def resolve_annotation(ftype: str, namespace: t.Mapping[str, type] | None = None) -> type: # pragma: no cover
190
190
  """
191
191
  Resolve an annotation that's in string representation.
192
192
 
@@ -220,6 +220,8 @@ if t.TYPE_CHECKING:
220
220
 
221
221
  def has_representer(self, field_type: str) -> bool: ...
222
222
 
223
+ def represent(self, name: str, *args: t.Any, **kwargs: t.Any) -> str: ...
224
+
223
225
  # pydal exposes dynamic table attributes like `db.my_table`.
224
226
  # this keeps type checkers from flagging these as missing attributes.
225
227
  def __getattr__(self, item: str) -> "Table": ...
@@ -46,7 +46,19 @@ from .types import (
46
46
  merge_permissions,
47
47
  require_permission,
48
48
  )
49
- from .warnings import UnusedWindowWarning
49
+ from .warnings import NoopQueryWarning, UnusedWindowWarning
50
+
51
+
52
+ def warn_noop(method: str) -> None:
53
+ """
54
+ Warn that `method` was called without arguments, which has no effect.
55
+ """
56
+ # stacklevel: 1 = here, 2 = the query builder method, 3 = the caller.
57
+ warnings.warn(
58
+ f"`.{method}()` without arguments does nothing. You can remove this call.",
59
+ stacklevel=3,
60
+ category=NoopQueryWarning,
61
+ )
50
62
 
51
63
 
52
64
  class QueryBuilder[T_MetaInstance: _TypedTable](Select):
@@ -175,6 +187,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
175
187
  """
176
188
  Return a clone of this builder with permission overrides merged in.
177
189
  """
190
+ if not permissions and self: # ty: ignore[redundant-condition]
191
+ warn_noop("permissions")
192
+ return self
193
+
178
194
  return self._extend(permissions=permissions)
179
195
 
180
196
  def _normalize_select_option(
@@ -216,8 +232,15 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
216
232
  join: othertable.on(query) - do an INNER JOIN. Using TypeDAL relationships with .join() is recommended!
217
233
  left: othertable.on(query) - do a LEFT JOIN. Using TypeDAL relationships with .join() is recommended!
218
234
  cache: cache the query result to speed up repeated queries; e.g. (cache=(cache.ram, 3600), cacheable=True)
235
+
236
+ Calling this without any fields or options on a builder that already has settings
237
+ does nothing and emits a NoopQueryWarning.
219
238
  """
220
239
 
240
+ if not fields and not options and self:
241
+ warn_noop("select")
242
+ return self
243
+
221
244
  for key in ("distinct",):
222
245
  if options.get(key):
223
246
  options[key] = self._normalize_select_option(options[key])
@@ -282,7 +305,15 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
282
305
  .where(lambda table: table.id == 5).where(lambda table: table.id == 6) == (table.id == 5) & (table.id=6)
283
306
  When passing multiple queries to a single .where, they will be ORed:
284
307
  .where(lambda table: table.id == 5, lambda table: table.id == 6) == (table.id == 5) | (table.id=6)
308
+
309
+ Calling this without any arguments on a builder that already has settings
310
+ does nothing and emits a NoopQueryWarning.
311
+ Starting an empty builder (e.g. `Model.where()`) is allowed and stays silent.
285
312
  """
313
+ if not queries_or_lambdas and not filters and self:
314
+ warn_noop("where")
315
+ return self
316
+
286
317
  new_query = self.query
287
318
  table = self._ensure_table_defined()
288
319
 
@@ -1268,7 +1299,8 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
1268
1299
  def __count(
1269
1300
  self,
1270
1301
  db: TypeDAL,
1271
- distinct: t.Optional[bool] = None,
1302
+ distinct: bool | Field | TypedField[t.Any] = False,
1303
+ *,
1272
1304
  include_left_for_distinct: bool = True,
1273
1305
  ) -> Query:
1274
1306
  # internal, shared logic between .count and ._count
@@ -1292,9 +1324,11 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
1292
1324
 
1293
1325
  return query
1294
1326
 
1295
- def count(self, distinct: t.Optional[bool] = None) -> int:
1327
+ def count(self, distinct: bool | Field | TypedField[t.Any] = False) -> int:
1296
1328
  """
1297
1329
  Return the amount of rows matching the current query.
1330
+
1331
+ Passing a field counts its distinct values.
1298
1332
  """
1299
1333
  require_permission(self._permissions, "read")
1300
1334
  db = self._get_db()
@@ -1302,7 +1336,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
1302
1336
 
1303
1337
  return db(query).count(distinct)
1304
1338
 
1305
- def _count(self, distinct: t.Optional[bool] = None) -> str:
1339
+ def _count(self, distinct: bool | Field | TypedField[t.Any] = False) -> str:
1306
1340
  """
1307
1341
  Return the SQL for .count().
1308
1342
  """
@@ -1328,8 +1362,9 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
1328
1362
  return self.count()
1329
1363
 
1330
1364
  db = self._get_db()
1331
- query = self.__count(db, distinct=self.model.id, include_left_for_distinct=False) # ty: ignore[invalid-argument-type]
1332
- return db(query).count(self.model.id)
1365
+ distinct = t.cast(TypedField[int], self.model.id)
1366
+ query = self.__count(db, distinct=distinct, include_left_for_distinct=False)
1367
+ return db(query).count(distinct)
1333
1368
 
1334
1369
  def __paginate(
1335
1370
  self,
@@ -1523,7 +1558,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
1523
1558
  column = t.cast(t.Callable[..., list[T]], self.column)
1524
1559
  return await run_async(self._get_db(), column, field, **options)
1525
1560
 
1526
- async def count_async(self, distinct: t.Optional[bool] = None) -> int:
1561
+ async def count_async(self, distinct: bool | Field | TypedField[t.Any] = False) -> int:
1527
1562
  """
1528
1563
  Async twin of `count()`.
1529
1564
  """
@@ -253,10 +253,12 @@ class Relationship[To_Type]:
253
253
  # relationship queried on class, that's allowed
254
254
  return self
255
255
 
256
+ db = instance._db
257
+
256
258
  # instance: TypedTable instance
257
259
  # owner: TypedTable class
258
260
 
259
- if not self.name: # pragma: no cover
261
+ if not (self.name and db): # pragma: no cover
260
262
  raise ValueError("Relationship does not seem to be connected to a table field.")
261
263
 
262
264
  # Handle different lazy policies
@@ -284,7 +286,7 @@ class Relationship[To_Type]:
284
286
 
285
287
  # For "tolerate" and "allow", we fetch the data
286
288
  try:
287
- resolved_table = self.get_table(instance._db)
289
+ resolved_table = self.get_table(db)
288
290
 
289
291
  builder = owner.where(id=instance.id).join(self.name)
290
292
  if issubclass(resolved_table, TypedTable) or isinstance(resolved_table, Table):
@@ -10,6 +10,7 @@ import typing as t
10
10
 
11
11
  from .asynchronous import run_async
12
12
  from .core import TypeDAL
13
+ from .fields import TypedField
13
14
  from .helpers import mktable
14
15
  from .query_builder import QueryBuilder
15
16
  from .serializers import as_json
@@ -563,7 +564,7 @@ class TypedSet(Set): # pragma: no cover
563
564
  This class is not actually used, only 'cast' by TypeDAL.__call__
564
565
  """
565
566
 
566
- def count(self, distinct: bool | None = None, cache: AnyDict | None = None) -> int:
567
+ def count(self, distinct: bool | Field | TypedField[t.Any] = False, cache: AnyDict | None = None) -> int:
567
568
  """
568
569
  Count returns an int.
569
570
  """
@@ -585,5 +586,3 @@ class TypedSet(Set): # pragma: no cover
585
586
 
586
587
 
587
588
  # note: these imports exist at the bottom of this file to prevent circular import issues:
588
-
589
- from .fields import TypedField # noqa: E402
@@ -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 # type: ignore
34
+ self._world = typtyp.World() if typtyp else None # ty: ignore[redundant-condition]
35
35
  self._names: set[str] = set()
36
36
 
37
37
  @property
@@ -1152,7 +1152,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1152
1152
  "after_delete": cls._after_delete,
1153
1153
  }
1154
1154
 
1155
- def _setup_instance_methods(self) -> None:
1155
+ def _setup_instance_methods(self: t.Self | type[t.Self]) -> None:
1156
1156
  # use setattr instead of self.x = y to make the typecheckers happier
1157
1157
  setattr(self, "as_dict", self._as_dict)
1158
1158
  setattr(self, "__json__", self._as_json)
@@ -3,3 +3,7 @@
3
3
 
4
4
  class UnusedWindowWarning(RuntimeWarning):
5
5
  """Warn when a window size is configured but the query is collected directly."""
6
+
7
+
8
+ class NoopQueryWarning(RuntimeWarning):
9
+ """Warn when a query builder method is called without arguments, so it does nothing."""
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