TypeDAL 4.9.1__tar.gz → 4.9.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 (75) hide show
  1. {typedal-4.9.1 → typedal-4.9.2}/CHANGELOG.md +6 -0
  2. {typedal-4.9.1 → typedal-4.9.2}/PKG-INFO +2 -1
  3. {typedal-4.9.1 → typedal-4.9.2}/pyproject.toml +1 -0
  4. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/__about__.py +1 -1
  5. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/fields.py +14 -0
  6. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/query_builder.py +44 -6
  7. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/types.py +4 -2
  8. typedal-4.9.2/tests/conftest.py +30 -0
  9. {typedal-4.9.1 → typedal-4.9.2}/tests/test_config.py +0 -17
  10. {typedal-4.9.1 → typedal-4.9.2}/tests/test_query_builder.py +44 -1
  11. {typedal-4.9.1 → typedal-4.9.2}/.github/workflows/su6.yml +0 -0
  12. {typedal-4.9.1 → typedal-4.9.2}/.gitignore +0 -0
  13. {typedal-4.9.1 → typedal-4.9.2}/.readthedocs.yml +0 -0
  14. {typedal-4.9.1 → typedal-4.9.2}/README.md +0 -0
  15. {typedal-4.9.1 → typedal-4.9.2}/coverage.svg +0 -0
  16. {typedal-4.9.1 → typedal-4.9.2}/docs/10_advanced_apis.md +0 -0
  17. {typedal-4.9.1 → typedal-4.9.2}/docs/1_getting_started.md +0 -0
  18. {typedal-4.9.1 → typedal-4.9.2}/docs/2_defining_tables.md +0 -0
  19. {typedal-4.9.1 → typedal-4.9.2}/docs/3_building_queries.md +0 -0
  20. {typedal-4.9.1 → typedal-4.9.2}/docs/4_relationships.md +0 -0
  21. {typedal-4.9.1 → typedal-4.9.2}/docs/5_py4web.md +0 -0
  22. {typedal-4.9.1 → typedal-4.9.2}/docs/6_migrations.md +0 -0
  23. {typedal-4.9.1 → typedal-4.9.2}/docs/7_configuration.md +0 -0
  24. {typedal-4.9.1 → typedal-4.9.2}/docs/8_mixins.md +0 -0
  25. {typedal-4.9.1 → typedal-4.9.2}/docs/9_memoization.md +0 -0
  26. {typedal-4.9.1 → typedal-4.9.2}/docs/css/code_blocks.css +0 -0
  27. {typedal-4.9.1 → typedal-4.9.2}/docs/index.md +0 -0
  28. {typedal-4.9.1 → typedal-4.9.2}/docs/requirements.txt +0 -0
  29. {typedal-4.9.1 → typedal-4.9.2}/example_new.py +0 -0
  30. {typedal-4.9.1 → typedal-4.9.2}/example_old.py +0 -0
  31. {typedal-4.9.1 → typedal-4.9.2}/mkdocs.yml +0 -0
  32. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/__init__.py +0 -0
  33. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/caching.py +0 -0
  34. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/cli.py +0 -0
  35. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/config.py +0 -0
  36. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/constants.py +0 -0
  37. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/core.py +0 -0
  38. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/define.py +0 -0
  39. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/enum_helpers.py +0 -0
  40. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/for_py4web.py +0 -0
  41. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/for_web2py.py +0 -0
  42. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/helpers.py +0 -0
  43. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/mixins.py +0 -0
  44. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/py.typed +0 -0
  45. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/relationships.py +0 -0
  46. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/rows.py +0 -0
  47. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/serializers/as_json.py +0 -0
  48. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/serializers/typescript.py +0 -0
  49. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/tables.py +0 -0
  50. {typedal-4.9.1 → typedal-4.9.2}/src/typedal/web2py_py4web_shared.py +0 -0
  51. {typedal-4.9.1 → typedal-4.9.2}/tasks.py +0 -0
  52. {typedal-4.9.1 → typedal-4.9.2}/tests/__init__.py +0 -0
  53. {typedal-4.9.1 → typedal-4.9.2}/tests/configs/simple.toml +0 -0
  54. {typedal-4.9.1 → typedal-4.9.2}/tests/configs/valid.env +0 -0
  55. {typedal-4.9.1 → typedal-4.9.2}/tests/configs/valid.toml +0 -0
  56. {typedal-4.9.1 → typedal-4.9.2}/tests/py314_tests.py +0 -0
  57. {typedal-4.9.1 → typedal-4.9.2}/tests/test_cli.py +0 -0
  58. {typedal-4.9.1 → typedal-4.9.2}/tests/test_docs_examples.py +0 -0
  59. {typedal-4.9.1 → typedal-4.9.2}/tests/test_helpers.py +0 -0
  60. {typedal-4.9.1 → typedal-4.9.2}/tests/test_json.py +0 -0
  61. {typedal-4.9.1 → typedal-4.9.2}/tests/test_main.py +0 -0
  62. {typedal-4.9.1 → typedal-4.9.2}/tests/test_mixins.py +0 -0
  63. {typedal-4.9.1 → typedal-4.9.2}/tests/test_mypy.py +0 -0
  64. {typedal-4.9.1 → typedal-4.9.2}/tests/test_orm.py +0 -0
  65. {typedal-4.9.1 → typedal-4.9.2}/tests/test_py4web.py +0 -0
  66. {typedal-4.9.1 → typedal-4.9.2}/tests/test_relationships.py +0 -0
  67. {typedal-4.9.1 → typedal-4.9.2}/tests/test_row.py +0 -0
  68. {typedal-4.9.1 → typedal-4.9.2}/tests/test_stats.py +0 -0
  69. {typedal-4.9.1 → typedal-4.9.2}/tests/test_table.py +0 -0
  70. {typedal-4.9.1 → typedal-4.9.2}/tests/test_typescript.py +0 -0
  71. {typedal-4.9.1 → typedal-4.9.2}/tests/test_typing_mypy.md +0 -0
  72. {typedal-4.9.1 → typedal-4.9.2}/tests/test_typing_pyright.md +0 -0
  73. {typedal-4.9.1 → typedal-4.9.2}/tests/test_web2py.py +0 -0
  74. {typedal-4.9.1 → typedal-4.9.2}/tests/test_xx_others.py +0 -0
  75. {typedal-4.9.1 → typedal-4.9.2}/tests/timings.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.9.2 (2026-07-02)
6
+
7
+ ### Fix
8
+
9
+ * **query-builder:** Normalize field rnames in select options ([`5fe46d9`](https://github.com/trialandsuccess/TypeDAL/commit/5fe46d9b4acd3081ff6e87cf7f9a45b8f4aa876b))
10
+
5
11
  ## v4.9.1 (2026-07-02)
6
12
 
7
13
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.9.1
3
+ Version: 4.9.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
@@ -43,6 +43,7 @@ Requires-Dist: ewok; extra == 'dev'
43
43
  Requires-Dist: hatch; extra == 'dev'
44
44
  Requires-Dist: mkdocs; extra == 'dev'
45
45
  Requires-Dist: mkdocs-dracula-theme; extra == 'dev'
46
+ Requires-Dist: psycopg2-binary; extra == 'dev'
46
47
  Requires-Dist: pydantic<3; extra == 'dev'
47
48
  Requires-Dist: pyright<1.1.400; extra == 'dev'
48
49
  Requires-Dist: pytest-typing; extra == 'dev'
@@ -97,6 +97,7 @@ dev = [
97
97
  "contextlib-chdir",
98
98
  "testcontainers",
99
99
  "pydantic < 3",
100
+ "psycopg2-binary",
100
101
  # depends on ->
101
102
  "requests<2.32",
102
103
  # mypy:
@@ -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.9.1"
8
+ __version__ = "4.9.2"
@@ -268,6 +268,20 @@ class TypedField[T_Value](Expression): # pragma: no cover
268
268
  return t.cast(Expression, self._field.lower())
269
269
 
270
270
 
271
+ def rname(field: TypedField[t.Any] | Field) -> str:
272
+ """
273
+ Return the full rname (table and field).
274
+ """
275
+
276
+ table = field._table
277
+ inner_field = field._field if isinstance(field, TypedField) else field
278
+
279
+ if not (table and inner_field):
280
+ raise ValueError("missing table or inner field on this 'field'")
281
+
282
+ return "%s.%s" % (table._rname, inner_field._rname)
283
+
284
+
271
285
  def is_typed_field(cls: t.Any) -> t.TypeGuard["TypedField[t.Any]"]:
272
286
  """
273
287
  Is `cls` an instance or subclass of TypedField?
@@ -152,7 +152,22 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
152
152
  """
153
153
  Return a clone of this builder with permission overrides merged in.
154
154
  """
155
- return self._extend(permissions=t.cast(Permissions, permissions))
155
+ return self._extend(permissions=permissions)
156
+
157
+ def _normalize_select_option(
158
+ self, value: str | Field | Expression | bool | t.Iterable[str | Field]
159
+ ) -> str | bool | list[str]:
160
+ # currently only used for 'distinct' since orderby, ... are patched by pydal itself in select()
161
+ if isinstance(value, bool):
162
+ return value
163
+
164
+ if isinstance(value, (list, tuple, set)):
165
+ return list(self._normalize_select_option(val) for val in value)
166
+
167
+ if rname := getattr(value, "_rname", None):
168
+ return str(rname)
169
+
170
+ return str(value)
156
171
 
157
172
  def select(self, *fields: t.Any, **options: t.Unpack[SelectKwargs]) -> "QueryBuilder[T_MetaInstance]":
158
173
  """
@@ -179,6 +194,11 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
179
194
  left: othertable.on(query) - do a LEFT JOIN. Using TypeDAL relationships with .join() is recommended!
180
195
  cache: cache the query result to speed up repeated queries; e.g. (cache=(cache.ram, 3600), cacheable=True)
181
196
  """
197
+
198
+ for key in ("distinct",):
199
+ if options.get(key):
200
+ options[key] = self._normalize_select_option(options[key])
201
+
182
202
  return self._extend(select_args=list(fields), select_kwargs=options)
183
203
 
184
204
  def orderby(self, *fields: OrderBy) -> "QueryBuilder[T_MetaInstance]":
@@ -272,7 +292,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
272
292
  return self._extend(overwrite_query=new_query)
273
293
 
274
294
  def _parse_relationships(
275
- self, fields: t.Iterable[str | t.Type[TypedTable]], method: JOIN_OPTIONS = None, **update: t.Any
295
+ self,
296
+ fields: t.Iterable[str | t.Type[TypedTable]],
297
+ method: JOIN_OPTIONS = None,
298
+ **update: t.Any,
276
299
  ) -> dict[str, Relationship[t.Any]]:
277
300
  """
278
301
  Parse relationship fields into a dict of base relationships with nested relationships.
@@ -766,7 +789,11 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
766
789
  return joins
767
790
 
768
791
  def _build_inner_joins_recursive(
769
- self, relation: Relationship[t.Any], parent_table: t.Type[_TypedTable], key: str, parent_key: str = ""
792
+ self,
793
+ relation: Relationship[t.Any],
794
+ parent_table: t.Type[_TypedTable],
795
+ key: str,
796
+ parent_key: str = "",
770
797
  ) -> list[t.Any]:
771
798
  """Recursively build inner joins for a relationship and its nested relationships."""
772
799
  db = self._get_db()
@@ -876,13 +903,21 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
876
903
  # todo: add additional test, deduplicate
877
904
  nested_key = f"{parent_key}.{nested_name}" if parent_key else f"{key}.{nested_name}"
878
905
  select_args = self._process_relationship_for_left_join(
879
- nested, nested_name, select_args, left_joins, other, nested_key
906
+ nested,
907
+ nested_name,
908
+ select_args,
909
+ left_joins,
910
+ other,
911
+ nested_key,
880
912
  )
881
913
 
882
914
  return select_args
883
915
 
884
916
  def _ensure_relationship_fields(
885
- self, select_args: list[t.Any], other: t.Type[TypedTable], select_fields: str
917
+ self,
918
+ select_args: list[t.Any],
919
+ other: t.Type[TypedTable],
920
+ select_fields: str,
886
921
  ) -> list[t.Any]:
887
922
  """Ensure required fields from relationship table are selected."""
888
923
  if f"{other}." not in select_fields:
@@ -895,7 +930,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
895
930
  return select_args
896
931
 
897
932
  def _update_select_args_with_alias(
898
- self, select_args: list[t.Any], pre_alias: str, other: t.Type[TypedTable]
933
+ self,
934
+ select_args: list[t.Any],
935
+ pre_alias: str,
936
+ other: t.Type[TypedTable],
899
937
  ) -> list[t.Any]:
900
938
  """Update select_args to use aliased table names."""
901
939
  post_alias = str(other).split(" AS ")[-1]
@@ -71,7 +71,7 @@ def merge_permissions(*permission_sets: Permissions | None) -> Permissions:
71
71
 
72
72
  for key in permission_types:
73
73
  if key in permission_set:
74
- merged[key] = merged[key] and bool(permission_set[key])
74
+ merged[key] = merged[key] and bool(permission_set[key]) # type: ignore
75
75
 
76
76
  return t.cast(Permissions, merged)
77
77
 
@@ -188,6 +188,8 @@ class Reference(_Reference):
188
188
  class Field(_Field):
189
189
  """Pydal Field object. Make mypy happy."""
190
190
 
191
+ _rname: str
192
+
191
193
 
192
194
  class Rows(_Rows):
193
195
  """Pydal Rows object. Make mypy happy."""
@@ -283,7 +285,7 @@ class SelectKwargs(t.TypedDict, total=False):
283
285
  groupby: "GroupBy | t.Iterable[GroupBy] | None"
284
286
  having: "Having | None"
285
287
  limitby: t.Optional[tuple[int, int]]
286
- distinct: bool | Field | Expression | str
288
+ distinct: bool | Field | Expression | str | t.Iterable[str]
287
289
  orderby_on_limitby: bool
288
290
  cacheable: bool
289
291
  cache: "CacheTuple"
@@ -0,0 +1,30 @@
1
+ import tempfile
2
+
3
+ import pytest
4
+ from testcontainers.postgres import PostgresContainer
5
+
6
+ from src.typedal import TypeDAL
7
+
8
+ postgres = PostgresContainer(
9
+ dbname="postgres",
10
+ username="someuser",
11
+ password="somepass",
12
+ )
13
+
14
+
15
+ @pytest.fixture(scope="module", autouse=True)
16
+ def psql(request):
17
+ postgres.ports = {
18
+ 5432: 9631, # as set in valid.env
19
+ }
20
+
21
+ request.addfinalizer(postgres.stop)
22
+ postgres.start()
23
+
24
+
25
+ @pytest.fixture
26
+ def dal_psql(psql):
27
+ conn_str = postgres.get_connection_url()
28
+ uri = "postgres://" + conn_str.split("://")[-1]
29
+ with tempfile.TemporaryDirectory() as d:
30
+ yield TypeDAL(uri, attempts=1, migrate=True, enable_typedal_caching=False, folder=d)
@@ -9,7 +9,6 @@ from pathlib import Path
9
9
 
10
10
  import pytest
11
11
  from contextlib_chdir import chdir
12
- from testcontainers.postgres import PostgresContainer
13
12
 
14
13
  from src.typedal import TypeDAL, TypedField, TypedTable
15
14
  from src.typedal.config import (
@@ -20,22 +19,6 @@ from src.typedal.config import (
20
19
  )
21
20
  from src.typedal.fields import PointField, TimestampField, UUIDField
22
21
 
23
- postgres = PostgresContainer(
24
- dbname="postgres",
25
- username="someuser",
26
- password="somepass",
27
- )
28
-
29
-
30
- @pytest.fixture(scope="module", autouse=True)
31
- def psql(request):
32
- postgres.ports = {
33
- 5432: 9631, # as set in valid.env
34
- }
35
-
36
- request.addfinalizer(postgres.stop)
37
- postgres.start()
38
-
39
22
 
40
23
  @pytest.fixture
41
24
  def at_temp_dir():
@@ -1,8 +1,9 @@
1
1
  import pytest
2
- from pydal.objects import Query
2
+ from pydal.objects import Field, Query
3
3
 
4
4
  from src.typedal import TypeDAL, TypedField, TypedTable, relationship
5
5
  from typedal import QueryBuilder
6
+ from typedal.fields import rname
6
7
 
7
8
  db = TypeDAL("sqlite:memory")
8
9
 
@@ -559,6 +560,48 @@ def test_orderby():
559
560
  )
560
561
 
561
562
 
563
+ def test_select_kwargs_use_rname_psql(dal_psql: TypeDAL):
564
+ db = dal_psql
565
+
566
+ @db.define(rname="some_table")
567
+ class Sometable(TypedTable):
568
+ name = TypedField(str, rname="some_name")
569
+
570
+ Sometable.insert(name="B")
571
+ Sometable.insert(name="A")
572
+ Sometable.insert(name="A")
573
+ db.commit()
574
+
575
+ # default orderby/distinct
576
+
577
+ orderby_sql = Sometable.select(orderby=Sometable.name).to_sql().lower()
578
+
579
+ assert "sometable.name" not in orderby_sql
580
+ assert "some_table.some_name" in orderby_sql
581
+
582
+ distinct_sql1 = Sometable.select(Sometable.name, distinct=Sometable.name).to_sql().lower()
583
+ distinct_sql2 = Sometable.select(Sometable.name, distinct=(Sometable.name, Sometable.id)).to_sql().lower()
584
+ distinct_sql3 = Sometable.select(Sometable.name, distinct=("name", "id")).to_sql().lower()
585
+
586
+ for sql in (distinct_sql1, distinct_sql2, distinct_sql3):
587
+ assert "sometable.name" not in sql
588
+ assert "some_table.some_name" in sql
589
+
590
+ builder = Sometable.select(orderby=Sometable.name)
591
+ existing_orderby: Field = builder.select_kwargs.get("orderby")
592
+
593
+ existing_orderby_name = rname(existing_orderby)
594
+ sometable_name = rname(Sometable.name)
595
+ sometable_name_pydal = rname(db.sometable.name)
596
+
597
+ with pytest.raises(ValueError):
598
+ rname(TypedField(str, rname="some_name"))
599
+
600
+ for sql in (existing_orderby_name, sometable_name, sometable_name_pydal):
601
+ assert "sometable.name" not in sql
602
+ assert "some_table.some_name" in sql
603
+
604
+
562
605
  def test_execute():
563
606
  _setup_data()
564
607
 
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