TypeDAL 4.7.1__tar.gz → 4.7.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.
Files changed (74) hide show
  1. {typedal-4.7.1 → typedal-4.7.2}/CHANGELOG.md +6 -0
  2. {typedal-4.7.1 → typedal-4.7.2}/PKG-INFO +1 -1
  3. {typedal-4.7.1 → typedal-4.7.2}/docs/2_defining_tables.md +31 -0
  4. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/__about__.py +1 -1
  5. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/define.py +3 -2
  6. typedal-4.7.2/src/typedal/enum_helpers.py +43 -0
  7. {typedal-4.7.1 → typedal-4.7.2}/tests/test_main.py +41 -10
  8. typedal-4.7.1/release_4_7.md +0 -139
  9. {typedal-4.7.1 → typedal-4.7.2}/.crush/.gitignore +0 -0
  10. {typedal-4.7.1 → typedal-4.7.2}/.crush/init +0 -0
  11. {typedal-4.7.1 → typedal-4.7.2}/.crush/logs/crush.log +0 -0
  12. {typedal-4.7.1 → typedal-4.7.2}/.github/workflows/su6.yml +0 -0
  13. {typedal-4.7.1 → typedal-4.7.2}/.gitignore +0 -0
  14. {typedal-4.7.1 → typedal-4.7.2}/.readthedocs.yml +0 -0
  15. {typedal-4.7.1 → typedal-4.7.2}/README.md +0 -0
  16. {typedal-4.7.1 → typedal-4.7.2}/coverage.svg +0 -0
  17. {typedal-4.7.1 → typedal-4.7.2}/docs/10_advanced_apis.md +0 -0
  18. {typedal-4.7.1 → typedal-4.7.2}/docs/1_getting_started.md +0 -0
  19. {typedal-4.7.1 → typedal-4.7.2}/docs/3_building_queries.md +0 -0
  20. {typedal-4.7.1 → typedal-4.7.2}/docs/4_relationships.md +0 -0
  21. {typedal-4.7.1 → typedal-4.7.2}/docs/5_py4web.md +0 -0
  22. {typedal-4.7.1 → typedal-4.7.2}/docs/6_migrations.md +0 -0
  23. {typedal-4.7.1 → typedal-4.7.2}/docs/7_configuration.md +0 -0
  24. {typedal-4.7.1 → typedal-4.7.2}/docs/8_mixins.md +0 -0
  25. {typedal-4.7.1 → typedal-4.7.2}/docs/9_memoization.md +0 -0
  26. {typedal-4.7.1 → typedal-4.7.2}/docs/css/code_blocks.css +0 -0
  27. {typedal-4.7.1 → typedal-4.7.2}/docs/index.md +0 -0
  28. {typedal-4.7.1 → typedal-4.7.2}/docs/requirements.txt +0 -0
  29. {typedal-4.7.1 → typedal-4.7.2}/example_new.py +0 -0
  30. {typedal-4.7.1 → typedal-4.7.2}/example_old.py +0 -0
  31. {typedal-4.7.1 → typedal-4.7.2}/mkdocs.yml +0 -0
  32. {typedal-4.7.1 → typedal-4.7.2}/pyproject.toml +0 -0
  33. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/__init__.py +0 -0
  34. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/caching.py +0 -0
  35. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/cli.py +0 -0
  36. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/config.py +0 -0
  37. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/constants.py +0 -0
  38. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/core.py +0 -0
  39. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/fields.py +0 -0
  40. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/for_py4web.py +0 -0
  41. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/for_web2py.py +0 -0
  42. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/helpers.py +0 -0
  43. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/mixins.py +0 -0
  44. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/py.typed +0 -0
  45. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/query_builder.py +0 -0
  46. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/relationships.py +0 -0
  47. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/rows.py +0 -0
  48. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/serializers/as_json.py +0 -0
  49. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/tables.py +0 -0
  50. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/types.py +0 -0
  51. {typedal-4.7.1 → typedal-4.7.2}/src/typedal/web2py_py4web_shared.py +0 -0
  52. {typedal-4.7.1 → typedal-4.7.2}/tasks.py +0 -0
  53. {typedal-4.7.1 → typedal-4.7.2}/tests/__init__.py +0 -0
  54. {typedal-4.7.1 → typedal-4.7.2}/tests/configs/simple.toml +0 -0
  55. {typedal-4.7.1 → typedal-4.7.2}/tests/configs/valid.env +0 -0
  56. {typedal-4.7.1 → typedal-4.7.2}/tests/configs/valid.toml +0 -0
  57. {typedal-4.7.1 → typedal-4.7.2}/tests/py314_tests.py +0 -0
  58. {typedal-4.7.1 → typedal-4.7.2}/tests/test_cli.py +0 -0
  59. {typedal-4.7.1 → typedal-4.7.2}/tests/test_config.py +0 -0
  60. {typedal-4.7.1 → typedal-4.7.2}/tests/test_docs_examples.py +0 -0
  61. {typedal-4.7.1 → typedal-4.7.2}/tests/test_helpers.py +0 -0
  62. {typedal-4.7.1 → typedal-4.7.2}/tests/test_json.py +0 -0
  63. {typedal-4.7.1 → typedal-4.7.2}/tests/test_mixins.py +0 -0
  64. {typedal-4.7.1 → typedal-4.7.2}/tests/test_mypy.py +0 -0
  65. {typedal-4.7.1 → typedal-4.7.2}/tests/test_orm.py +0 -0
  66. {typedal-4.7.1 → typedal-4.7.2}/tests/test_py4web.py +0 -0
  67. {typedal-4.7.1 → typedal-4.7.2}/tests/test_query_builder.py +0 -0
  68. {typedal-4.7.1 → typedal-4.7.2}/tests/test_relationships.py +0 -0
  69. {typedal-4.7.1 → typedal-4.7.2}/tests/test_row.py +0 -0
  70. {typedal-4.7.1 → typedal-4.7.2}/tests/test_stats.py +0 -0
  71. {typedal-4.7.1 → typedal-4.7.2}/tests/test_table.py +0 -0
  72. {typedal-4.7.1 → typedal-4.7.2}/tests/test_web2py.py +0 -0
  73. {typedal-4.7.1 → typedal-4.7.2}/tests/test_xx_others.py +0 -0
  74. {typedal-4.7.1 → typedal-4.7.2}/tests/timings.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.7.2 (2026-04-20)
6
+
7
+ ### Fix
8
+
9
+ * **enums:** Add safe enum parsing helper and docs for mixed-type rejection + invalid DB sentinel ([`33ee169`](https://github.com/trialandsuccess/TypeDAL/commit/33ee169b61522cf7a863e29c68822a3b0e07d376))
10
+
5
11
  ## v4.7.1 (2026-04-20)
6
12
 
7
13
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.7.1
3
+ Version: 4.7.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
@@ -51,6 +51,37 @@ Any keyword arguments you would pass to `db.define_table`, you can also pass to
51
51
  | `Field('name', 'big-id')` | × | × | × | × |
52
52
  | `Field('name', 'big-reference')` | × | × | × | × |
53
53
 
54
+ ### Enum fields
55
+
56
+ TypeDAL supports `enum.Enum` subclasses directly (including `enum.StrEnum` and `enum.IntEnum`):
57
+
58
+ ```python
59
+ import enum
60
+
61
+
62
+ class Status(enum.StrEnum):
63
+ DRAFT = "draft"
64
+ PUBLISHED = "published"
65
+
66
+
67
+ class Priority(enum.IntEnum):
68
+ LOW = 1
69
+ HIGH = 2
70
+
71
+
72
+ @db.define()
73
+ class Article(TypedTable):
74
+ status: Status
75
+ priority: Priority
76
+ ```
77
+
78
+ Important constraints and behavior:
79
+
80
+ - All enum member values in one enum must share the same underlying Python type for DB fields.
81
+ Mixed enums (for example `str` + `int` in one enum class) raise `TypeError` when defining the table.
82
+ - Reading rows with invalid enum values in the database does not crash. Those values are returned as
83
+ `typedal.enum_helpers.InvalidEnumValue`, where `.value` is `None`.
84
+
54
85
  ### Making a field required/optional
55
86
 
56
87
  | pydal | typedal (native python type) | typedal (using TypedField annotation) | typedal (using TypedField) | typedal (using specific Field) |
@@ -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.7.1"
8
+ __version__ = "4.7.2"
@@ -17,6 +17,7 @@ from pydal.validators import IS_IN_SET, ValidationError, Validator
17
17
 
18
18
  from .constants import BASIC_MAPPINGS
19
19
  from .core import TypeDAL, evaluate_forward_reference, resolve_annotation
20
+ from .enum_helpers import enum_value_type, make_enum_filter_out
20
21
  from .fields import TypedField, is_typed_field
21
22
  from .helpers import (
22
23
  all_annotations,
@@ -178,10 +179,10 @@ class TableDefinitionBuilder:
178
179
  _child_type = type(t.get_args(ftype)[0])
179
180
  return self.annotation_to_pydal_fieldtype(_child_type, mut_kw)
180
181
  elif isinstance(ftype, type) and issubclass(ftype, enum.Enum):
181
- _values = [v.value for v in ftype]
182
- _child_type = type(_values[0])
182
+ _child_type = enum_value_type(ftype)
183
183
  # mut_kw.setdefault("requires", [IS_IN_SET(_values)])
184
184
  mut_kw.setdefault("requires", [IS_IN_ENUM(ftype)])
185
+ mut_kw.setdefault("filter_out", make_enum_filter_out(ftype))
185
186
  return self.annotation_to_pydal_fieldtype(_child_type, mut_kw)
186
187
  elif isinstance(ftype, types.GenericAlias) and t.get_origin(ftype) in (list, TypedField):
187
188
  # list[str] -> str -> string -> list:string
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import typing as t
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class InvalidEnumValue:
10
+ enum_type: type[enum.Enum]
11
+ raw: t.Any
12
+ value: None = None
13
+
14
+
15
+ def enum_value_type(enum_type: type[enum.Enum]) -> type[t.Any]:
16
+ values = [member.value for member in enum_type]
17
+ if not values: # pragma: no cover
18
+ raise TypeError(f"Enum {enum_type.__name__} has no members.")
19
+
20
+ first_type = type(values[0])
21
+ if any(type(value) is not first_type for value in values):
22
+ raise TypeError(
23
+ f"Enum {enum_type.__name__} has mixed value types; all values must share one type for DB fields.",
24
+ )
25
+ return first_type
26
+
27
+
28
+ def parse_enum_value(enum_type: type[enum.Enum], raw: t.Any) -> enum.Enum | InvalidEnumValue | None:
29
+ if raw is None: # pragma: no cover
30
+ return None
31
+
32
+ if isinstance(raw, enum_type): # pragma: no cover
33
+ return raw
34
+
35
+ value_map = {str(member.value): member for member in enum_type}
36
+ return value_map.get(str(raw), InvalidEnumValue(enum_type=enum_type, raw=raw))
37
+
38
+
39
+ def make_enum_filter_out(enum_type: type[enum.Enum]) -> t.Callable[[t.Any], enum.Enum | InvalidEnumValue | None]:
40
+ def _filter_out(raw: t.Any) -> enum.Enum | InvalidEnumValue | None:
41
+ return parse_enum_value(enum_type, raw)
42
+
43
+ return _filter_out
@@ -10,6 +10,7 @@ import pytest
10
10
 
11
11
  from src.typedal import TypedRows
12
12
  from src.typedal.__about__ import __version__
13
+ from src.typedal.enum_helpers import InvalidEnumValue
13
14
  from src.typedal.fields import *
14
15
 
15
16
 
@@ -193,14 +194,12 @@ def test_dont_allow_bool_in_query():
193
194
 
194
195
  def test_invalid_union():
195
196
  with pytest.raises(NotImplementedError):
196
-
197
197
  @db.define
198
198
  class Invalid(TypedTable):
199
199
  valid: int | None
200
200
  invalid: int | str
201
201
 
202
202
  with pytest.raises(NotImplementedError):
203
-
204
203
  @db.define
205
204
  class Invalid(TypedTable):
206
205
  valid: list[int]
@@ -278,7 +277,6 @@ def test_typedfield_to_field_type():
278
277
  optional_two = TypedField(str | None)
279
278
 
280
279
  with pytest.raises(NotImplementedError):
281
-
282
280
  @db.define()
283
281
  class Invalid(TypedTable):
284
282
  third = TypedField(dict[str, int]) # not supported
@@ -592,7 +590,8 @@ def test_forward_reference_class_314():
592
590
  class WithFakeRef(TypedTable):
593
591
  fwd: Fake
594
592
 
595
- class Future(TypedTable): ...
593
+ class Future(TypedTable):
594
+ ...
596
595
 
597
596
  # note: this still has to be defined first because otherwise pydal can't create a database relation!:
598
597
  assert db.define(Future)
@@ -643,34 +642,52 @@ def test_reorder_fields():
643
642
 
644
643
 
645
644
  def test_literal_enum_fields():
646
- class TestEnum(enum.Enum):
645
+ class MixedEnum(enum.Enum):
647
646
  FIRST = "first"
648
647
  SECOND = 2
649
648
 
649
+ with pytest.raises(TypeError, match="mixed value types"):
650
+ @db.define()
651
+ class MixedLiteralTable(TypedTable):
652
+ enum_one: MixedEnum
653
+
654
+ class TestStrEnum(enum.StrEnum):
655
+ FIRST = "first"
656
+ SECOND = "second"
657
+
658
+ class TestIntEnum(enum.IntEnum):
659
+ FIRST = 1
660
+ SECOND = 2
661
+
650
662
  @db.define()
651
663
  class LiteralTable(TypedTable):
652
664
  lit_one: t.Literal["first", "second"]
653
665
  lit_two = TypedField(t.Literal["first", "second"])
654
666
 
655
- enum_one: TestEnum
656
- enum_two = TypedField(TestEnum)
667
+ enum_one: TestStrEnum
668
+ enum_two = TypedField(TestIntEnum)
657
669
 
658
670
  # should be ok
659
671
  row, err = LiteralTable.validate_and_insert(
660
672
  lit_one="first",
661
673
  lit_two="second",
662
- enum_one=TestEnum.FIRST,
674
+ enum_one=TestStrEnum.FIRST,
663
675
  enum_two=2,
664
676
  )
665
677
  assert not err, "unexpected error"
666
678
  assert row, "expected row"
667
679
 
680
+ assert isinstance(row.enum_one, TestStrEnum)
681
+ assert row.enum_one.value == "first"
682
+ assert isinstance(row.enum_two, TestIntEnum)
683
+ assert row.enum_two == TestIntEnum.SECOND
684
+
668
685
  # should error on lit_one
669
686
  row, err = LiteralTable.validate_and_insert(
670
687
  lit_one="wrong",
671
688
  lit_two="wronger",
672
- enum_one=1,
673
- enum_two="two",
689
+ enum_one="invalid",
690
+ enum_two=-1,
674
691
  )
675
692
  assert not row, "unexpected row"
676
693
  assert err, "expected err"
@@ -679,3 +696,17 @@ def test_literal_enum_fields():
679
696
  assert "lit_two" in err
680
697
  assert "enum_one" in err
681
698
  assert "enum_two" in err
699
+
700
+ idx = db.executesql("""
701
+ INSERT INTO literal_table(lit_one, lit_two, enum_one, enum_two)
702
+ VALUES ('fake', 'fake', 'fake', 999)
703
+ RETURNING id
704
+ """)[0][0]
705
+
706
+ invalid_row = LiteralTable(idx)
707
+ assert isinstance(invalid_row.enum_one, InvalidEnumValue)
708
+ assert invalid_row.enum_one.raw == "fake"
709
+ assert invalid_row.enum_one.value is None
710
+ assert isinstance(invalid_row.enum_two, InvalidEnumValue)
711
+ assert invalid_row.enum_two.raw == 999
712
+ assert invalid_row.enum_two.value is None
@@ -1,139 +0,0 @@
1
- # TypeDAL 4.7: A literal improvement to Enums
2
-
3
- **Release date:** 20-04-2026
4
-
5
- ---
6
-
7
- TypeDAL v4.7 introduces native support for `typing.Literal` and `enum.Enum` as field types.
8
- No more manually wiring `IS_IN_SET` validators for constrained values.
9
-
10
- ## What's New
11
-
12
- ### `typing.Literal` fields
13
-
14
- Declare a field with a fixed set of allowed string/integer values and TypeDAL automatically infers the database type and
15
- attaches an `IS_IN_SET` validator:
16
-
17
- ```python
18
- from typing import Literal
19
-
20
-
21
- @db.define()
22
- class Ticket(TypedTable):
23
- status: Literal["open", "in_progress", "closed"]
24
- priority: Literal[1, 2, 3]
25
- ```
26
-
27
- ### `Enum` fields
28
-
29
- Use Python `Enum` classes directly. TypeDAL extracts the values, infers the underlying type, and applies an `IS_IN_ENUM`
30
- validator:
31
-
32
- ```python
33
- import enum
34
-
35
-
36
- class Color(enum.Enum):
37
- RED = "red"
38
- GREEN = "green"
39
- BLUE = "blue"
40
-
41
-
42
- @db.define()
43
- class Palette(TypedTable):
44
- color: Color
45
- ```
46
-
47
- The `IS_IN_ENUM` validator accepts both enum members and their raw values:
48
-
49
- ```python
50
- row = Palette(color=Color.RED) # via enum member
51
- row = Palette(color="red") # via raw value
52
- ```
53
-
54
- ## Other changes since v4.6
55
-
56
- These fixes landed incrementally between v4.6 and now (shipped as 4.6.1–4.6.4):
57
- Included here as a recap for users upgrading from 4.6.0 directly to 4.7.
58
-
59
- ### collect_into
60
-
61
- Collect query results into a different model class. A practical use case is
62
- returning a safe API-facing view from the same underlying table:
63
-
64
- The `init` callback is optional and only needed for per-row runtime enrichment.
65
- Without an explicit `.select(...)`, TypeDAL maps fields declared on the target model.
66
-
67
- ```python
68
- @db.define()
69
- class User(TypedTable):
70
- id: int
71
- email: str
72
- password_hash: str
73
- is_active: bool
74
-
75
- class PublicUser(TypedTable):
76
- """Same users table, but safe for API responses."""
77
- id: int
78
- email: str
79
- is_active: bool
80
- # note: no password_hash
81
- profile_url: str | None = None
82
-
83
- def enrich_profile_url(row: PublicUser, _raw):
84
- row.profile_url = f"/users/{row.id}"
85
-
86
- rows = (
87
- User.where(is_active=True)
88
- # note: `init` is optional
89
- .collect_into(PublicUser, init=enrich_profile_url)
90
- )
91
- row = rows.first()
92
- assert isinstance(row, PublicUser)
93
- assert hasattr(row, "profile_url")
94
- assert "password_hash" not in row
95
- ```
96
-
97
- The cache is isolated per model class, so `.cache().collect_into(PublicUser)`
98
- and a regular `.cache().collect()` do not interfere.
99
-
100
- ### render() → as_dict() consistency
101
-
102
- As of this fix, calling `.render()` on a row also affects subsequent
103
- `.as_dict()` output:
104
-
105
- ```python
106
- row = db.article(1).render()
107
- row.as_dict() # now returns the rendered representation, not raw pydal dict
108
- ```
109
-
110
- ### Pydantic visibility / readable=False
111
-
112
- Fields marked with `readable=False` are now excluded from generated Pydantic
113
- schemas, matching pydal/py4web behavior. Visibility and lazy-load filtering has
114
- also been moved into the schema-converter path so it applies consistently to
115
- FastAPI endpoints.
116
-
117
- ### Mixin typing without MRO conflicts
118
-
119
- Mixin-based table classes now type-check properly in mypy and PyCharm while
120
- keeping runtime MRO clean:
121
-
122
- ```python
123
- class SearchMixin(Mixin):
124
- @classmethod
125
- def by_title(cls, title: str):
126
- return cls.where(title=title)
127
-
128
- @db.define()
129
- class SearchableTable(TypedTable, SearchMixin):
130
- title: str
131
-
132
- def search(table: type[SearchMixin], q: str):
133
- table.by_title(q) # fully typed now
134
- ```
135
-
136
- ## 📜 Changelog
137
-
138
- For all details, see the full changelog:
139
- **[CHANGELOG.md](https://github.com/trialandsuccess/TypeDAL/blob/master/CHANGELOG.md)**
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes