TypeDAL 3.8.0__tar.gz → 3.8.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.8.0 → typedal-3.8.2}/.github/workflows/su6.yml +1 -1
  2. {typedal-3.8.0 → typedal-3.8.2}/CHANGELOG.md +10 -0
  3. {typedal-3.8.0 → typedal-3.8.2}/PKG-INFO +1 -1
  4. {typedal-3.8.0 → typedal-3.8.2}/example_new.py +3 -2
  5. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/__about__.py +1 -1
  6. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/core.py +15 -6
  7. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/for_web2py.py +2 -0
  8. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/mixins.py +17 -6
  9. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/types.py +1 -1
  10. {typedal-3.8.0 → typedal-3.8.2}/tests/test_config.py +13 -9
  11. {typedal-3.8.0 → typedal-3.8.2}/tests/test_helpers.py +5 -4
  12. {typedal-3.8.0 → typedal-3.8.2}/tests/test_mixins.py +35 -1
  13. {typedal-3.8.0 → typedal-3.8.2}/tests/test_relationships.py +20 -5
  14. {typedal-3.8.0 → typedal-3.8.2}/.gitignore +0 -0
  15. {typedal-3.8.0 → typedal-3.8.2}/.readthedocs.yml +0 -0
  16. {typedal-3.8.0 → typedal-3.8.2}/README.md +0 -0
  17. {typedal-3.8.0 → typedal-3.8.2}/coverage.svg +0 -0
  18. {typedal-3.8.0 → typedal-3.8.2}/docs/1_getting_started.md +0 -0
  19. {typedal-3.8.0 → typedal-3.8.2}/docs/2_defining_tables.md +0 -0
  20. {typedal-3.8.0 → typedal-3.8.2}/docs/3_building_queries.md +0 -0
  21. {typedal-3.8.0 → typedal-3.8.2}/docs/4_relationships.md +0 -0
  22. {typedal-3.8.0 → typedal-3.8.2}/docs/5_py4web.md +0 -0
  23. {typedal-3.8.0 → typedal-3.8.2}/docs/6_migrations.md +0 -0
  24. {typedal-3.8.0 → typedal-3.8.2}/docs/7_mixins.md +0 -0
  25. {typedal-3.8.0 → typedal-3.8.2}/docs/css/code_blocks.css +0 -0
  26. {typedal-3.8.0 → typedal-3.8.2}/docs/index.md +0 -0
  27. {typedal-3.8.0 → typedal-3.8.2}/docs/requirements.txt +0 -0
  28. {typedal-3.8.0 → typedal-3.8.2}/example_old.py +0 -0
  29. {typedal-3.8.0 → typedal-3.8.2}/mkdocs.yml +0 -0
  30. {typedal-3.8.0 → typedal-3.8.2}/pyproject.toml +0 -0
  31. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/__init__.py +0 -0
  32. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/caching.py +0 -0
  33. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/cli.py +0 -0
  34. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/config.py +0 -0
  35. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/fields.py +0 -0
  36. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/for_py4web.py +0 -0
  37. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/helpers.py +0 -0
  38. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/py.typed +0 -0
  39. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/serializers/as_json.py +0 -0
  40. {typedal-3.8.0 → typedal-3.8.2}/src/typedal/web2py_py4web_shared.py +0 -0
  41. {typedal-3.8.0 → typedal-3.8.2}/tests/__init__.py +0 -0
  42. {typedal-3.8.0 → typedal-3.8.2}/tests/configs/simple.toml +0 -0
  43. {typedal-3.8.0 → typedal-3.8.2}/tests/configs/valid.env +0 -0
  44. {typedal-3.8.0 → typedal-3.8.2}/tests/configs/valid.toml +0 -0
  45. {typedal-3.8.0 → typedal-3.8.2}/tests/test_cli.py +0 -0
  46. {typedal-3.8.0 → typedal-3.8.2}/tests/test_docs_examples.py +0 -0
  47. {typedal-3.8.0 → typedal-3.8.2}/tests/test_json.py +0 -0
  48. {typedal-3.8.0 → typedal-3.8.2}/tests/test_main.py +0 -0
  49. {typedal-3.8.0 → typedal-3.8.2}/tests/test_mypy.py +0 -0
  50. {typedal-3.8.0 → typedal-3.8.2}/tests/test_orm.py +0 -0
  51. {typedal-3.8.0 → typedal-3.8.2}/tests/test_py4web.py +0 -0
  52. {typedal-3.8.0 → typedal-3.8.2}/tests/test_query_builder.py +0 -0
  53. {typedal-3.8.0 → typedal-3.8.2}/tests/test_row.py +0 -0
  54. {typedal-3.8.0 → typedal-3.8.2}/tests/test_stats.py +0 -0
  55. {typedal-3.8.0 → typedal-3.8.2}/tests/test_table.py +0 -0
  56. {typedal-3.8.0 → typedal-3.8.2}/tests/test_web2py.py +0 -0
  57. {typedal-3.8.0 → typedal-3.8.2}/tests/test_xx_others.py +0 -0
  58. {typedal-3.8.0 → typedal-3.8.2}/tests/timings.py +0 -0
@@ -25,7 +25,7 @@ jobs:
25
25
  - uses: actions/checkout@v3
26
26
  - uses: actions/setup-python@v4
27
27
  with:
28
- python-version: '3.12'
28
+ python-version: '3.13'
29
29
  - uses: yezz123/setup-uv@v4
30
30
  with:
31
31
  uv-venv: ".venv"
@@ -2,6 +2,16 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v3.8.2 (2024-10-23)
6
+
7
+
8
+
9
+ ## v3.8.1 (2024-10-22)
10
+
11
+ ### Fix
12
+
13
+ * Make 'requires=' also accept list[Validator] or a single Validator/Callable ([`a4a7c00`](https://github.com/trialandsuccess/TypeDAL/commit/a4a7c002186f8824971987f96d573fe455dcd01d))
14
+
5
15
  ## v3.8.0 (2024-10-11)
6
16
 
7
17
  ### Feature
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 3.8.0
3
+ Version: 3.8.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,7 +5,8 @@ from decimal import Decimal
5
5
  import datetime as dt
6
6
 
7
7
  from src.typedal.fields import TextField
8
- from typedal.helpers import utcnow
8
+ from src.typedal.helpers import utcnow
9
+ from pydal.validators import IS_NOT_EMPTY
9
10
 
10
11
  db = TypeDAL("sqlite:memory")
11
12
 
@@ -16,7 +17,7 @@ db = TypeDAL("sqlite:memory")
16
17
  class Person(TypedTable):
17
18
  name: TypedField[str]
18
19
 
19
- age = TypedField(int, default=18)
20
+ age = TypedField(int, default=18, requires=IS_NOT_EMPTY())
20
21
  nicknames: list[str]
21
22
 
22
23
  ts = TypedField(dt.datetime, type="timestamp")
@@ -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.8.0"
8
+ __version__ = "3.8.2"
@@ -279,9 +279,10 @@ def relationship(_type: To_Type, condition: Condition = None, join: JOIN_OPTIONS
279
279
  )
280
280
 
281
281
 
282
- def _generate_relationship_condition(
283
- _: Type["TypedTable"], key: str, field: typing.Union["TypedField[Any]", "Table", Type["TypedTable"]]
284
- ) -> Condition:
282
+ T_Field: typing.TypeAlias = typing.Union["TypedField[Any]", "Table", Type["TypedTable"]]
283
+
284
+
285
+ def _generate_relationship_condition(_: Type["TypedTable"], key: str, field: T_Field) -> Condition:
285
286
  origin = typing.get_origin(field)
286
287
  # else: generic
287
288
 
@@ -299,7 +300,7 @@ def _generate_relationship_condition(
299
300
  def to_relationship(
300
301
  cls: Type["TypedTable"] | type[Any],
301
302
  key: str,
302
- field: typing.Union["TypedField[Any]", "Table", Type["TypedTable"]],
303
+ field: T_Field,
303
304
  ) -> typing.Optional[Relationship[Any]]:
304
305
  """
305
306
  Used to automatically create relationship instance for reference fields.
@@ -317,9 +318,14 @@ def to_relationship(
317
318
  Also works for list:reference (list[OtherTable]) and TypedField[OtherTable].
318
319
  """
319
320
  if looks_like(field, TypedField):
321
+ # typing.get_args works for list[str] but not for TypedField[role] :(
320
322
  if args := typing.get_args(field):
323
+ # TypedField[SomeType] -> SomeType
321
324
  field = args[0]
322
- else:
325
+ elif hasattr(field, "_type"):
326
+ # TypedField(SomeType) -> SomeType
327
+ field = typing.cast(T_Field, field._type)
328
+ else: # pragma: no cover
323
329
  # weird
324
330
  return None
325
331
 
@@ -543,13 +549,16 @@ class TypeDAL(pydal.DAL): # type: ignore
543
549
  ]
544
550
 
545
551
  # add implicit relationships:
546
- # User; list[User]; TypedField[User]; TypedField[list[User]]
552
+ # User; list[User]; TypedField[User]; TypedField[list[User]]; TypedField(User); TypedField(list[User])
547
553
  relationships |= {
548
554
  k: new_relationship
549
555
  for k in reference_field_keys
550
556
  if k not in relationships and (new_relationship := to_relationship(cls, k, annotations[k]))
551
557
  }
552
558
 
559
+ # fixme: list[Reference] is recognized as relationship,
560
+ # TypedField(list[Reference]) is NOT recognized!!!
561
+
553
562
  cache_dependency = self._config.caching and kwargs.pop("cache_dependency", True)
554
563
 
555
564
  table: Table = self.define_table(tablename, *fields.values(), **kwargs)
@@ -10,6 +10,8 @@ from .core import TypeDAL, TypedField, TypedTable
10
10
  from .fields import TextField
11
11
  from .web2py_py4web_shared import AuthUser
12
12
 
13
+ DAL = TypeDAL # export as DAL for compatibility with py4web
14
+
13
15
 
14
16
  class AuthGroup(TypedTable):
15
17
  """
@@ -36,6 +36,14 @@ class Mixin(_TypedTable):
36
36
  ('inconsistent method resolution' or 'metaclass conflicts')
37
37
  """
38
38
 
39
+ __settings__: typing.ClassVar[dict[str, Any]]
40
+
41
+ def __init_subclass__(cls, **kwargs: Any):
42
+ """
43
+ Ensures __settings__ exists for other mixins.
44
+ """
45
+ cls.__settings__ = getattr(cls, "__settings__", None) or {}
46
+
39
47
 
40
48
  class TimestampsMixin(Mixin):
41
49
  """
@@ -102,12 +110,16 @@ class SlugMixin(Mixin):
102
110
  },
103
111
  ) # set via init subclass
104
112
 
105
- def __init_subclass__(cls, slug_field: str = None, slug_suffix_length: int = 0, **kw: Any) -> None:
113
+ def __init_subclass__(
114
+ cls, slug_field: str = None, slug_suffix_length: int = 0, slug_suffix: Optional[int] = None, **kw: Any
115
+ ) -> None:
106
116
  """
107
117
  Bind 'slug field' option to be used later (on_define).
108
118
 
109
119
  You can control the length of the random suffix with the `slug_suffix_length` option (0 is no suffix).
110
120
  """
121
+ super().__init_subclass__(**kw)
122
+
111
123
  # unfortunately, PyCharm and mypy do not recognize/autocomplete/typecheck init subclass (keyword) arguments.
112
124
  if slug_field is None:
113
125
  raise ValueError(
@@ -115,7 +127,7 @@ class SlugMixin(Mixin):
115
127
  "e.g. `class MyClass(TypedTable, SlugMixin, slug_field='title'): ...`"
116
128
  )
117
129
 
118
- if "slug_suffix" in kw:
130
+ if slug_suffix:
119
131
  warnings.warn(
120
132
  "The 'slug_suffix' option is deprecated, use 'slug_suffix_length' instead.",
121
133
  DeprecationWarning,
@@ -123,10 +135,9 @@ class SlugMixin(Mixin):
123
135
 
124
136
  slug_suffix = slug_suffix_length or kw.get("slug_suffix", 0)
125
137
 
126
- cls.__settings__ = {
127
- "slug_field": slug_field,
128
- "slug_suffix": slug_suffix,
129
- }
138
+ # append settings:
139
+ cls.__settings__["slug_field"] = slug_field
140
+ cls.__settings__["slug_suffix"] = slug_suffix
130
141
 
131
142
  @classmethod
132
143
  def __on_define__(cls, db: TypeDAL) -> None:
@@ -281,7 +281,7 @@ class FieldSettings(TypedDict, total=False):
281
281
  length: int
282
282
  default: Any
283
283
  required: bool
284
- requires: list[AnyCallable | Any]
284
+ requires: list[AnyCallable | Any | Validator] | Validator | AnyCallable
285
285
  ondelete: str
286
286
  onupdate: str
287
287
  notnull: bool
@@ -1,24 +1,25 @@
1
+ import datetime as dt
1
2
  import os
2
3
  import shutil
3
4
  import tempfile
4
- from pathlib import Path
5
- import datetime as dt
6
5
  import uuid
6
+ from pathlib import Path
7
7
 
8
- from pydal2sql import generate_sql
9
8
  import pytest
9
+
10
10
  # from contextlib import chdir
11
11
  from contextlib_chdir import chdir
12
+ from pydal2sql import generate_sql
12
13
  from testcontainers.postgres import PostgresContainer
13
14
 
14
- from src.typedal import TypeDAL, TypedTable, TypedField
15
+ from src.typedal import TypeDAL, TypedField, TypedTable
15
16
  from src.typedal.config import (
16
17
  _load_dotenv,
17
18
  _load_toml,
18
19
  expand_env_vars_into_toml_values,
19
20
  load_config,
20
21
  )
21
- from src.typedal.fields import TimestampField, PointField, UUIDField
22
+ from src.typedal.fields import PointField, TimestampField, UUIDField
22
23
 
23
24
  postgres = PostgresContainer(
24
25
  dbname="postgres",
@@ -58,10 +59,12 @@ def test_load_toml(at_temp_dir):
58
59
  base = Path("pyproject.toml")
59
60
  base.write_text("# empty")
60
61
 
61
- assert _load_toml(False) == ("", {})
62
- assert _load_toml(None) == (str(base.resolve().absolute()), {})
63
- assert _load_toml(str(base)) == ("pyproject.toml", {})
64
- assert _load_toml(".") == (str(base.resolve().absolute()), {})
62
+ with pytest.warns(UserWarning):
63
+ # it should warn because the toml is empty
64
+ assert _load_toml(False) == ("", {})
65
+ assert _load_toml(None) == (str(base.resolve().absolute()), {})
66
+ assert _load_toml(str(base)) == ("pyproject.toml", {})
67
+ assert _load_toml(".") == (str(base.resolve().absolute()), {})
65
68
 
66
69
 
67
70
  def test_load_dotenv(at_temp_dir):
@@ -160,6 +163,7 @@ def test_expand_env_vars():
160
163
 
161
164
  # note: these are not really 'config' specific but we already have access to postgres here so good enough:
162
165
 
166
+
163
167
  def test_timestamp_fields_sqlite(at_temp_dir):
164
168
  db = TypeDAL("sqlite:memory")
165
169
 
@@ -11,6 +11,9 @@ from src.typedal.helpers import (
11
11
  all_annotations,
12
12
  as_lambda,
13
13
  extract_type_optional,
14
+ get_db,
15
+ get_field,
16
+ get_table,
14
17
  instanciate,
15
18
  is_union,
16
19
  looks_like,
@@ -18,7 +21,7 @@ from src.typedal.helpers import (
18
21
  mktable,
19
22
  origin_is_subclass,
20
23
  to_snake,
21
- unwrap_type, get_db, get_table, get_field,
24
+ unwrap_type,
22
25
  )
23
26
  from typedal import TypeDAL, TypedTable
24
27
  from typedal.types import Field
@@ -202,7 +205,5 @@ def test_get_functions():
202
205
  assert isinstance(table, pydal.objects.Table)
203
206
  assert not isinstance(table, TypedTable)
204
207
  field = get_field(TestGetFunctions.string)
205
- print(
206
- type(field)
207
- )
208
+ print(type(field))
208
209
  assert isinstance(field, Field)
@@ -1,11 +1,13 @@
1
1
  import time
2
+ import uuid
2
3
  from datetime import datetime
3
4
  from typing import Optional
4
5
 
5
6
  import pytest
6
7
 
7
8
  from src.typedal import TypeDAL, TypedTable
8
- from src.typedal.mixins import SlugMixin, TimestampsMixin
9
+ from src.typedal.fields import StringField, TypedField, UUIDField
10
+ from src.typedal.mixins import Mixin, SlugMixin, TimestampsMixin
9
11
 
10
12
 
11
13
  class AllMixins(TypedTable, SlugMixin, TimestampsMixin, slug_field="name"):
@@ -93,3 +95,35 @@ def test_reusing(db):
93
95
 
94
96
  assert str(TableWithTimestamps.created_at) == "table_with_timestamps.created_at"
95
97
  assert str(TableWithTimestamps.unrelated) == "table_with_timestamps.unrelated"
98
+
99
+
100
+ def test_combining_mixins():
101
+ class FirstMixin(Mixin):
102
+ def __init_subclass__(cls, first: str, **kw):
103
+ super().__init_subclass__(**kw)
104
+
105
+ cls.__settings__["first"] = first
106
+
107
+ @classmethod
108
+ def one(cls):
109
+ return cls.__settings__["first"] == "first" and cls.__settings__["second"] == "second"
110
+
111
+ class SecondMixin(Mixin):
112
+ def __init_subclass__(cls, second: str, **kw):
113
+ super().__init_subclass__(**kw)
114
+
115
+ cls.__settings__["second"] = second
116
+
117
+ @classmethod
118
+ def two(cls):
119
+ return cls.__settings__["first"] == "first" and cls.__settings__["second"] == "second"
120
+
121
+ class Combined(TypedTable, FirstMixin, SecondMixin, first="first", second="second"): ...
122
+
123
+ assert Combined.one()
124
+ assert Combined.two()
125
+
126
+ class CombinedDifferentOrder(TypedTable, SecondMixin, FirstMixin, first="first", second="second"): ...
127
+
128
+ assert CombinedDifferentOrder.one()
129
+ assert CombinedDifferentOrder.two()
@@ -45,6 +45,8 @@ class User(TypedTable, TaggableMixin):
45
45
  gid = TypedField(str, default=uuid4)
46
46
  name: str
47
47
  roles: TypedField[list[Role]]
48
+ main_role = TypedField(Role)
49
+ extra_roles = TypedField(list[Role])
48
50
 
49
51
  # relationships:
50
52
  articles = relationship(list["Article"], lambda self, other: other.author == self.id)
@@ -102,9 +104,9 @@ def _setup_data():
102
104
 
103
105
  reader, writer, editor = User.bulk_insert(
104
106
  [
105
- {"name": "Reader 1", "roles": [reader]},
106
- {"name": "Writer 1", "roles": [reader, writer]},
107
- {"name": "Editor 1", "roles": [reader, writer, editor]},
107
+ {"name": "Reader 1", "roles": [reader], "main_role": reader, "extra_roles": []},
108
+ {"name": "Writer 1", "roles": [reader, writer], "main_role": writer, "extra_roles": []},
109
+ {"name": "Editor 1", "roles": [reader, writer, editor], "main_role": editor, "extra_roles": []},
108
110
  ]
109
111
  )
110
112
 
@@ -333,9 +335,22 @@ class CacheTwoRelationships(TypedTable):
333
335
  second: NoCacheSecond
334
336
 
335
337
 
336
- def test_caching():
337
- _setup_data()
338
+ def test_relationship_detection():
339
+ user_table_relationships = User.get_relationships()
340
+
341
+ assert user_table_relationships["roles"]
342
+ assert user_table_relationships["main_role"]
343
+ assert user_table_relationships["extra_roles"]
344
+ assert user_table_relationships["articles"]
345
+ assert user_table_relationships["bestie"]
346
+ assert user_table_relationships["tags"]
338
347
 
348
+ assert user_table_relationships["roles"].join == "left"
349
+ assert user_table_relationships["main_role"].join == "inner"
350
+ assert user_table_relationships["extra_roles"].join == "left"
351
+
352
+
353
+ def test_caching():
339
354
  uncached = User.join().collect_or_fail()
340
355
  cached = User.cache().join().collect_or_fail() # not actually cached yet!
341
356
  cached_user_only = User.join().cache(User.id).collect_or_fail() # idem
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