TypeDAL 4.6.1__tar.gz → 4.6.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.
Files changed (74) hide show
  1. {typedal-4.6.1 → typedal-4.6.3}/CHANGELOG.md +12 -0
  2. {typedal-4.6.1 → typedal-4.6.3}/PKG-INFO +1 -1
  3. {typedal-4.6.1 → typedal-4.6.3}/pyproject.toml +8 -1
  4. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/__about__.py +1 -1
  5. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/cli.py +1 -2
  6. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/core.py +4 -1
  7. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/fields.py +1 -3
  8. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/mixins.py +25 -10
  9. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/rows.py +2 -2
  10. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/tables.py +1 -0
  11. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/types.py +3 -1
  12. {typedal-4.6.1 → typedal-4.6.3}/tests/test_mixins.py +46 -0
  13. {typedal-4.6.1 → typedal-4.6.3}/tests/test_row.py +20 -0
  14. {typedal-4.6.1 → typedal-4.6.3}/.crush/.gitignore +0 -0
  15. {typedal-4.6.1 → typedal-4.6.3}/.crush/crush.db-shm +0 -0
  16. {typedal-4.6.1 → typedal-4.6.3}/.crush/crush.db-wal +0 -0
  17. {typedal-4.6.1 → typedal-4.6.3}/.crush/init +0 -0
  18. {typedal-4.6.1 → typedal-4.6.3}/.crush/logs/crush.log +0 -0
  19. {typedal-4.6.1 → typedal-4.6.3}/.github/workflows/su6.yml +0 -0
  20. {typedal-4.6.1 → typedal-4.6.3}/.gitignore +0 -0
  21. {typedal-4.6.1 → typedal-4.6.3}/.readthedocs.yml +0 -0
  22. {typedal-4.6.1 → typedal-4.6.3}/README.md +0 -0
  23. {typedal-4.6.1 → typedal-4.6.3}/coverage.svg +0 -0
  24. {typedal-4.6.1 → typedal-4.6.3}/docs/10_advanced_apis.md +0 -0
  25. {typedal-4.6.1 → typedal-4.6.3}/docs/1_getting_started.md +0 -0
  26. {typedal-4.6.1 → typedal-4.6.3}/docs/2_defining_tables.md +0 -0
  27. {typedal-4.6.1 → typedal-4.6.3}/docs/3_building_queries.md +0 -0
  28. {typedal-4.6.1 → typedal-4.6.3}/docs/4_relationships.md +0 -0
  29. {typedal-4.6.1 → typedal-4.6.3}/docs/5_py4web.md +0 -0
  30. {typedal-4.6.1 → typedal-4.6.3}/docs/6_migrations.md +0 -0
  31. {typedal-4.6.1 → typedal-4.6.3}/docs/7_configuration.md +0 -0
  32. {typedal-4.6.1 → typedal-4.6.3}/docs/8_mixins.md +0 -0
  33. {typedal-4.6.1 → typedal-4.6.3}/docs/9_memoization.md +0 -0
  34. {typedal-4.6.1 → typedal-4.6.3}/docs/css/code_blocks.css +0 -0
  35. {typedal-4.6.1 → typedal-4.6.3}/docs/index.md +0 -0
  36. {typedal-4.6.1 → typedal-4.6.3}/docs/requirements.txt +0 -0
  37. {typedal-4.6.1 → typedal-4.6.3}/example_new.py +0 -0
  38. {typedal-4.6.1 → typedal-4.6.3}/example_old.py +0 -0
  39. {typedal-4.6.1 → typedal-4.6.3}/mkdocs.yml +0 -0
  40. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/__init__.py +0 -0
  41. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/caching.py +0 -0
  42. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/config.py +0 -0
  43. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/constants.py +0 -0
  44. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/define.py +0 -0
  45. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/for_py4web.py +0 -0
  46. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/for_web2py.py +0 -0
  47. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/helpers.py +0 -0
  48. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/py.typed +0 -0
  49. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/query_builder.py +0 -0
  50. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/relationships.py +0 -0
  51. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/serializers/as_json.py +0 -0
  52. {typedal-4.6.1 → typedal-4.6.3}/src/typedal/web2py_py4web_shared.py +0 -0
  53. {typedal-4.6.1 → typedal-4.6.3}/tasks.py +0 -0
  54. {typedal-4.6.1 → typedal-4.6.3}/tests/__init__.py +0 -0
  55. {typedal-4.6.1 → typedal-4.6.3}/tests/configs/simple.toml +0 -0
  56. {typedal-4.6.1 → typedal-4.6.3}/tests/configs/valid.env +0 -0
  57. {typedal-4.6.1 → typedal-4.6.3}/tests/configs/valid.toml +0 -0
  58. {typedal-4.6.1 → typedal-4.6.3}/tests/py314_tests.py +0 -0
  59. {typedal-4.6.1 → typedal-4.6.3}/tests/test_cli.py +0 -0
  60. {typedal-4.6.1 → typedal-4.6.3}/tests/test_config.py +0 -0
  61. {typedal-4.6.1 → typedal-4.6.3}/tests/test_docs_examples.py +0 -0
  62. {typedal-4.6.1 → typedal-4.6.3}/tests/test_helpers.py +0 -0
  63. {typedal-4.6.1 → typedal-4.6.3}/tests/test_json.py +0 -0
  64. {typedal-4.6.1 → typedal-4.6.3}/tests/test_main.py +0 -0
  65. {typedal-4.6.1 → typedal-4.6.3}/tests/test_mypy.py +0 -0
  66. {typedal-4.6.1 → typedal-4.6.3}/tests/test_orm.py +0 -0
  67. {typedal-4.6.1 → typedal-4.6.3}/tests/test_py4web.py +0 -0
  68. {typedal-4.6.1 → typedal-4.6.3}/tests/test_query_builder.py +0 -0
  69. {typedal-4.6.1 → typedal-4.6.3}/tests/test_relationships.py +0 -0
  70. {typedal-4.6.1 → typedal-4.6.3}/tests/test_stats.py +0 -0
  71. {typedal-4.6.1 → typedal-4.6.3}/tests/test_table.py +0 -0
  72. {typedal-4.6.1 → typedal-4.6.3}/tests/test_web2py.py +0 -0
  73. {typedal-4.6.1 → typedal-4.6.3}/tests/test_xx_others.py +0 -0
  74. {typedal-4.6.1 → typedal-4.6.3}/tests/timings.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.6.3 (2026-03-19)
6
+
7
+ ### Fix
8
+
9
+ * `as_dict()` and similar changed behavior after doing `.render()` ([`8241503`](https://github.com/trialandsuccess/TypeDAL/commit/8241503073c5a9fd76cc898048f6c7796de3ce7a))
10
+
11
+ ## v4.6.2 (2026-03-13)
12
+
13
+ ### Fix
14
+
15
+ * Move pydantic visibility/lazy-load filtering into schema-converter path used by FastAPI ([`63c2e2c`](https://github.com/trialandsuccess/TypeDAL/commit/63c2e2c2e3c78f8aa817911dd269f650f5ba3d7d))
16
+
5
17
  ## v4.6.1 (2026-03-13)
6
18
 
7
19
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.6.1
3
+ Version: 4.6.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
@@ -183,6 +183,7 @@ select = [
183
183
  # "COM", # comma's - NO: annoying
184
184
  # "PTH", # use pathlib - NO: annoying
185
185
  "RUF", # ruff rules
186
+ # "D", # docs
186
187
  ]
187
188
  unfixable = [
188
189
  # Don't touch unused imports
@@ -191,13 +192,19 @@ unfixable = [
191
192
  extend-ignore = [
192
193
  # db.field == None should NOT be fixed to db.field is None
193
194
  "E711",
195
+ "D200",
196
+ "D212",
197
+ "D418",
194
198
  ]
195
199
 
196
-
197
200
  ignore = [
198
201
  "RUF013" # implicit optional
199
202
  ]
200
203
 
204
+
205
+ [tool.ruff.lint.pydocstyle]
206
+ convention = "google"
207
+
201
208
  [tool.bandit]
202
209
  # bandit -c pyproject.toml -r .
203
210
  exclude_dirs = [".bak", "venv"]
@@ -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__ = "4.6.1"
8
+ __version__ = "4.6.3"
@@ -390,8 +390,7 @@ def fake_migrations(
390
390
 
391
391
  previously_migrated = (
392
392
  db(
393
- db.ewh_implemented_features.name.belongs(to_fake)
394
- & (db.ewh_implemented_features.installed == True) # noqa E712
393
+ db.ewh_implemented_features.name.belongs(to_fake) & (db.ewh_implemented_features.installed == True) # noqa E712
395
394
  )
396
395
  .select(db.ewh_implemented_features.name)
397
396
  .column("name")
@@ -4,6 +4,7 @@ Core functionality of TypeDAL.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ # noinspection PyUnusedImports
7
8
  import datetime as dt
8
9
  import sys
9
10
  import typing as t
@@ -21,7 +22,9 @@ from .helpers import (
21
22
  sql_expression,
22
23
  to_snake,
23
24
  )
24
- from .types import CacheStatus, Field, Template # noqa: F401
25
+
26
+ # noinspection PyUnusedImports
27
+ from .types import CacheStatus, Field, Template
25
28
 
26
29
  try:
27
30
  # python 3.14+
@@ -373,9 +373,7 @@ def UploadField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
373
373
  Upload = UploadField
374
374
 
375
375
 
376
- def ReferenceField[
377
- T_subclass: (TypedTable, Table)
378
- ](
376
+ def ReferenceField[T_subclass: (TypedTable, Table)](
379
377
  other_table: str | t.Type[TypedTable] | TypedTable | Table | T_subclass,
380
378
  **kw: t.Unpack[FieldSettings],
381
379
  ) -> TypedField[int]:
@@ -376,6 +376,12 @@ class PydanticMixin(Mixin):
376
376
  field_name: cls._unwrap_pydantic_field_type(field_type) for field_name, field_type in annotations.items()
377
377
  }
378
378
 
379
+ # Respect pyDAL visibility: unreadable DB fields should not be part of pydantic output/schema.
380
+ for field_name in list(fields):
381
+ model_attr = full_dict.get(field_name, getattr(cls, field_name, None))
382
+ if hasattr(model_attr, "readable") and not getattr(model_attr, "readable"):
383
+ fields.pop(field_name, None)
384
+
379
385
  for field_name, field_type in fields.items():
380
386
  cls._ensure_pydantic_compatible_type(field_name, field_type)
381
387
 
@@ -420,8 +426,9 @@ class PydanticMixin(Mixin):
420
426
  )
421
427
 
422
428
  @staticmethod
423
- def _make_instance_converter(_: type, fields: dict[str, t.Any]) -> t.Callable[[t.Any], t.Any]:
429
+ def _make_instance_converter(model_cls: type, fields: dict[str, t.Any]) -> t.Callable[[t.Any], t.Any]:
424
430
  _PRIMITIVES = (str, float, bool, bytes)
431
+ relationship_names = set(model_cls.get_relationships()) if hasattr(model_cls, "get_relationships") else set()
425
432
 
426
433
  def convert(value: t.Any) -> t.Any:
427
434
  if isinstance(value, dict):
@@ -432,7 +439,16 @@ class PydanticMixin(Mixin):
432
439
  if isinstance(value, _PRIMITIVES) or value is None:
433
440
  return value
434
441
  # Handles both TypedTable instances and raw pydal Row objects
435
- return {k: getattr(value, k, None) for k in fields}
442
+ values_dict = getattr(value, "__dict__", {})
443
+ result: dict[str, t.Any] = {}
444
+ for field_name in fields:
445
+ # Never trigger lazy-loads during pydantic conversion.
446
+ if field_name in relationship_names and field_name not in values_dict:
447
+ continue
448
+
449
+ result[field_name] = getattr(value, field_name, None)
450
+
451
+ return result
436
452
 
437
453
  return convert
438
454
 
@@ -529,11 +545,9 @@ class PydanticMixin(Mixin):
529
545
  if field_name in _required_fields:
530
546
  # Always computed — keep required and non-nullable for clean TS types
531
547
  return core_schema.typed_dict_field(inner, required=True)
532
- # DB fields / relationships: TypeDAL can return partial rows, so allow None
533
- return core_schema.typed_dict_field(
534
- core_schema.with_default_schema(core_schema.nullable_schema(inner), default=None),
535
- required=False,
536
- )
548
+ # DB fields / relationships: TypeDAL can return partial rows, so allow None when present,
549
+ # but keep missing fields absent (don't auto-fill null/default values).
550
+ return core_schema.typed_dict_field(core_schema.nullable_schema(inner), required=False)
537
551
 
538
552
  schema_fields = {field_name: make_field(field_name, field_type) for field_name, field_type in fields.items()}
539
553
 
@@ -586,14 +600,15 @@ class PydanticMixin(Mixin):
586
600
  def model_dump(self, mode: str = "python", *, _shallow: bool = False) -> dict[str, t.Any]:
587
601
  """Serialize this model to a dict, with optional shallow nested output."""
588
602
  cls = type(self)
603
+ relationship_names = set(cls.get_relationships())
589
604
  data: dict[str, t.Any] = {}
590
605
  for field_name in self._pydantic_fields(
591
606
  include_relationships=not _shallow,
592
607
  include_properties=not _shallow,
593
608
  ):
594
- # Match web2py/pyDAL behavior: unreadable db fields are excluded from serialized output.
595
- model_attr = getattr(cls, field_name, None)
596
- if hasattr(model_attr, "readable") and not getattr(model_attr, "readable"):
609
+ # Only include relationship data that was already selected/joined.
610
+ # This prevents model_dump from triggering lazy-loading queries.
611
+ if field_name in relationship_names and field_name not in self.__dict__:
597
612
  continue
598
613
 
599
614
  data[field_name] = getattr(self, field_name, None)
@@ -421,8 +421,8 @@ class TypedRows(t.Collection[T_MetaInstance], Rows):
421
421
  fields: list[Field] | None = None,
422
422
  ) -> t.Generator[T_MetaInstance, None, None] | T_MetaInstance:
423
423
  """
424
- Takes an index and returns a copy of the indexed row with values \
425
- transformed via the "represent" attributes of the associated fields.
424
+ Takes an index and returns a copy of the indexed row with values
425
+ transformed via the "represent" attributes of the associated fields.
426
426
 
427
427
  Args:
428
428
  i: index. If not specified, a generator is returned for iteration
@@ -1104,6 +1104,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
1104
1104
  # then create a new (more empty) row object:
1105
1105
  state["_row"] = Row(json.loads(state["_row"]))
1106
1106
  self.__dict__ |= state
1107
+ self._setup_instance_methods()
1107
1108
 
1108
1109
  @classmethod
1109
1110
  def _sql(cls) -> str:
@@ -35,7 +35,9 @@ except ImportError:
35
35
  # Internal references
36
36
  if t.TYPE_CHECKING:
37
37
  from .fields import TypedField
38
- from .tables import TypedTable, _TypedTable # noqa: F401
38
+
39
+ # noinspection PyUnusedImports
40
+ from .tables import TypedTable, _TypedTable
39
41
 
40
42
  # ---------------------------------------------------------------------------
41
43
  # Aliases
@@ -237,6 +237,11 @@ class PydanticHiddenTypedField(TypedTable, PydanticMixin):
237
237
  hidden = TypedField(str, readable=False)
238
238
 
239
239
 
240
+ class PydanticHiddenRelationshipHost(TypedTable, PydanticMixin):
241
+ name: str
242
+ with_hidden = relationship(list[PydanticHiddenTypedField], lambda self, other: self.id == other.id, lazy="allow")
243
+
244
+
240
245
  @pytest.fixture
241
246
  def pydantic_db():
242
247
  db = TypeDAL("sqlite:memory")
@@ -247,6 +252,7 @@ def pydantic_db():
247
252
  db.define(PydanticGenericResolvedRelationship)
248
253
  db.define(PydanticGenericUnresolvedRelationship)
249
254
  db.define(PydanticHiddenTypedField)
255
+ db.define(PydanticHiddenRelationshipHost)
250
256
  db.define(NonPydanticAuthor)
251
257
  yield db
252
258
 
@@ -460,6 +466,46 @@ def test_pydantic_skips_unreadable_typedfield_in_model_dump(pydantic_db):
460
466
  assert data == {"id": row.id}
461
467
 
462
468
 
469
+ def test_pydantic_skips_unreadable_typedfield_in_nested_list_relationship_dump(pydantic_db):
470
+ hidden = PydanticHiddenTypedField.insert(visible="show", hidden="hide")
471
+ host = PydanticHiddenRelationshipHost.insert(name="Host")
472
+ assert hidden.id == host.id
473
+
474
+ joined = PydanticHiddenRelationshipHost.where(id=host.id).join("with_hidden").first()
475
+ assert joined is not None
476
+
477
+ data = joined.model_dump(mode="json")
478
+ assert data["with_hidden"] == [{"id": hidden.id, "visible": "show"}]
479
+ assert joined.with_hidden[0].hidden == "hide"
480
+ assert [item.model_dump() for item in joined.with_hidden] == [{"id": hidden.id, "visible": "show"}]
481
+
482
+
483
+ def test_pydantic_model_dump_never_lazy_loads_unjoined_relationships(pydantic_db):
484
+ hidden = PydanticHiddenTypedField.insert(visible="show", hidden="hide")
485
+ host = PydanticHiddenRelationshipHost.insert(name="Host")
486
+ assert hidden.id == host.id
487
+
488
+ data = host.model_dump(mode="json")
489
+ assert "with_hidden" not in data
490
+
491
+
492
+ def test_pydantic_type_adapter_skips_unreadable_fields(pydantic_db):
493
+ row = PydanticHiddenTypedField.insert(visible="show", hidden="hide")
494
+ ta = pydantic.TypeAdapter(PydanticHiddenTypedField)
495
+ data = ta.validate_python(row)
496
+ assert data == {"id": row.id, "visible": "show"}
497
+
498
+
499
+ def test_pydantic_type_adapter_never_lazy_loads_unjoined_relationships(pydantic_db):
500
+ hidden = PydanticHiddenTypedField.insert(visible="show", hidden="hide")
501
+ host = PydanticHiddenRelationshipHost.insert(name="Host")
502
+ assert hidden.id == host.id
503
+
504
+ ta = pydantic.TypeAdapter(PydanticHiddenRelationshipHost)
505
+ data = ta.validate_python(host)
506
+ assert "with_hidden" not in data
507
+
508
+
463
509
  def test_pydantic_compatibility_non_type_and_missing_relationship_type():
464
510
  # ForwardRef is not a runtime type; this should be a no-op, not a crash.
465
511
  PydanticMixin._ensure_pydantic_compatible_type("x", typing.ForwardRef("Anything"))
@@ -266,6 +266,15 @@ def test_render():
266
266
  assert rendered_two.normal == "123"
267
267
  assert rendered_two.list_field == "abc, def"
268
268
  assert rendered_two.related.also_normal == "321"
269
+ assert json.loads(rendered_two.as_json()) == {
270
+ "id": rendered_two.id,
271
+ "normal": "123",
272
+ "list_field": "abc, def",
273
+ "related": {
274
+ "id": rendered_two.related.id,
275
+ "also_normal": "321",
276
+ },
277
+ }
269
278
 
270
279
  # test list:
271
280
 
@@ -287,3 +296,14 @@ def test_render():
287
296
  assert rendered_four.normal == "123"
288
297
  assert rendered_four.list_field == "abc, def"
289
298
  assert rendered_four.related_list[0].also_normal == "321"
299
+ assert json.loads(rendered_four.as_json()) == {
300
+ "id": rendered_four.id,
301
+ "normal": "123",
302
+ "list_field": "abc, def",
303
+ "related_list": [
304
+ {
305
+ "id": rendered_four.related_list[0].id,
306
+ "also_normal": "321",
307
+ }
308
+ ],
309
+ }
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
File without changes
File without changes
File without changes