TypeDAL 3.8.5__tar.gz → 3.9.1__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.

Potentially problematic release.


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

Files changed (58) hide show
  1. {typedal-3.8.5 → typedal-3.9.1}/CHANGELOG.md +12 -0
  2. {typedal-3.8.5 → typedal-3.9.1}/PKG-INFO +1 -1
  3. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/__about__.py +1 -1
  4. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/core.py +43 -11
  5. {typedal-3.8.5 → typedal-3.9.1}/tests/test_query_builder.py +10 -2
  6. {typedal-3.8.5 → typedal-3.9.1}/tests/test_relationships.py +39 -1
  7. {typedal-3.8.5 → typedal-3.9.1}/.github/workflows/su6.yml +0 -0
  8. {typedal-3.8.5 → typedal-3.9.1}/.gitignore +0 -0
  9. {typedal-3.8.5 → typedal-3.9.1}/.readthedocs.yml +0 -0
  10. {typedal-3.8.5 → typedal-3.9.1}/README.md +0 -0
  11. {typedal-3.8.5 → typedal-3.9.1}/coverage.svg +0 -0
  12. {typedal-3.8.5 → typedal-3.9.1}/docs/1_getting_started.md +0 -0
  13. {typedal-3.8.5 → typedal-3.9.1}/docs/2_defining_tables.md +0 -0
  14. {typedal-3.8.5 → typedal-3.9.1}/docs/3_building_queries.md +0 -0
  15. {typedal-3.8.5 → typedal-3.9.1}/docs/4_relationships.md +0 -0
  16. {typedal-3.8.5 → typedal-3.9.1}/docs/5_py4web.md +0 -0
  17. {typedal-3.8.5 → typedal-3.9.1}/docs/6_migrations.md +0 -0
  18. {typedal-3.8.5 → typedal-3.9.1}/docs/7_mixins.md +0 -0
  19. {typedal-3.8.5 → typedal-3.9.1}/docs/css/code_blocks.css +0 -0
  20. {typedal-3.8.5 → typedal-3.9.1}/docs/index.md +0 -0
  21. {typedal-3.8.5 → typedal-3.9.1}/docs/requirements.txt +0 -0
  22. {typedal-3.8.5 → typedal-3.9.1}/example_new.py +0 -0
  23. {typedal-3.8.5 → typedal-3.9.1}/example_old.py +0 -0
  24. {typedal-3.8.5 → typedal-3.9.1}/mkdocs.yml +0 -0
  25. {typedal-3.8.5 → typedal-3.9.1}/pyproject.toml +0 -0
  26. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/__init__.py +0 -0
  27. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/caching.py +0 -0
  28. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/cli.py +0 -0
  29. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/config.py +0 -0
  30. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/fields.py +0 -0
  31. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/for_py4web.py +0 -0
  32. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/for_web2py.py +0 -0
  33. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/helpers.py +0 -0
  34. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/mixins.py +0 -0
  35. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/py.typed +0 -0
  36. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/serializers/as_json.py +0 -0
  37. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/types.py +0 -0
  38. {typedal-3.8.5 → typedal-3.9.1}/src/typedal/web2py_py4web_shared.py +0 -0
  39. {typedal-3.8.5 → typedal-3.9.1}/tests/__init__.py +0 -0
  40. {typedal-3.8.5 → typedal-3.9.1}/tests/configs/simple.toml +0 -0
  41. {typedal-3.8.5 → typedal-3.9.1}/tests/configs/valid.env +0 -0
  42. {typedal-3.8.5 → typedal-3.9.1}/tests/configs/valid.toml +0 -0
  43. {typedal-3.8.5 → typedal-3.9.1}/tests/test_cli.py +0 -0
  44. {typedal-3.8.5 → typedal-3.9.1}/tests/test_config.py +0 -0
  45. {typedal-3.8.5 → typedal-3.9.1}/tests/test_docs_examples.py +0 -0
  46. {typedal-3.8.5 → typedal-3.9.1}/tests/test_helpers.py +0 -0
  47. {typedal-3.8.5 → typedal-3.9.1}/tests/test_json.py +0 -0
  48. {typedal-3.8.5 → typedal-3.9.1}/tests/test_main.py +0 -0
  49. {typedal-3.8.5 → typedal-3.9.1}/tests/test_mixins.py +0 -0
  50. {typedal-3.8.5 → typedal-3.9.1}/tests/test_mypy.py +0 -0
  51. {typedal-3.8.5 → typedal-3.9.1}/tests/test_orm.py +0 -0
  52. {typedal-3.8.5 → typedal-3.9.1}/tests/test_py4web.py +0 -0
  53. {typedal-3.8.5 → typedal-3.9.1}/tests/test_row.py +0 -0
  54. {typedal-3.8.5 → typedal-3.9.1}/tests/test_stats.py +0 -0
  55. {typedal-3.8.5 → typedal-3.9.1}/tests/test_table.py +0 -0
  56. {typedal-3.8.5 → typedal-3.9.1}/tests/test_web2py.py +0 -0
  57. {typedal-3.8.5 → typedal-3.9.1}/tests/test_xx_others.py +0 -0
  58. {typedal-3.8.5 → typedal-3.9.1}/tests/timings.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v3.9.1 (2024-10-25)
6
+
7
+ ### Fix
8
+
9
+ * Bool(QueryBuilder) should NOT look at the data but if any filters were applied ([`962ddf3`](https://github.com/trialandsuccess/TypeDAL/commit/962ddf37f5d1ac014b73fd6349099100cc7efb5f))
10
+
11
+ ## v3.9.0 (2024-10-25)
12
+
13
+ ### Feature
14
+
15
+ * Add `condition_and` to .join so you can add additional requirements to inner joins ([`0ae688b`](https://github.com/trialandsuccess/TypeDAL/commit/0ae688ba01f421c70a660b5bb5d9672484494aa4))
16
+
5
17
  ## v3.8.5 (2024-10-24)
6
18
 
7
19
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 3.8.5
3
+ Version: 3.9.1
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
@@ -5,4 +5,4 @@ This file contains the Version info for this package.
5
5
  # SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
6
6
  #
7
7
  # SPDX-License-Identifier: MIT
8
- __version__ = "3.8.5"
8
+ __version__ = "3.9.1"
@@ -130,6 +130,7 @@ class Relationship(typing.Generic[To_Type]):
130
130
  _type: To_Type
131
131
  table: Type["TypedTable"] | type | str
132
132
  condition: Condition
133
+ condition_and: Condition
133
134
  on: OnQuery
134
135
  multiple: bool
135
136
  join: JOIN_OPTIONS
@@ -140,6 +141,7 @@ class Relationship(typing.Generic[To_Type]):
140
141
  condition: Condition = None,
141
142
  join: JOIN_OPTIONS = None,
142
143
  on: OnQuery = None,
144
+ condition_and: Condition = None,
143
145
  ):
144
146
  """
145
147
  Should not be called directly, use relationship() instead!
@@ -152,6 +154,7 @@ class Relationship(typing.Generic[To_Type]):
152
154
  self.condition = condition
153
155
  self.join = "left" if on else join # .on is always left join!
154
156
  self.on = on
157
+ self.condition_and = condition_and
155
158
 
156
159
  if args := typing.get_args(_type):
157
160
  self.table = unwrap_type(args[0])
@@ -172,6 +175,7 @@ class Relationship(typing.Generic[To_Type]):
172
175
  update.get("condition") or self.condition,
173
176
  update.get("join") or self.join,
174
177
  update.get("on") or self.on,
178
+ update.get("condition_and") or self.condition_and,
175
179
  )
176
180
 
177
181
  def __repr__(self) -> str:
@@ -180,6 +184,10 @@ class Relationship(typing.Generic[To_Type]):
180
184
  """
181
185
  if callback := self.condition or self.on:
182
186
  src_code = inspect.getsource(callback).strip()
187
+
188
+ if c_and := self.condition_and:
189
+ and_code = inspect.getsource(c_and).strip()
190
+ src_code += " AND " + and_code
183
191
  else:
184
192
  cls_name = self._type if isinstance(self._type, str) else self._type.__name__ # type: ignore
185
193
  src_code = f"to {cls_name} (missing condition)"
@@ -1050,11 +1058,12 @@ class TableMeta(type):
1050
1058
  method: JOIN_OPTIONS = None,
1051
1059
  on: OnQuery | list[Expression] | Expression = None,
1052
1060
  condition: Condition = None,
1061
+ condition_and: Condition = None,
1053
1062
  ) -> "QueryBuilder[T_MetaInstance]":
1054
1063
  """
1055
1064
  See QueryBuilder.join!
1056
1065
  """
1057
- return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method)
1066
+ return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method, condition_and=condition_and)
1058
1067
 
1059
1068
  def collect(self: Type[T_MetaInstance], verbose: bool = False) -> "TypedRows[T_MetaInstance]":
1060
1069
  """
@@ -2221,9 +2230,19 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2221
2230
 
2222
2231
  def __bool__(self) -> bool:
2223
2232
  """
2224
- Querybuilder is truthy if it has rows.
2233
+ Querybuilder is truthy if it has any conditions.
2225
2234
  """
2226
- return self.count() > 0
2235
+ table = self.model._ensure_table_defined()
2236
+ default_query = typing.cast(Query, table.id > 0)
2237
+ return any(
2238
+ [
2239
+ self.query != default_query,
2240
+ self.select_args,
2241
+ self.select_kwargs,
2242
+ self.relationships,
2243
+ self.metadata,
2244
+ ]
2245
+ )
2227
2246
 
2228
2247
  def _extend(
2229
2248
  self,
@@ -2317,6 +2336,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2317
2336
  method: JOIN_OPTIONS = None,
2318
2337
  on: OnQuery | list[Expression] | Expression = None,
2319
2338
  condition: Condition = None,
2339
+ condition_and: Condition = None,
2320
2340
  ) -> "QueryBuilder[T_MetaInstance]":
2321
2341
  """
2322
2342
  Include relationship fields in the result.
@@ -2326,8 +2346,13 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2326
2346
 
2327
2347
  By default, the `method` defined in the relationship is used.
2328
2348
  This can be overwritten with the `method` keyword argument (left or inner)
2349
+
2350
+ `condition_and` can be used to add extra conditions to an inner join.
2329
2351
  """
2330
2352
  # todo: allow limiting amount of related rows returned for join?
2353
+ # todo: it would be nice if 'fields' could be an actual relationship
2354
+ # (Article.tags = list[Tag]) and you could change the .condition and .on
2355
+ # this could deprecate condition_and
2331
2356
 
2332
2357
  relationships = self.model.get_relationships()
2333
2358
 
@@ -2340,7 +2365,9 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2340
2365
  if isinstance(condition, pydal.objects.Query):
2341
2366
  condition = as_lambda(condition)
2342
2367
 
2343
- relationships = {str(fields[0]): Relationship(fields[0], condition=condition, join=method)}
2368
+ relationships = {
2369
+ str(fields[0]): Relationship(fields[0], condition=condition, join=method, condition_and=condition_and)
2370
+ }
2344
2371
  elif on:
2345
2372
  if len(fields) != 1:
2346
2373
  raise ValueError("join(field, on=...) can only be used with exactly one field!")
@@ -2350,15 +2377,17 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2350
2377
 
2351
2378
  if isinstance(on, list):
2352
2379
  on = as_lambda(on)
2353
- relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method)}
2380
+ relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method, condition_and=condition_and)}
2354
2381
 
2355
2382
  else:
2356
2383
  if fields:
2357
2384
  # join on every relationship
2358
- relationships = {str(k): relationships[str(k)] for k in fields}
2385
+ relationships = {str(k): relationships[str(k)].clone(condition_and=condition_and) for k in fields}
2359
2386
 
2360
2387
  if method:
2361
- relationships = {str(k): r.clone(join=method) for k, r in relationships.items()}
2388
+ relationships = {
2389
+ str(k): r.clone(join=method, condition_and=condition_and) for k, r in relationships.items()
2390
+ }
2362
2391
 
2363
2392
  return self._extend(relationships=relationships)
2364
2393
 
@@ -2579,7 +2608,6 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2579
2608
 
2580
2609
  metadata["relationships"] = set(self.relationships.keys())
2581
2610
 
2582
- # query = self._update_query_for_inner(db, model, query)
2583
2611
  join = []
2584
2612
  for key, relation in self.relationships.items():
2585
2613
  if not relation.condition or relation.join != "inner":
@@ -2587,7 +2615,11 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2587
2615
 
2588
2616
  other = relation.get_table(db)
2589
2617
  other = other.with_alias(f"{key}_{hash(relation)}")
2590
- join.append(other.on(relation.condition(model, other)))
2618
+ condition = relation.condition(model, other)
2619
+ if callable(relation.condition_and):
2620
+ condition &= relation.condition_and(model, other)
2621
+
2622
+ join.append(other.on(condition))
2591
2623
 
2592
2624
  if limitby := select_kwargs.pop("limitby", ()):
2593
2625
 
@@ -2637,12 +2669,12 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2637
2669
  # .on not given, generate it:
2638
2670
  other = other.with_alias(f"{key}_{hash(relation)}")
2639
2671
  condition = typing.cast(Query, relation.condition(model, other))
2672
+ if callable(relation.condition_and):
2673
+ condition &= relation.condition_and(model, other)
2640
2674
  left.append(other.on(condition))
2641
2675
  else:
2642
2676
  # else: inner join (handled earlier)
2643
2677
  other = other.with_alias(f"{key}_{hash(relation)}") # only for replace
2644
- # other = other.with_alias(f"{key}_{hash(relation)}")
2645
- # query &= relation.condition(model, other)
2646
2678
 
2647
2679
  # if no fields of 'other' are included, add other.ALL
2648
2680
  # else: only add other.id if missing
@@ -400,8 +400,16 @@ def test_complex_join():
400
400
 
401
401
 
402
402
  def test_reprs_and_bool():
403
- assert TestQueryTable.where(id=1)
404
- assert not TestQueryTable.where(id=101)
403
+ _setup_data()
404
+
405
+ # NOTE: This logic changed: any 'empty' query table is False, and adding any conditions make it True.
406
+ empty = TestQueryTable.where()
407
+ notempty = TestQueryTable.where(id=1)
408
+ assert (empty or notempty) is notempty
409
+ assert (notempty or empty) is notempty
410
+ assert not empty
411
+ assert notempty
412
+ assert TestQueryTable.where(id=10000000000)
405
413
 
406
414
  assert repr(TestQueryTable.where(id=1))
407
415
  assert str(TestQueryTable.where(id=1))
@@ -295,6 +295,7 @@ def test_typedal_way():
295
295
 
296
296
 
297
297
  def test_reprs():
298
+ _setup_data()
298
299
  assert "Relationship:left on=" in repr(Article.tags)
299
300
 
300
301
  article = Article.first()
@@ -323,6 +324,16 @@ def test_reprs():
323
324
 
324
325
  assert empty.get_table_name() == "new"
325
326
 
327
+ relation = Article.join("author").relationships["author"]
328
+
329
+ assert "AND" not in repr(relation)
330
+
331
+ relation = Article.join("author",
332
+ condition_and=lambda article, author: author.name != "Hank"
333
+ ).relationships["author"]
334
+
335
+ assert "AND" in repr(relation) and "Hank" in repr(relation)
336
+
326
337
 
327
338
  @db.define()
328
339
  class CacheFirst(TypedTable):
@@ -355,6 +366,33 @@ def test_relationship_detection():
355
366
  assert user_table_relationships["extra_roles"].join == "left"
356
367
 
357
368
 
369
+ def test_join_with_different_condition():
370
+ _setup_data()
371
+
372
+ role_with_users = Role.join(
373
+ "users",
374
+ method="inner",
375
+ ).first()
376
+
377
+ assert role_with_users.users
378
+ assert role_with_users.users[0].name == "Reader 1"
379
+
380
+ role_with_users = Role.join(
381
+ "users", method="inner", condition_and=lambda role, user: ~user.name.like("Reader%")
382
+ ).first()
383
+
384
+ assert role_with_users.users
385
+ assert role_with_users.users[0].name != "Reader 1"
386
+
387
+ # left:
388
+ role_with_users = Role.join(
389
+ "users", method="left", condition_and=lambda role, user: ~user.name.like("Reader%")
390
+ ).first()
391
+
392
+ assert role_with_users.users
393
+ assert role_with_users.users[0].name != "Reader 1"
394
+
395
+
358
396
  def test_caching():
359
397
  uncached = User.join().collect_or_fail()
360
398
  cached = User.cache().join().collect_or_fail() # not actually cached yet!
@@ -538,11 +576,11 @@ def test_caching_dependencies():
538
576
 
539
577
  def test_illegal():
540
578
  with pytest.raises(ValueError), pytest.warns(UserWarning):
579
+
541
580
  class HasRelationship:
542
581
  something = relationship("...", condition=lambda: 1, on=lambda: 2)
543
582
 
544
583
 
545
-
546
584
  def test_join_with_select():
547
585
  _setup_data()
548
586
 
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