TypeDAL 4.4.6__tar.gz → 4.5.0__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 (69) hide show
  1. {typedal-4.4.6 → typedal-4.5.0}/CHANGELOG.md +6 -0
  2. {typedal-4.4.6 → typedal-4.5.0}/PKG-INFO +1 -1
  3. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/__about__.py +1 -1
  4. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/cli.py +2 -1
  5. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/fields.py +2 -2
  6. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/mixins.py +3 -6
  7. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/query_builder.py +3 -3
  8. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/relationships.py +1 -1
  9. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/rows.py +17 -1
  10. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/tables.py +63 -12
  11. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/types.py +2 -2
  12. {typedal-4.4.6 → typedal-4.5.0}/tests/test_mypy.py +24 -0
  13. {typedal-4.4.6 → typedal-4.5.0}/.github/workflows/su6.yml +0 -0
  14. {typedal-4.4.6 → typedal-4.5.0}/.gitignore +0 -0
  15. {typedal-4.4.6 → typedal-4.5.0}/.readthedocs.yml +0 -0
  16. {typedal-4.4.6 → typedal-4.5.0}/README.md +0 -0
  17. {typedal-4.4.6 → typedal-4.5.0}/coverage.svg +0 -0
  18. {typedal-4.4.6 → typedal-4.5.0}/docs/10_advanced_apis.md +0 -0
  19. {typedal-4.4.6 → typedal-4.5.0}/docs/1_getting_started.md +0 -0
  20. {typedal-4.4.6 → typedal-4.5.0}/docs/2_defining_tables.md +0 -0
  21. {typedal-4.4.6 → typedal-4.5.0}/docs/3_building_queries.md +0 -0
  22. {typedal-4.4.6 → typedal-4.5.0}/docs/4_relationships.md +0 -0
  23. {typedal-4.4.6 → typedal-4.5.0}/docs/5_py4web.md +0 -0
  24. {typedal-4.4.6 → typedal-4.5.0}/docs/6_migrations.md +0 -0
  25. {typedal-4.4.6 → typedal-4.5.0}/docs/7_configuration.md +0 -0
  26. {typedal-4.4.6 → typedal-4.5.0}/docs/8_mixins.md +0 -0
  27. {typedal-4.4.6 → typedal-4.5.0}/docs/9_memoization.md +0 -0
  28. {typedal-4.4.6 → typedal-4.5.0}/docs/css/code_blocks.css +0 -0
  29. {typedal-4.4.6 → typedal-4.5.0}/docs/index.md +0 -0
  30. {typedal-4.4.6 → typedal-4.5.0}/docs/requirements.txt +0 -0
  31. {typedal-4.4.6 → typedal-4.5.0}/example_new.py +0 -0
  32. {typedal-4.4.6 → typedal-4.5.0}/example_old.py +0 -0
  33. {typedal-4.4.6 → typedal-4.5.0}/mkdocs.yml +0 -0
  34. {typedal-4.4.6 → typedal-4.5.0}/pyproject.toml +0 -0
  35. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/__init__.py +0 -0
  36. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/caching.py +0 -0
  37. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/config.py +0 -0
  38. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/constants.py +0 -0
  39. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/core.py +0 -0
  40. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/define.py +0 -0
  41. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/for_py4web.py +0 -0
  42. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/for_web2py.py +0 -0
  43. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/helpers.py +0 -0
  44. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/py.typed +0 -0
  45. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/serializers/as_json.py +0 -0
  46. {typedal-4.4.6 → typedal-4.5.0}/src/typedal/web2py_py4web_shared.py +0 -0
  47. {typedal-4.4.6 → typedal-4.5.0}/tasks.py +0 -0
  48. {typedal-4.4.6 → typedal-4.5.0}/tests/__init__.py +0 -0
  49. {typedal-4.4.6 → typedal-4.5.0}/tests/configs/simple.toml +0 -0
  50. {typedal-4.4.6 → typedal-4.5.0}/tests/configs/valid.env +0 -0
  51. {typedal-4.4.6 → typedal-4.5.0}/tests/configs/valid.toml +0 -0
  52. {typedal-4.4.6 → typedal-4.5.0}/tests/py314_tests.py +0 -0
  53. {typedal-4.4.6 → typedal-4.5.0}/tests/test_cli.py +0 -0
  54. {typedal-4.4.6 → typedal-4.5.0}/tests/test_config.py +0 -0
  55. {typedal-4.4.6 → typedal-4.5.0}/tests/test_docs_examples.py +0 -0
  56. {typedal-4.4.6 → typedal-4.5.0}/tests/test_helpers.py +0 -0
  57. {typedal-4.4.6 → typedal-4.5.0}/tests/test_json.py +0 -0
  58. {typedal-4.4.6 → typedal-4.5.0}/tests/test_main.py +0 -0
  59. {typedal-4.4.6 → typedal-4.5.0}/tests/test_mixins.py +0 -0
  60. {typedal-4.4.6 → typedal-4.5.0}/tests/test_orm.py +0 -0
  61. {typedal-4.4.6 → typedal-4.5.0}/tests/test_py4web.py +0 -0
  62. {typedal-4.4.6 → typedal-4.5.0}/tests/test_query_builder.py +0 -0
  63. {typedal-4.4.6 → typedal-4.5.0}/tests/test_relationships.py +0 -0
  64. {typedal-4.4.6 → typedal-4.5.0}/tests/test_row.py +0 -0
  65. {typedal-4.4.6 → typedal-4.5.0}/tests/test_stats.py +0 -0
  66. {typedal-4.4.6 → typedal-4.5.0}/tests/test_table.py +0 -0
  67. {typedal-4.4.6 → typedal-4.5.0}/tests/test_web2py.py +0 -0
  68. {typedal-4.4.6 → typedal-4.5.0}/tests/test_xx_others.py +0 -0
  69. {typedal-4.4.6 → typedal-4.5.0}/tests/timings.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.5.0 (2026-03-06)
6
+
7
+ ### Feature
8
+
9
+ * **typing:** Support mixin-typed table class APIs without MRO conflicts ([`fe0c99e`](https://github.com/trialandsuccess/TypeDAL/commit/fe0c99eb2790baff8ddf4de3ec1f23c53e4b3a11))
10
+
5
11
  ## v4.4.6 (2026-03-05)
6
12
 
7
13
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.4.6
3
+ Version: 4.5.0
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__ = "4.4.6"
8
+ __version__ = "4.5.0"
@@ -392,7 +392,8 @@ def fake_migrations(
392
392
 
393
393
  previously_migrated = (
394
394
  db(
395
- db.ewh_implemented_features.name.belongs(to_fake) & (db.ewh_implemented_features.installed == True) # noqa E712
395
+ db.ewh_implemented_features.name.belongs(to_fake)
396
+ & (db.ewh_implemented_features.installed == True) # noqa E712
396
397
  )
397
398
  .select(db.ewh_implemented_features.name)
398
399
  .column("name")
@@ -31,7 +31,7 @@ from .types import (
31
31
 
32
32
  if t.TYPE_CHECKING:
33
33
  # will be imported for real later:
34
- from .tables import TypedTable
34
+ from .tables import TypedTable, _TypedTable
35
35
 
36
36
 
37
37
  ## general
@@ -79,7 +79,7 @@ class TypedField(Expression, t.Generic[T_Value]): # pragma: no cover
79
79
  """
80
80
 
81
81
  @t.overload
82
- def __get__(self, instance: None, owner: "t.Type[TypedTable]") -> "TypedField[T_Value]": # pragma: no cover
82
+ def __get__(self, instance: None, owner: "t.Type[_TypedTable]") -> "TypedField[T_Value]": # pragma: no cover
83
83
  """
84
84
  Table.field -> Field.
85
85
  """
@@ -16,21 +16,18 @@ from slugify import slugify
16
16
 
17
17
  from .core import TypeDAL
18
18
  from .fields import DatetimeField, StringField
19
- from .tables import _TypedTable
19
+ from .tables import TableMeta, _TypedTable
20
20
  from .types import OpRow, Set, T_MetaInstance
21
21
 
22
- if t.TYPE_CHECKING:
23
- from .tables import TypedTable # noqa: F401
24
22
 
25
-
26
- class Mixin(_TypedTable):
23
+ class Mixin(_TypedTable, metaclass=TableMeta):
27
24
  """
28
25
  A mixin should be derived from this class.
29
26
 
30
27
  The mixin base class itself doesn't do anything,
31
28
  but using it makes sure the mixin fields are placed AFTER the table's normal fields (instead of before)
32
29
 
33
- During runtime, mixin should not have a base class in order to prevent MRO issues
30
+ During runtime, mixin should not inherit from TypedTable to prevent MRO issues
34
31
  ('inconsistent method resolution' or 'metaclass conflicts')
35
32
  """
36
33
 
@@ -23,7 +23,7 @@ from .helpers import (
23
23
  normalize_table_keys,
24
24
  throw,
25
25
  )
26
- from .tables import TableMeta, TypedTable
26
+ from .tables import TableMeta, TypedTable, _TypedTable
27
27
  from .types import (
28
28
  CacheMetadata,
29
29
  Condition,
@@ -698,7 +698,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
698
698
  return joins
699
699
 
700
700
  def _build_inner_joins_recursive(
701
- self, relation: Relationship[t.Any], parent_table: t.Type[TypedTable], key: str, parent_key: str = ""
701
+ self, relation: Relationship[t.Any], parent_table: t.Type[_TypedTable], key: str, parent_key: str = ""
702
702
  ) -> list[t.Any]:
703
703
  """Recursively build inner joins for a relationship and its nested relationships."""
704
704
  db = self._get_db()
@@ -764,7 +764,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
764
764
  key: str,
765
765
  select_args: list[t.Any],
766
766
  left_joins: list[Expression],
767
- parent_table: t.Type[TypedTable],
767
+ parent_table: t.Type[_TypedTable],
768
768
  parent_key: str = "",
769
769
  ) -> list[t.Any]:
770
770
  """Process a single relationship for left join and field selection."""
@@ -184,7 +184,7 @@ class Relationship(t.Generic[To_Type]):
184
184
  return db._config.lazy_policy
185
185
 
186
186
  # conservative fallback:
187
- return "warn"
187
+ return "warn" # pragma: no cover
188
188
 
189
189
  def get_table_name(self) -> str:
190
190
  """
@@ -400,11 +400,27 @@ class TypedRows(t.Collection[T_MetaInstance], Rows):
400
400
  self.__dict__.update(state)
401
401
  # db etc. set after undill by caching.py
402
402
 
403
+ @t.overload
403
404
  def render(
404
405
  self,
405
- i: int | None = None,
406
+ i: None = None,
406
407
  fields: list[Field] | None = None,
407
408
  ) -> t.Generator[T_MetaInstance, None, None]:
409
+ """With no index, yield rendered rows as a generator."""
410
+
411
+ @t.overload
412
+ def render(
413
+ self,
414
+ i: int,
415
+ fields: list[Field] | None = None,
416
+ ) -> T_MetaInstance:
417
+ """With an index, return one rendered row instance."""
418
+
419
+ def render(
420
+ self,
421
+ i: int | None = None,
422
+ fields: list[Field] | None = None,
423
+ ) -> t.Generator[T_MetaInstance, None, None] | T_MetaInstance:
408
424
  """
409
425
  Takes an index and returns a copy of the indexed row with values \
410
426
  transformed via the "represent" attributes of the associated fields.
@@ -650,7 +650,7 @@ class TableMeta(type):
650
650
  return reorder_fields(cls._table, fields, keep_others=keep_others)
651
651
 
652
652
 
653
- class _TypedTable:
653
+ class _TypedTable(metaclass=TableMeta):
654
654
  """
655
655
  This class is a final shared parent between TypedTable and Mixins.
656
656
 
@@ -661,6 +661,9 @@ class _TypedTable:
661
661
  -> Setting 'TypedTable' as the parent for Mixin does not work at runtime (and works semi at type check time)
662
662
  """
663
663
 
664
+ # This class contains weird typing glue to dodge MRO headaches without losing editor/mypy table methods
665
+ # you can safely ignore it when changing runtime behavior; touch it only for typing/mypy issues
666
+
664
667
  id: "TypedField[int]"
665
668
 
666
669
  _before_insert: list[t.Callable[[t.Self], t.Optional[bool]] | t.Callable[[OpRow], t.Optional[bool]]]
@@ -671,6 +674,8 @@ class _TypedTable:
671
674
  _after_update: list[t.Callable[[Set, t.Self], t.Optional[bool]] | t.Callable[[Set, OpRow], t.Optional[bool]]]
672
675
  _before_delete: list[t.Callable[[Set], t.Optional[bool]]]
673
676
  _after_delete: list[t.Callable[[Set], t.Optional[bool]]]
677
+ _rows: tuple[Row, ...]
678
+ _with: list[str]
674
679
 
675
680
  @classmethod
676
681
  def __on_define__(cls, db: TypeDAL) -> None:
@@ -681,16 +686,49 @@ class _TypedTable:
681
686
  where you need a reference to the current database, which may not exist yet when defining the model.
682
687
  """
683
688
 
684
- @classproperty
685
- def _hooks(cls) -> dict[str, list[t.Callable[..., t.Optional[bool]]]]:
686
- return {
687
- "before_insert": cls._before_insert,
688
- "after_insert": cls._after_insert,
689
- "before_update": cls._before_update,
690
- "after_update": cls._after_update,
691
- "before_delete": cls._before_delete,
692
- "after_delete": cls._after_delete,
693
- }
689
+ def __new__(cls, *_args: t.Any, **_kwargs: t.Any) -> t.Self:
690
+ """
691
+ Shared constructor signature for typing.
692
+
693
+ TypedTable provides the concrete behavior; this base only keeps static typing happy
694
+ for generic classmethod flows that instantiate `self(...)`.
695
+ """
696
+ return super().__new__(cls)
697
+
698
+ # Add an abstract placeholder here only when generic code is typed against
699
+ # `T_MetaInstance` (bound to `_TypedTable`) and directly calls/accesses that member.
700
+ # If a member is only used on concrete `TypedTable` paths, it should stay on `TypedTable`.
701
+ def _ensure_matching_row(self) -> Row:
702
+ # Typed on the shared base so generic instance helpers can call into row access safely.
703
+ raise NotImplementedError # pragma: no cover
704
+
705
+ def _update(self: t.Self, **fields: t.Any) -> t.Self:
706
+ # Declared here for generic update flows; real behavior is implemented in TypedTable.
707
+ raise NotImplementedError # pragma: no cover
708
+
709
+ def _update_record(self: t.Self, **fields: t.Any) -> t.Self:
710
+ # Declared here for generic update flows; real behavior is implemented in TypedTable.
711
+ raise NotImplementedError # pragma: no cover
712
+
713
+ def update_record(self: t.Self, **fields: t.Any) -> t.Self:
714
+ # Declared here for generic update flows; real behavior is implemented in TypedTable.
715
+ raise NotImplementedError # pragma: no cover
716
+
717
+ def as_dict(self, *args: t.Any, **kwargs: t.Any) -> AnyDict:
718
+ # Broad signature keeps class/instance serialization overrides LSP-compatible.
719
+ raise NotImplementedError # pragma: no cover
720
+
721
+ def render(self: t.Self, *fields: t.Any, **kwargs: t.Any) -> t.Self:
722
+ # Rows/QueryBuilder treat render as model-preserving, so this returns Self for typing.
723
+ raise NotImplementedError # pragma: no cover
724
+
725
+ def __getitem__(self, key: str) -> t.Any:
726
+ # Relationship collection writes into model instances via dict-style access.
727
+ raise NotImplementedError # pragma: no cover
728
+
729
+ def __setitem__(self, key: str, value: t.Any) -> None:
730
+ # Relationship collection writes into model instances via dict-style access.
731
+ raise NotImplementedError # pragma: no cover
694
732
 
695
733
 
696
734
  class TypedTable(_TypedTable, metaclass=TableMeta):
@@ -702,8 +740,21 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
702
740
  _row: Row | None = None
703
741
  _rows: tuple[Row, ...] = ()
704
742
 
743
+ id: "TypedField[int]"
744
+
705
745
  _with: list[str]
706
746
 
747
+ @classproperty
748
+ def _hooks(cls) -> dict[str, list[t.Callable[..., t.Optional[bool]]]]:
749
+ return {
750
+ "before_insert": cls._before_insert,
751
+ "after_insert": cls._after_insert,
752
+ "before_update": cls._before_update,
753
+ "after_update": cls._after_update,
754
+ "before_delete": cls._before_delete,
755
+ "after_delete": cls._after_delete,
756
+ }
757
+
707
758
  def _setup_instance_methods(self) -> None:
708
759
  self.as_dict = self._as_dict # type: ignore
709
760
  self.__json__ = self.as_json = self._as_json # type: ignore
@@ -990,7 +1041,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
990
1041
  def _update_record(self: T_MetaInstance, **fields: t.Any) -> T_MetaInstance:
991
1042
  row = self._ensure_matching_row()
992
1043
  new_row = row.update_record(**fields)
993
- self.update(**new_row)
1044
+ self._update(**new_row)
994
1045
  return self
995
1046
 
996
1047
  def update_record(self: T_MetaInstance, **fields: t.Any) -> T_MetaInstance: # pragma: no cover
@@ -35,7 +35,7 @@ except ImportError:
35
35
  # Internal references
36
36
  if t.TYPE_CHECKING:
37
37
  from .fields import TypedField
38
- from .tables import TypedTable
38
+ from .tables import TypedTable, _TypedTable
39
39
 
40
40
  # ---------------------------------------------------------------------------
41
41
  # Aliases
@@ -297,7 +297,7 @@ T = t.TypeVar("T", bound=t.Any)
297
297
  P = t.ParamSpec("P")
298
298
  R = t.TypeVar("R")
299
299
 
300
- T_MetaInstance = t.TypeVar("T_MetaInstance", bound="TypedTable")
300
+ T_MetaInstance = t.TypeVar("T_MetaInstance", bound="_TypedTable")
301
301
  T_Query = t.Union[
302
302
  "Table",
303
303
  Query,
@@ -10,6 +10,7 @@ from typedal import ( # todo: why does src.typedal not work anymore?
10
10
  TypedRows,
11
11
  TypedTable,
12
12
  )
13
+ from typedal.mixins import Mixin
13
14
  from typedal.types import CacheFn, CacheTuple, OpRow, Reference, Rows
14
15
 
15
16
  db = TypeDAL("sqlite:memory")
@@ -29,6 +30,14 @@ class OtherTable(TypedTable): ...
29
30
  class LaterDefine(TypedTable): ...
30
31
 
31
32
 
33
+ class SearchMixin(Mixin): ...
34
+
35
+
36
+ @db.define
37
+ class SearchableTable(TypedTable, SearchMixin):
38
+ title: str
39
+
40
+
32
41
  old_style = db.define_table("old_table")
33
42
 
34
43
 
@@ -152,6 +161,21 @@ def mypy_test_query() -> None:
152
161
  reveal_type(MyTable.where().column(MyTable.fancy)) # R: builtins.list[builtins.str]
153
162
 
154
163
 
164
+ @pytest.mark.mypy_testing
165
+ def mypy_test_rows_render_overload() -> None:
166
+ rows = MyTable.where().collect()
167
+ reveal_type(rows.render()) # R: typing.Generator[tests.test_mypy.MyTable, None, None]
168
+ reveal_type(rows.render(1)) # R: tests.test_mypy.MyTable
169
+
170
+
171
+ @pytest.mark.mypy_testing
172
+ def mypy_test_mixin_typed_table_argument() -> None:
173
+ def using_mixin(table: type[SearchMixin]) -> None:
174
+ reveal_type(table.where()) # R: typedal.query_builder.QueryBuilder[tests.test_mypy.SearchMixin]
175
+
176
+ using_mixin(SearchableTable)
177
+
178
+
155
179
  @pytest.mark.mypy_testing
156
180
  def mypy_test_cachefn() -> None:
157
181
  def cache_model(key: str, fn: CacheFn, expire: int) -> Rows:
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