TypeDAL 3.16.1__tar.gz → 3.16.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.

Potentially problematic release.


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

Files changed (58) hide show
  1. {typedal-3.16.1 → typedal-3.16.2}/CHANGELOG.md +6 -0
  2. {typedal-3.16.1 → typedal-3.16.2}/PKG-INFO +1 -1
  3. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/__about__.py +1 -1
  4. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/core.py +126 -21
  5. {typedal-3.16.1 → typedal-3.16.2}/tests/test_orm.py +0 -29
  6. {typedal-3.16.1 → typedal-3.16.2}/tests/test_py4web.py +1 -1
  7. {typedal-3.16.1 → typedal-3.16.2}/tests/test_row.py +34 -1
  8. {typedal-3.16.1 → typedal-3.16.2}/.github/workflows/su6.yml +0 -0
  9. {typedal-3.16.1 → typedal-3.16.2}/.gitignore +0 -0
  10. {typedal-3.16.1 → typedal-3.16.2}/.readthedocs.yml +0 -0
  11. {typedal-3.16.1 → typedal-3.16.2}/README.md +0 -0
  12. {typedal-3.16.1 → typedal-3.16.2}/coverage.svg +0 -0
  13. {typedal-3.16.1 → typedal-3.16.2}/docs/1_getting_started.md +0 -0
  14. {typedal-3.16.1 → typedal-3.16.2}/docs/2_defining_tables.md +0 -0
  15. {typedal-3.16.1 → typedal-3.16.2}/docs/3_building_queries.md +0 -0
  16. {typedal-3.16.1 → typedal-3.16.2}/docs/4_relationships.md +0 -0
  17. {typedal-3.16.1 → typedal-3.16.2}/docs/5_py4web.md +0 -0
  18. {typedal-3.16.1 → typedal-3.16.2}/docs/6_migrations.md +0 -0
  19. {typedal-3.16.1 → typedal-3.16.2}/docs/7_mixins.md +0 -0
  20. {typedal-3.16.1 → typedal-3.16.2}/docs/css/code_blocks.css +0 -0
  21. {typedal-3.16.1 → typedal-3.16.2}/docs/index.md +0 -0
  22. {typedal-3.16.1 → typedal-3.16.2}/docs/requirements.txt +0 -0
  23. {typedal-3.16.1 → typedal-3.16.2}/example_new.py +0 -0
  24. {typedal-3.16.1 → typedal-3.16.2}/example_old.py +0 -0
  25. {typedal-3.16.1 → typedal-3.16.2}/mkdocs.yml +0 -0
  26. {typedal-3.16.1 → typedal-3.16.2}/pyproject.toml +0 -0
  27. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/__init__.py +0 -0
  28. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/caching.py +0 -0
  29. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/cli.py +0 -0
  30. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/config.py +0 -0
  31. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/fields.py +0 -0
  32. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/for_py4web.py +0 -0
  33. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/for_web2py.py +0 -0
  34. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/helpers.py +0 -0
  35. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/mixins.py +0 -0
  36. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/py.typed +0 -0
  37. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/serializers/as_json.py +0 -0
  38. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/types.py +0 -0
  39. {typedal-3.16.1 → typedal-3.16.2}/src/typedal/web2py_py4web_shared.py +0 -0
  40. {typedal-3.16.1 → typedal-3.16.2}/tests/__init__.py +0 -0
  41. {typedal-3.16.1 → typedal-3.16.2}/tests/configs/simple.toml +0 -0
  42. {typedal-3.16.1 → typedal-3.16.2}/tests/configs/valid.env +0 -0
  43. {typedal-3.16.1 → typedal-3.16.2}/tests/configs/valid.toml +0 -0
  44. {typedal-3.16.1 → typedal-3.16.2}/tests/test_cli.py +0 -0
  45. {typedal-3.16.1 → typedal-3.16.2}/tests/test_config.py +0 -0
  46. {typedal-3.16.1 → typedal-3.16.2}/tests/test_docs_examples.py +0 -0
  47. {typedal-3.16.1 → typedal-3.16.2}/tests/test_helpers.py +0 -0
  48. {typedal-3.16.1 → typedal-3.16.2}/tests/test_json.py +0 -0
  49. {typedal-3.16.1 → typedal-3.16.2}/tests/test_main.py +0 -0
  50. {typedal-3.16.1 → typedal-3.16.2}/tests/test_mixins.py +0 -0
  51. {typedal-3.16.1 → typedal-3.16.2}/tests/test_mypy.py +0 -0
  52. {typedal-3.16.1 → typedal-3.16.2}/tests/test_query_builder.py +0 -0
  53. {typedal-3.16.1 → typedal-3.16.2}/tests/test_relationships.py +0 -0
  54. {typedal-3.16.1 → typedal-3.16.2}/tests/test_stats.py +0 -0
  55. {typedal-3.16.1 → typedal-3.16.2}/tests/test_table.py +0 -0
  56. {typedal-3.16.1 → typedal-3.16.2}/tests/test_web2py.py +0 -0
  57. {typedal-3.16.1 → typedal-3.16.2}/tests/test_xx_others.py +0 -0
  58. {typedal-3.16.1 → typedal-3.16.2}/tests/timings.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v3.16.2 (2025-09-08)
6
+
7
+ ### Fix
8
+
9
+ * Support `.render()` on rows ([`11e115a`](https://github.com/trialandsuccess/TypeDAL/commit/11e115aceaa0207c1b6a969cd3fec95b5e898970))
10
+
5
11
  ## v3.16.1 (2025-09-01)
6
12
 
7
13
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 3.16.1
3
+ Version: 3.16.2
4
4
  Summary: Typing support for PyDAL
5
5
  Project-URL: Documentation, https://typedal.readthedocs.io/
6
6
  Project-URL: Issues, https://github.com/trialandsuccess/TypeDAL/issues
@@ -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.16.1"
8
+ __version__ = "3.16.2"
@@ -5,6 +5,7 @@ Core functionality of TypeDAL.
5
5
  from __future__ import annotations
6
6
 
7
7
  import contextlib
8
+ import copy
8
9
  import csv
9
10
  import datetime as dt
10
11
  import functools
@@ -18,7 +19,6 @@ import typing
18
19
  import uuid
19
20
  import warnings
20
21
  from collections import defaultdict
21
- from copy import copy
22
22
  from decimal import Decimal
23
23
  from pathlib import Path
24
24
  from typing import Any, Optional, Type
@@ -246,7 +246,8 @@ class Relationship(typing.Generic[To_Type]):
246
246
  return self
247
247
 
248
248
  warnings.warn(
249
- "Trying to get data from a relationship object! Did you forget to join it?", category=RuntimeWarning
249
+ "Trying to get data from a relationship object! Did you forget to join it?",
250
+ category=RuntimeWarning,
250
251
  )
251
252
  if self.multiple:
252
253
  return []
@@ -255,7 +256,10 @@ class Relationship(typing.Generic[To_Type]):
255
256
 
256
257
 
257
258
  def relationship(
258
- _type: typing.Type[To_Type], condition: Condition = None, join: JOIN_OPTIONS = None, on: OnQuery = None
259
+ _type: typing.Type[To_Type],
260
+ condition: Condition = None,
261
+ join: JOIN_OPTIONS = None,
262
+ on: OnQuery = None,
259
263
  ) -> To_Type:
260
264
  """
261
265
  Define a relationship to another table, when its id is not stored in the current table.
@@ -546,7 +550,7 @@ class TypeDAL(pydal.DAL): # type: ignore
546
550
 
547
551
  for key, field in typedfields.items():
548
552
  # clone every property so it can be re-used across mixins:
549
- clone = copy(field)
553
+ clone = copy.copy(field)
550
554
  setattr(cls, key, clone)
551
555
  typedfields[key] = clone
552
556
 
@@ -737,7 +741,9 @@ class TypeDAL(pydal.DAL): # type: ignore
737
741
 
738
742
  @classmethod
739
743
  def _annotation_to_pydal_fieldtype(
740
- cls, _ftype: T_annotation, mut_kw: typing.MutableMapping[str, Any]
744
+ cls,
745
+ _ftype: T_annotation,
746
+ mut_kw: typing.MutableMapping[str, Any],
741
747
  ) -> Optional[str]:
742
748
  # ftype can be a union or type. typing.cast is sometimes used to tell mypy when it's not a union.
743
749
  ftype = typing.cast(type, _ftype) # cast from Type to type to make mypy happy)
@@ -824,12 +830,26 @@ class TypeDAL(pydal.DAL): # type: ignore
824
830
  return to_snake(camel)
825
831
 
826
832
 
833
+ def default_representer(field: TypedField[T], value: T, table: Type[TypedTable]) -> str:
834
+ """
835
+ Simply call field.represent on the value.
836
+ """
837
+ if represent := getattr(field, "represent", None):
838
+ return field.represent(value, table)
839
+ else:
840
+ return repr(value)
841
+
842
+
843
+ TypeDAL.representers.setdefault("rows_render", default_representer)
844
+
827
845
  P = typing.ParamSpec("P")
828
846
  R = typing.TypeVar("R")
829
847
 
830
848
 
831
849
  def reorder_fields(
832
- table: pydal.objects.Table, fields: typing.Iterable[str | Field | TypedField], keep_others: bool = True
850
+ table: pydal.objects.Table,
851
+ fields: typing.Iterable[str | Field | TypedField],
852
+ keep_others: bool = True,
833
853
  ) -> None:
834
854
  """
835
855
  Reorder fields of a pydal table.
@@ -987,7 +1007,9 @@ class TableMeta(type):
987
1007
  return self.where(lambda row: row.id.belongs(result)).collect()
988
1008
 
989
1009
  def update_or_insert(
990
- self: Type[T_MetaInstance], query: T_Query | AnyDict = DEFAULT, **values: Any
1010
+ self: Type[T_MetaInstance],
1011
+ query: T_Query | AnyDict = DEFAULT,
1012
+ **values: Any,
991
1013
  ) -> T_MetaInstance:
992
1014
  """
993
1015
  Update a row if query matches, else insert a new one.
@@ -1010,7 +1032,8 @@ class TableMeta(type):
1010
1032
  return self(record)
1011
1033
 
1012
1034
  def validate_and_insert(
1013
- self: Type[T_MetaInstance], **fields: Any
1035
+ self: Type[T_MetaInstance],
1036
+ **fields: Any,
1014
1037
  ) -> tuple[Optional[T_MetaInstance], Optional[dict[str, str]]]:
1015
1038
  """
1016
1039
  Validate input data and then insert a row.
@@ -1025,7 +1048,9 @@ class TableMeta(type):
1025
1048
  return None, result.get("errors")
1026
1049
 
1027
1050
  def validate_and_update(
1028
- self: Type[T_MetaInstance], query: Query, **fields: Any
1051
+ self: Type[T_MetaInstance],
1052
+ query: Query,
1053
+ **fields: Any,
1029
1054
  ) -> tuple[Optional[T_MetaInstance], Optional[dict[str, str]]]:
1030
1055
  """
1031
1056
  Validate input data and then update max 1 row.
@@ -1045,7 +1070,9 @@ class TableMeta(type):
1045
1070
  return None, None
1046
1071
 
1047
1072
  def validate_and_update_or_insert(
1048
- self: Type[T_MetaInstance], query: Query, **fields: Any
1073
+ self: Type[T_MetaInstance],
1074
+ query: Query,
1075
+ **fields: Any,
1049
1076
  ) -> tuple[Optional[T_MetaInstance], Optional[dict[str, str]]]:
1050
1077
  """
1051
1078
  Validate input data and then update_and_insert (on max 1 row).
@@ -1262,7 +1289,9 @@ class TableMeta(type):
1262
1289
 
1263
1290
  # hooks:
1264
1291
  def _hook_once(
1265
- cls: Type[T_MetaInstance], hooks: list[typing.Callable[P, R]], fn: typing.Callable[P, R]
1292
+ cls: Type[T_MetaInstance],
1293
+ hooks: list[typing.Callable[P, R]],
1294
+ fn: typing.Callable[P, R],
1266
1295
  ) -> Type[T_MetaInstance]:
1267
1296
  @functools.wraps(fn)
1268
1297
  def wraps(*a: P.args, **kw: P.kwargs) -> R:
@@ -1456,7 +1485,9 @@ class TypedField(Expression, typing.Generic[T_Value]): # pragma: no cover
1456
1485
  """
1457
1486
 
1458
1487
  def __get__(
1459
- self, instance: T_MetaInstance | None, owner: Type[T_MetaInstance]
1488
+ self,
1489
+ instance: T_MetaInstance | None,
1490
+ owner: Type[T_MetaInstance],
1460
1491
  ) -> typing.Union[T_Value, "TypedField[T_Value]"]:
1461
1492
  """
1462
1493
  Since this class is a Descriptor field, \
@@ -1665,7 +1696,9 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1665
1696
  self.update_record = self._update_record # type: ignore
1666
1697
 
1667
1698
  def __new__(
1668
- cls, row_or_id: typing.Union[Row, Query, pydal.objects.Set, int, str, None, "TypedTable"] = None, **filters: Any
1699
+ cls,
1700
+ row_or_id: typing.Union[Row, Query, pydal.objects.Set, int, str, None, "TypedTable"] = None,
1701
+ **filters: Any,
1669
1702
  ) -> Self:
1670
1703
  """
1671
1704
  Create a Typed Rows model instance from an existing row, ID or query.
@@ -1848,7 +1881,9 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1848
1881
  return typing.cast(str, table.as_yaml(sanitize))
1849
1882
 
1850
1883
  def _as_dict(
1851
- self, datetime_to_str: bool = False, custom_types: typing.Iterable[type] | type | None = None
1884
+ self,
1885
+ datetime_to_str: bool = False,
1886
+ custom_types: typing.Iterable[type] | type | None = None,
1852
1887
  ) -> AnyDict:
1853
1888
  row = self._ensure_matching_row()
1854
1889
 
@@ -2104,7 +2139,9 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
2104
2139
  return self[max_id]
2105
2140
 
2106
2141
  def find(
2107
- self, f: typing.Callable[[T_MetaInstance], Query], limitby: tuple[int, int] = None
2142
+ self,
2143
+ f: typing.Callable[[T_MetaInstance], Query],
2144
+ limitby: tuple[int, int] = None,
2108
2145
  ) -> "TypedRows[T_MetaInstance]":
2109
2146
  """
2110
2147
  Returns a new Rows object, a subset of the original object, filtered by the function `f`.
@@ -2176,7 +2213,10 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
2176
2213
  return mktable(data, headers)
2177
2214
 
2178
2215
  def group_by_value(
2179
- self, *fields: "str | Field | TypedField[T]", one_result: bool = False, **kwargs: Any
2216
+ self,
2217
+ *fields: "str | Field | TypedField[T]",
2218
+ one_result: bool = False,
2219
+ **kwargs: Any,
2180
2220
  ) -> dict[T, list[T_MetaInstance]]:
2181
2221
  """
2182
2222
  Group the rows by a specific field (which will be the dict key).
@@ -2342,7 +2382,10 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
2342
2382
 
2343
2383
  @classmethod
2344
2384
  def from_rows(
2345
- cls, rows: Rows, model: Type[T_MetaInstance], metadata: Metadata = None
2385
+ cls,
2386
+ rows: Rows,
2387
+ model: Type[T_MetaInstance],
2388
+ metadata: Metadata = None,
2346
2389
  ) -> "TypedRows[T_MetaInstance]":
2347
2390
  """
2348
2391
  Internal method to convert a Rows object to a TypedRows.
@@ -2368,6 +2411,59 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
2368
2411
  self.__dict__.update(state)
2369
2412
  # db etc. set after undill by caching.py
2370
2413
 
2414
+ def render(self, i=None, fields=None) -> typing.Generator[T_MetaInstance, None, None]:
2415
+ """
2416
+ Takes an index and returns a copy of the indexed row with values
2417
+ transformed via the "represent" attributes of the associated fields.
2418
+
2419
+ Args:
2420
+ i: index. If not specified, a generator is returned for iteration
2421
+ over all the rows.
2422
+ fields: a list of fields to transform (if None, all fields with
2423
+ "represent" attributes will be transformed)
2424
+ """
2425
+ if i is None:
2426
+ # difference: uses .keys() instead of index
2427
+ return (self.render(i, fields=fields) for i in self.records.keys())
2428
+
2429
+ if not self.db.has_representer("rows_render"): # pragma: no cover
2430
+ raise RuntimeError(
2431
+ "Rows.render() needs a `rows_render` representer in DAL instance",
2432
+ )
2433
+
2434
+ row = copy.deepcopy(self.records[i])
2435
+ keys = list(row)
2436
+ if not fields:
2437
+ fields = [f for f in self.fields if isinstance(f, Field) and f.represent]
2438
+
2439
+ for field in fields:
2440
+ if field._table == self.model._table:
2441
+ row[field.name] = self.db.represent(
2442
+ "rows_render",
2443
+ field,
2444
+ row[field.name],
2445
+ row,
2446
+ )
2447
+ # else: relationship, different logic:
2448
+
2449
+ for relation_name in row._with:
2450
+ if relation := self.model._relationships.get(relation_name):
2451
+ relation_table = relation.table
2452
+
2453
+ relation_row = row[relation_name]
2454
+ for fieldname in relation_row:
2455
+ field = relation_table[fieldname]
2456
+ row[relation_name][fieldname] = self.db.represent(
2457
+ "rows_render",
2458
+ field,
2459
+ relation_row[field.name],
2460
+ relation_row,
2461
+ )
2462
+
2463
+ if self.compact and len(keys) == 1 and keys[0] != "_extra": # pragma: no cover
2464
+ return row[keys[0]]
2465
+ return row
2466
+
2371
2467
 
2372
2468
  from .caching import ( # noqa: E402
2373
2469
  _remove_cache,
@@ -2472,7 +2568,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2472
2568
  self.select_kwargs,
2473
2569
  self.relationships,
2474
2570
  self.metadata,
2475
- ]
2571
+ ],
2476
2572
  )
2477
2573
 
2478
2574
  def _extend(
@@ -2631,7 +2727,10 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2631
2727
  return self._extend(relationships=relationships)
2632
2728
 
2633
2729
  def cache(
2634
- self, *deps: Any, expires_at: Optional[dt.datetime] = None, ttl: Optional[int | dt.timedelta] = None
2730
+ self,
2731
+ *deps: Any,
2732
+ expires_at: Optional[dt.datetime] = None,
2733
+ ttl: Optional[int | dt.timedelta] = None,
2635
2734
  ) -> "QueryBuilder[T_MetaInstance]":
2636
2735
  """
2637
2736
  Enable caching for this query to load repeated calls from a dill row \
@@ -2772,7 +2871,10 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2772
2871
  return db(query).select(*select_args, **select_kwargs)
2773
2872
 
2774
2873
  def collect(
2775
- self, verbose: bool = False, _to: Type["TypedRows[Any]"] = None, add_id: bool = True
2874
+ self,
2875
+ verbose: bool = False,
2876
+ _to: Type["TypedRows[Any]"] = None,
2877
+ add_id: bool = True,
2776
2878
  ) -> "TypedRows[T_MetaInstance]":
2777
2879
  """
2778
2880
  Execute the built query and turn it into model instances, while handling relationships.
@@ -2938,7 +3040,10 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2938
3040
  return query, select_args
2939
3041
 
2940
3042
  def _collect_with_relationships(
2941
- self, rows: Rows, metadata: Metadata, _to: Type["TypedRows[Any]"]
3043
+ self,
3044
+ rows: Rows,
3045
+ metadata: Metadata,
3046
+ _to: Type["TypedRows[Any]"],
2942
3047
  ) -> "TypedRows[T_MetaInstance]":
2943
3048
  """
2944
3049
  Transform the raw rows into Typed Table model instances.
@@ -34,35 +34,6 @@ T_Table = typing.TypeVar("T_Table", bound=TypedTable)
34
34
 
35
35
  TypeTable = typing.Type[T_Table]
36
36
 
37
- # class TypeDAL:
38
- # tables: list[typing.Type[TypedTable]]
39
- #
40
- # def __init__(self, conn):
41
- # self.tables = []
42
- #
43
- # @typing.overload
44
- # def define(self, table: TypeTable) -> TypeTable:
45
- # ...
46
- #
47
- # @typing.overload
48
- # def define(self, table: None = None) -> typing.Callable[[TypeTable], TypeTable]:
49
- # ...
50
- #
51
- # def define(
52
- # self, table: typing.Optional[TypeTable] = None
53
- # ) -> TypeTable | typing.Callable[[TypeTable], TypeTable]:
54
- # if table: # and issubclass(table, Table)
55
- # self.tables.append(table)
56
- # table._db = self
57
- # return table
58
- # else:
59
- # # called with ()
60
- # def wrapper(table: TypeTable) -> TypeTable:
61
- # return self.define(table)
62
- #
63
- # return wrapper
64
-
65
-
66
37
  T_Value = typing.TypeVar("T_Value") # actual type of the Field (via Generic)
67
38
 
68
39
  # T_Table = typing.TypeVar("T_Table") # typevar used by __get__
@@ -42,7 +42,7 @@ def test_auth_user():
42
42
  requirements = AuthUser.email.requires
43
43
 
44
44
  assert requirements
45
- assert isinstance(requirements, tuple)
45
+ assert isinstance(requirements, list)
46
46
  assert len(requirements) == 2
47
47
 
48
48
  assert isinstance(requirements[0], IS_EMAIL)
@@ -9,7 +9,7 @@ import pydal
9
9
  import pytest
10
10
  from pydal.objects import Rows
11
11
 
12
- from src.typedal import TypeDAL, TypedField, TypedTable
12
+ from src.typedal import TypeDAL, TypedField, TypedTable, relationship
13
13
  from src.typedal.fields import IntegerField, ReferenceField
14
14
 
15
15
  db = TypeDAL("sqlite:memory")
@@ -220,3 +220,36 @@ def test_rows():
220
220
  empty_rows = NewStyleClass.where(NewStyleClass.id < 0).collect()
221
221
  assert str(empty_rows)
222
222
  assert repr(empty_rows)
223
+
224
+
225
+ def test_render():
226
+ def comma_separated(lst: list[str], _):
227
+ return ", ".join(lst) if lst else ""
228
+
229
+ @db.define()
230
+ class RelatedTable(TypedTable):
231
+ also_normal = TypedField(str, represent=lambda value, _: value[::-1])
232
+
233
+ @db.define()
234
+ class RenderTable(TypedTable):
235
+ normal = TypedField(str)
236
+ list_field = TypedField(list[str], represent=comma_separated)
237
+
238
+ related = relationship(
239
+ RelatedTable,
240
+ condition=lambda this, that: this.normal == that.also_normal,
241
+ )
242
+
243
+ RelatedTable.insert(also_normal="123")
244
+ RenderTable.insert(normal="123", list_field=["abc", "def"])
245
+
246
+ rows = RenderTable.select().join("related").collect()
247
+
248
+ assert rows.first().related
249
+
250
+ iterator = rows.render()
251
+ first = next(iterator)
252
+
253
+ assert first.normal == "123"
254
+ assert first.list_field == "abc, def"
255
+ assert first.related.also_normal == "321"
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