TypeDAL 3.16.1__tar.gz → 3.16.3__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.3}/CHANGELOG.md +12 -0
  2. {typedal-3.16.1 → typedal-3.16.3}/PKG-INFO +1 -1
  3. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/__about__.py +1 -1
  4. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/core.py +131 -21
  5. {typedal-3.16.1 → typedal-3.16.3}/tests/test_orm.py +0 -29
  6. {typedal-3.16.1 → typedal-3.16.3}/tests/test_py4web.py +1 -1
  7. {typedal-3.16.1 → typedal-3.16.3}/tests/test_row.py +42 -1
  8. {typedal-3.16.1 → typedal-3.16.3}/.github/workflows/su6.yml +0 -0
  9. {typedal-3.16.1 → typedal-3.16.3}/.gitignore +0 -0
  10. {typedal-3.16.1 → typedal-3.16.3}/.readthedocs.yml +0 -0
  11. {typedal-3.16.1 → typedal-3.16.3}/README.md +0 -0
  12. {typedal-3.16.1 → typedal-3.16.3}/coverage.svg +0 -0
  13. {typedal-3.16.1 → typedal-3.16.3}/docs/1_getting_started.md +0 -0
  14. {typedal-3.16.1 → typedal-3.16.3}/docs/2_defining_tables.md +0 -0
  15. {typedal-3.16.1 → typedal-3.16.3}/docs/3_building_queries.md +0 -0
  16. {typedal-3.16.1 → typedal-3.16.3}/docs/4_relationships.md +0 -0
  17. {typedal-3.16.1 → typedal-3.16.3}/docs/5_py4web.md +0 -0
  18. {typedal-3.16.1 → typedal-3.16.3}/docs/6_migrations.md +0 -0
  19. {typedal-3.16.1 → typedal-3.16.3}/docs/7_mixins.md +0 -0
  20. {typedal-3.16.1 → typedal-3.16.3}/docs/css/code_blocks.css +0 -0
  21. {typedal-3.16.1 → typedal-3.16.3}/docs/index.md +0 -0
  22. {typedal-3.16.1 → typedal-3.16.3}/docs/requirements.txt +0 -0
  23. {typedal-3.16.1 → typedal-3.16.3}/example_new.py +0 -0
  24. {typedal-3.16.1 → typedal-3.16.3}/example_old.py +0 -0
  25. {typedal-3.16.1 → typedal-3.16.3}/mkdocs.yml +0 -0
  26. {typedal-3.16.1 → typedal-3.16.3}/pyproject.toml +0 -0
  27. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/__init__.py +0 -0
  28. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/caching.py +0 -0
  29. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/cli.py +0 -0
  30. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/config.py +0 -0
  31. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/fields.py +0 -0
  32. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/for_py4web.py +0 -0
  33. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/for_web2py.py +0 -0
  34. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/helpers.py +0 -0
  35. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/mixins.py +0 -0
  36. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/py.typed +0 -0
  37. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/serializers/as_json.py +0 -0
  38. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/types.py +0 -0
  39. {typedal-3.16.1 → typedal-3.16.3}/src/typedal/web2py_py4web_shared.py +0 -0
  40. {typedal-3.16.1 → typedal-3.16.3}/tests/__init__.py +0 -0
  41. {typedal-3.16.1 → typedal-3.16.3}/tests/configs/simple.toml +0 -0
  42. {typedal-3.16.1 → typedal-3.16.3}/tests/configs/valid.env +0 -0
  43. {typedal-3.16.1 → typedal-3.16.3}/tests/configs/valid.toml +0 -0
  44. {typedal-3.16.1 → typedal-3.16.3}/tests/test_cli.py +0 -0
  45. {typedal-3.16.1 → typedal-3.16.3}/tests/test_config.py +0 -0
  46. {typedal-3.16.1 → typedal-3.16.3}/tests/test_docs_examples.py +0 -0
  47. {typedal-3.16.1 → typedal-3.16.3}/tests/test_helpers.py +0 -0
  48. {typedal-3.16.1 → typedal-3.16.3}/tests/test_json.py +0 -0
  49. {typedal-3.16.1 → typedal-3.16.3}/tests/test_main.py +0 -0
  50. {typedal-3.16.1 → typedal-3.16.3}/tests/test_mixins.py +0 -0
  51. {typedal-3.16.1 → typedal-3.16.3}/tests/test_mypy.py +0 -0
  52. {typedal-3.16.1 → typedal-3.16.3}/tests/test_query_builder.py +0 -0
  53. {typedal-3.16.1 → typedal-3.16.3}/tests/test_relationships.py +0 -0
  54. {typedal-3.16.1 → typedal-3.16.3}/tests/test_stats.py +0 -0
  55. {typedal-3.16.1 → typedal-3.16.3}/tests/test_table.py +0 -0
  56. {typedal-3.16.1 → typedal-3.16.3}/tests/test_web2py.py +0 -0
  57. {typedal-3.16.1 → typedal-3.16.3}/tests/test_xx_others.py +0 -0
  58. {typedal-3.16.1 → typedal-3.16.3}/tests/timings.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v3.16.3 (2025-09-08)
6
+
7
+ ### Fix
8
+
9
+ * Support `.render()` on individual row ([`200a64c`](https://github.com/trialandsuccess/TypeDAL/commit/200a64c0e0c2647c5036dee0476d9ad8ebb416e4))
10
+
11
+ ## v3.16.2 (2025-09-08)
12
+
13
+ ### Fix
14
+
15
+ * Support `.render()` on rows ([`11e115a`](https://github.com/trialandsuccess/TypeDAL/commit/11e115aceaa0207c1b6a969cd3fec95b5e898970))
16
+
5
17
  ## v3.16.1 (2025-09-01)
6
18
 
7
19
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 3.16.1
3
+ Version: 3.16.3
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.3"
@@ -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
 
@@ -2002,6 +2037,41 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
2002
2037
 
2003
2038
  return pydal2sql.generate_sql(cls)
2004
2039
 
2040
+ def render(self, fields=None, compact=False) -> Self:
2041
+ row = copy.deepcopy(self)
2042
+ keys = list(row)
2043
+ if not fields:
2044
+ fields = [self._table[f] for f in self._table._fields]
2045
+ fields = [f for f in fields if isinstance(f, Field) and f.represent]
2046
+
2047
+ for field in fields:
2048
+ if field._table == self._table:
2049
+ row[field.name] = self._db.represent(
2050
+ "rows_render",
2051
+ field,
2052
+ row[field.name],
2053
+ row,
2054
+ )
2055
+ # else: relationship, different logic:
2056
+
2057
+ for relation_name in row._with:
2058
+ if relation := self._relationships.get(relation_name):
2059
+ relation_table = relation.table
2060
+
2061
+ relation_row = row[relation_name]
2062
+ for fieldname in relation_row:
2063
+ field = relation_table[fieldname]
2064
+ row[relation_name][fieldname] = self._db.represent(
2065
+ "rows_render",
2066
+ field,
2067
+ relation_row[field.name],
2068
+ relation_row,
2069
+ )
2070
+
2071
+ if compact and len(keys) == 1 and keys[0] != "_extra": # pragma: no cover
2072
+ return row[keys[0]]
2073
+ return row
2074
+
2005
2075
 
2006
2076
  # backwards compat:
2007
2077
  TypedRow = TypedTable
@@ -2104,7 +2174,9 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
2104
2174
  return self[max_id]
2105
2175
 
2106
2176
  def find(
2107
- self, f: typing.Callable[[T_MetaInstance], Query], limitby: tuple[int, int] = None
2177
+ self,
2178
+ f: typing.Callable[[T_MetaInstance], Query],
2179
+ limitby: tuple[int, int] = None,
2108
2180
  ) -> "TypedRows[T_MetaInstance]":
2109
2181
  """
2110
2182
  Returns a new Rows object, a subset of the original object, filtered by the function `f`.
@@ -2176,7 +2248,10 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
2176
2248
  return mktable(data, headers)
2177
2249
 
2178
2250
  def group_by_value(
2179
- self, *fields: "str | Field | TypedField[T]", one_result: bool = False, **kwargs: Any
2251
+ self,
2252
+ *fields: "str | Field | TypedField[T]",
2253
+ one_result: bool = False,
2254
+ **kwargs: Any,
2180
2255
  ) -> dict[T, list[T_MetaInstance]]:
2181
2256
  """
2182
2257
  Group the rows by a specific field (which will be the dict key).
@@ -2342,7 +2417,10 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
2342
2417
 
2343
2418
  @classmethod
2344
2419
  def from_rows(
2345
- cls, rows: Rows, model: Type[T_MetaInstance], metadata: Metadata = None
2420
+ cls,
2421
+ rows: Rows,
2422
+ model: Type[T_MetaInstance],
2423
+ metadata: Metadata = None,
2346
2424
  ) -> "TypedRows[T_MetaInstance]":
2347
2425
  """
2348
2426
  Internal method to convert a Rows object to a TypedRows.
@@ -2368,6 +2446,29 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
2368
2446
  self.__dict__.update(state)
2369
2447
  # db etc. set after undill by caching.py
2370
2448
 
2449
+ def render(self, i=None, fields=None) -> typing.Generator[T_MetaInstance, None, None]:
2450
+ """
2451
+ Takes an index and returns a copy of the indexed row with values
2452
+ transformed via the "represent" attributes of the associated fields.
2453
+
2454
+ Args:
2455
+ i: index. If not specified, a generator is returned for iteration
2456
+ over all the rows.
2457
+ fields: a list of fields to transform (if None, all fields with
2458
+ "represent" attributes will be transformed)
2459
+ """
2460
+ if i is None:
2461
+ # difference: uses .keys() instead of index
2462
+ return (self.render(i, fields=fields) for i in self.records.keys())
2463
+
2464
+ if not self.db.has_representer("rows_render"): # pragma: no cover
2465
+ raise RuntimeError(
2466
+ "Rows.render() needs a `rows_render` representer in DAL instance",
2467
+ )
2468
+
2469
+ row = self.records[i]
2470
+ return row.render(fields, compact=self.compact)
2471
+
2371
2472
 
2372
2473
  from .caching import ( # noqa: E402
2373
2474
  _remove_cache,
@@ -2472,7 +2573,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2472
2573
  self.select_kwargs,
2473
2574
  self.relationships,
2474
2575
  self.metadata,
2475
- ]
2576
+ ],
2476
2577
  )
2477
2578
 
2478
2579
  def _extend(
@@ -2631,7 +2732,10 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2631
2732
  return self._extend(relationships=relationships)
2632
2733
 
2633
2734
  def cache(
2634
- self, *deps: Any, expires_at: Optional[dt.datetime] = None, ttl: Optional[int | dt.timedelta] = None
2735
+ self,
2736
+ *deps: Any,
2737
+ expires_at: Optional[dt.datetime] = None,
2738
+ ttl: Optional[int | dt.timedelta] = None,
2635
2739
  ) -> "QueryBuilder[T_MetaInstance]":
2636
2740
  """
2637
2741
  Enable caching for this query to load repeated calls from a dill row \
@@ -2772,7 +2876,10 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2772
2876
  return db(query).select(*select_args, **select_kwargs)
2773
2877
 
2774
2878
  def collect(
2775
- self, verbose: bool = False, _to: Type["TypedRows[Any]"] = None, add_id: bool = True
2879
+ self,
2880
+ verbose: bool = False,
2881
+ _to: Type["TypedRows[Any]"] = None,
2882
+ add_id: bool = True,
2776
2883
  ) -> "TypedRows[T_MetaInstance]":
2777
2884
  """
2778
2885
  Execute the built query and turn it into model instances, while handling relationships.
@@ -2938,7 +3045,10 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2938
3045
  return query, select_args
2939
3046
 
2940
3047
  def _collect_with_relationships(
2941
- self, rows: Rows, metadata: Metadata, _to: Type["TypedRows[Any]"]
3048
+ self,
3049
+ rows: Rows,
3050
+ metadata: Metadata,
3051
+ _to: Type["TypedRows[Any]"],
2942
3052
  ) -> "TypedRows[T_MetaInstance]":
2943
3053
  """
2944
3054
  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,44 @@ 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
+ first = rows.first()
249
+
250
+ assert first.related
251
+
252
+ iterator = rows.render()
253
+ rendered_one = next(iterator)
254
+
255
+ assert rendered_one.normal == "123"
256
+ assert rendered_one.list_field == "abc, def"
257
+ assert rendered_one.related.also_normal == "321"
258
+
259
+ # .render() on one row:
260
+ rendered_two = first.render()
261
+ assert rendered_two.normal == "123"
262
+ assert rendered_two.list_field == "abc, def"
263
+ assert rendered_two.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