TypeDAL 4.0.0__tar.gz → 4.0.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 (65) hide show
  1. {typedal-4.0.0 → typedal-4.0.2}/CHANGELOG.md +12 -0
  2. {typedal-4.0.0 → typedal-4.0.2}/PKG-INFO +1 -1
  3. {typedal-4.0.0 → typedal-4.0.2}/pyproject.toml +2 -2
  4. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/__about__.py +1 -1
  5. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/__init__.py +2 -1
  6. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/cli.py +1 -2
  7. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/query_builder.py +1 -1
  8. {typedal-4.0.0 → typedal-4.0.2}/tests/test_helpers.py +2 -1
  9. {typedal-4.0.0 → typedal-4.0.2}/tests/test_relationships.py +129 -22
  10. {typedal-4.0.0 → typedal-4.0.2}/.github/workflows/su6.yml +0 -0
  11. {typedal-4.0.0 → typedal-4.0.2}/.gitignore +0 -0
  12. {typedal-4.0.0 → typedal-4.0.2}/.readthedocs.yml +0 -0
  13. {typedal-4.0.0 → typedal-4.0.2}/README.md +0 -0
  14. {typedal-4.0.0 → typedal-4.0.2}/coverage.svg +0 -0
  15. {typedal-4.0.0 → typedal-4.0.2}/docs/1_getting_started.md +0 -0
  16. {typedal-4.0.0 → typedal-4.0.2}/docs/2_defining_tables.md +0 -0
  17. {typedal-4.0.0 → typedal-4.0.2}/docs/3_building_queries.md +0 -0
  18. {typedal-4.0.0 → typedal-4.0.2}/docs/4_relationships.md +0 -0
  19. {typedal-4.0.0 → typedal-4.0.2}/docs/5_py4web.md +0 -0
  20. {typedal-4.0.0 → typedal-4.0.2}/docs/6_migrations.md +0 -0
  21. {typedal-4.0.0 → typedal-4.0.2}/docs/7_mixins.md +0 -0
  22. {typedal-4.0.0 → typedal-4.0.2}/docs/css/code_blocks.css +0 -0
  23. {typedal-4.0.0 → typedal-4.0.2}/docs/index.md +0 -0
  24. {typedal-4.0.0 → typedal-4.0.2}/docs/requirements.txt +0 -0
  25. {typedal-4.0.0 → typedal-4.0.2}/example_new.py +0 -0
  26. {typedal-4.0.0 → typedal-4.0.2}/example_old.py +0 -0
  27. {typedal-4.0.0 → typedal-4.0.2}/mkdocs.yml +0 -0
  28. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/caching.py +0 -0
  29. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/config.py +0 -0
  30. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/constants.py +0 -0
  31. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/core.py +0 -0
  32. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/define.py +0 -0
  33. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/fields.py +0 -0
  34. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/for_py4web.py +0 -0
  35. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/for_web2py.py +0 -0
  36. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/helpers.py +0 -0
  37. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/mixins.py +0 -0
  38. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/py.typed +0 -0
  39. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/relationships.py +0 -0
  40. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/rows.py +0 -0
  41. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/serializers/as_json.py +0 -0
  42. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/tables.py +0 -0
  43. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/types.py +0 -0
  44. {typedal-4.0.0 → typedal-4.0.2}/src/typedal/web2py_py4web_shared.py +0 -0
  45. {typedal-4.0.0 → typedal-4.0.2}/tests/__init__.py +0 -0
  46. {typedal-4.0.0 → typedal-4.0.2}/tests/configs/simple.toml +0 -0
  47. {typedal-4.0.0 → typedal-4.0.2}/tests/configs/valid.env +0 -0
  48. {typedal-4.0.0 → typedal-4.0.2}/tests/configs/valid.toml +0 -0
  49. {typedal-4.0.0 → typedal-4.0.2}/tests/py314_tests.py +0 -0
  50. {typedal-4.0.0 → typedal-4.0.2}/tests/test_cli.py +0 -0
  51. {typedal-4.0.0 → typedal-4.0.2}/tests/test_config.py +0 -0
  52. {typedal-4.0.0 → typedal-4.0.2}/tests/test_docs_examples.py +0 -0
  53. {typedal-4.0.0 → typedal-4.0.2}/tests/test_json.py +0 -0
  54. {typedal-4.0.0 → typedal-4.0.2}/tests/test_main.py +1 -1
  55. {typedal-4.0.0 → typedal-4.0.2}/tests/test_mixins.py +0 -0
  56. {typedal-4.0.0 → typedal-4.0.2}/tests/test_mypy.py +0 -0
  57. {typedal-4.0.0 → typedal-4.0.2}/tests/test_orm.py +0 -0
  58. {typedal-4.0.0 → typedal-4.0.2}/tests/test_py4web.py +0 -0
  59. {typedal-4.0.0 → typedal-4.0.2}/tests/test_query_builder.py +0 -0
  60. {typedal-4.0.0 → typedal-4.0.2}/tests/test_row.py +0 -0
  61. {typedal-4.0.0 → typedal-4.0.2}/tests/test_stats.py +0 -0
  62. {typedal-4.0.0 → typedal-4.0.2}/tests/test_table.py +0 -0
  63. {typedal-4.0.0 → typedal-4.0.2}/tests/test_web2py.py +0 -0
  64. {typedal-4.0.0 → typedal-4.0.2}/tests/test_xx_others.py +0 -0
  65. {typedal-4.0.0 → typedal-4.0.2}/tests/timings.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.0.2 (2025-10-14)
6
+
7
+ ### Fix
8
+
9
+ * Resolve nested join bug where shared foreign key relationships weren't loaded correctly ([`b093454`](https://github.com/trialandsuccess/TypeDAL/commit/b09345410576cf0a61c0395d91b03d60d2c056a2))
10
+
11
+ ## v4.0.1 (2025-10-13)
12
+
13
+ ### Fix
14
+
15
+ * Export 'PaginatedRows' ([`977a83b`](https://github.com/trialandsuccess/TypeDAL/commit/977a83b30e21412893fe24434b86d7f40ef77e87))
16
+
5
17
  ## v4.0.0 (2025-10-12)
6
18
 
7
19
  ### Feature
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.0.0
3
+ Version: 4.0.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
@@ -118,7 +118,7 @@ badge = true
118
118
  mypy = "--disable-error-code misc"
119
119
 
120
120
  [tool.black]
121
- target-version = ["py313"]
121
+ target-version = ["py314"]
122
122
  line-length = 120
123
123
  # 'extend-exclude' excludes files or directories in addition to the defaults
124
124
  extend-exclude = '''
@@ -161,7 +161,7 @@ strict = true
161
161
  exclude = ["venv", ".bak"]
162
162
 
163
163
  [tool.ruff]
164
- target-version = "py313"
164
+ target-version = "py314"
165
165
  line-length = 120
166
166
 
167
167
  extend-exclude = ["*.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.0.0"
8
+ __version__ = "4.0.2"
@@ -7,7 +7,7 @@ from .fields import TypedField
7
7
  from .helpers import sql_expression
8
8
  from .query_builder import QueryBuilder
9
9
  from .relationships import Relationship, relationship
10
- from .rows import TypedRows
10
+ from .rows import PaginatedRows, TypedRows
11
11
  from .tables import TypedTable
12
12
 
13
13
  from . import fields # isort: skip
@@ -18,6 +18,7 @@ except ImportError: # pragma: no cover
18
18
  P4W_DAL = None # type: ignore
19
19
 
20
20
  __all__ = [
21
+ "PaginatedRows",
21
22
  "QueryBuilder",
22
23
  "Relationship",
23
24
  "TypeDAL",
@@ -392,8 +392,7 @@ def fake_migrations(
392
392
 
393
393
  previously_migrated = (
394
394
  db(
395
- db.ewh_implemented_features.name.belongs(to_fake)
396
- & (db.ewh_implemented_features.installed == True) # noqa E712
395
+ db.ewh_implemented_features.name.belongs(to_fake) & (db.ewh_implemented_features.installed == True) # noqa E712
397
396
  )
398
397
  .select(db.ewh_implemented_features.name)
399
398
  .column("name")
@@ -894,7 +894,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
894
894
  column=nested_col,
895
895
  relation=nested_relation,
896
896
  parent_record=instance,
897
- parent_id=parent_id,
897
+ parent_id=instance.id,
898
898
  seen_relations=seen_relations,
899
899
  db=db,
900
900
  path=path,
@@ -1,3 +1,4 @@
1
+ import datetime as dt
1
2
  import sys
2
3
  import typing
3
4
 
@@ -27,7 +28,6 @@ from src.typedal.helpers import (
27
28
  )
28
29
  from src.typedal.types import Field
29
30
 
30
- import datetime as dt
31
31
 
32
32
  def test_is_union():
33
33
  assert is_union(int | str)
@@ -267,6 +267,7 @@ def test_sql_expression():
267
267
  assert str(database.sql_expression("LOWER(%s)", TestSqlExpression.value)) == 'LOWER("test_sql_expression"."value")'
268
268
  assert str(database.sql_expression("LOWER(%s.value)", TestSqlExpression)) == 'LOWER("test_sql_expression".value)'
269
269
 
270
+
270
271
  @pytest.mark.skipif(not SYSTEM_SUPPORTS_TEMPLATES, reason="t-strings contain breaking syntax!")
271
272
  def test_sql_expression_314():
272
273
  from .py314_tests import test_sql_expression_314
@@ -17,9 +17,6 @@ from src.typedal.caching import (
17
17
  db = TypeDAL("sqlite:memory")
18
18
 
19
19
 
20
- # db = TypeDAL("sqlite://debug.db")
21
-
22
-
23
20
  class TaggableMixin:
24
21
  tags = relationship(
25
22
  list["Tag"],
@@ -87,8 +84,7 @@ class Tagged(TypedTable): # pivot table
87
84
 
88
85
 
89
86
  @db.define()
90
- class Empty(TypedTable):
91
- ...
87
+ class Empty(TypedTable): ...
92
88
 
93
89
 
94
90
  def _setup_data():
@@ -297,8 +293,7 @@ def test_typedal_way():
297
293
  author1 = User.where(id=4).join().first()
298
294
 
299
295
  assert (
300
- len(author1.as_dict()["articles"]) == len(author1.__dict__["articles"]) == len(
301
- dict(author1)["articles"]) == 2
296
+ len(author1.as_dict()["articles"]) == len(author1.__dict__["articles"]) == len(dict(author1)["articles"]) == 2
302
297
  )
303
298
 
304
299
 
@@ -386,7 +381,9 @@ def test_join_with_different_condition():
386
381
  assert role_with_users.users[0].name == "Reader 1"
387
382
 
388
383
  role_with_users = Role.join(
389
- "users", method="inner", condition_and=lambda role, user: ~user.name.like("Reader%"),
384
+ "users",
385
+ method="inner",
386
+ condition_and=lambda role, user: ~user.name.like("Reader%"),
390
387
  ).first()
391
388
 
392
389
  assert role_with_users.users
@@ -394,7 +391,9 @@ def test_join_with_different_condition():
394
391
 
395
392
  # left:
396
393
  role_with_users = Role.join(
397
- "users", method="left", condition_and=lambda role, user: ~user.name.like("Reader%"),
394
+ "users",
395
+ method="left",
396
+ condition_and=lambda role, user: ~user.name.like("Reader%"),
398
397
  ).first()
399
398
 
400
399
  assert role_with_users.users
@@ -431,12 +430,12 @@ def test_caching():
431
430
  cached_user_only2 = User.join().cache(User.id).collect_or_fail()
432
431
 
433
432
  assert (
434
- len(uncached2)
435
- == len(uncached)
436
- == len(cached2)
437
- == len(cached)
438
- == len(cached_user_only2)
439
- == len(cached_user_only)
433
+ len(uncached2)
434
+ == len(uncached)
435
+ == len(cached2)
436
+ == len(cached)
437
+ == len(cached_user_only2)
438
+ == len(cached_user_only)
440
439
  )
441
440
 
442
441
  assert uncached.as_json() == uncached2.as_json() == cached.as_json() == cached2.as_json()
@@ -444,9 +443,9 @@ def test_caching():
444
443
  assert cached.first().gid == cached2.first().gid
445
444
 
446
445
  assert (
447
- [_.name for _ in uncached2.first().roles]
448
- == [_.name for _ in cached.first().roles]
449
- == [_.name for _ in cached2.first().roles]
446
+ [_.name for _ in uncached2.first().roles]
447
+ == [_.name for _ in cached.first().roles]
448
+ == [_.name for _ in cached2.first().roles]
450
449
  )
451
450
 
452
451
  assert not uncached2.metadata.get("cache", {}).get("enabled")
@@ -584,6 +583,7 @@ def test_caching_dependencies():
584
583
 
585
584
  def test_illegal():
586
585
  with pytest.raises(ValueError), pytest.warns(UserWarning):
586
+
587
587
  class HasRelationship:
588
588
  something = relationship("...", condition=lambda: 1, on=lambda: 2)
589
589
 
@@ -667,9 +667,10 @@ def test_nested_relationships():
667
667
 
668
668
  assert old_besties == new_besties == {"Editor 1": "-", "Reader 1": "Reader's Bestie", "Writer 1": "-"}
669
669
 
670
-
671
670
  # more complex:
672
- role = Role.where(name="reader").join("users.bestie", "users.articles.final_editor", "users.articles.secondary_author")
671
+ role = Role.where(name="reader").join(
672
+ "users.bestie", "users.articles.final_editor", "users.articles.secondary_author"
673
+ )
673
674
 
674
675
  nested_article = role.first().users[2].articles[0]
675
676
 
@@ -679,7 +680,113 @@ def test_nested_relationships():
679
680
  assert not nested_article.final_editor
680
681
 
681
682
  # complex, inner:
682
- role_inner = Role.where(name="reader").join("users.bestie", "users.articles.final_editor", "users.articles.secondary_author", method="inner")
683
+ role_inner = Role.where(name="reader").join(
684
+ "users.bestie", "users.articles.final_editor", "users.articles.secondary_author", method="inner"
685
+ )
683
686
 
684
687
  # no final_editor -> inner join should fail:
685
- assert not role_inner.first()
688
+ assert not role_inner.first()
689
+
690
+
691
+ """
692
+ In production I noticed this bug:
693
+
694
+ When joining nested data like `process = Process.join("pros_and_cons.product")`
695
+ process.pros_and_cons[0].product # is fine
696
+ process.pros_and_cons[1].product # is None
697
+ while they both share the same `product_gid`
698
+ This indicates that something goes wrong when collecting the nested data.
699
+ `pros_and_cons` is very specific to that production use-case so please think of a more general example and write a test for it.
700
+ 1. define the required tables and relationships
701
+ 2. define the required data
702
+ 3. perform the query and check the results
703
+ """
704
+
705
+
706
+ class City(TypedTable):
707
+ gid = TypedField(str, default=uuid4)
708
+ name: str
709
+
710
+
711
+ class Office(TypedTable):
712
+ gid = TypedField(str, default=uuid4)
713
+ address: str
714
+ city_id: City
715
+ company: "Company"
716
+
717
+ city_alternative = relationship(City, lambda office, city: office.city_id == city.id)
718
+
719
+
720
+ class Company(TypedTable):
721
+ name: str
722
+
723
+ offices = relationship(list[Office], lambda self, other: other.company == self.id)
724
+
725
+
726
+ def test_nested_join_with_shared_foreign_key():
727
+ """
728
+ Test that nested joins properly load relationships for all items,
729
+ especially when multiple items share the same foreign key value.
730
+
731
+ Bug: When joining nested data like company.offices[0].city works
732
+ but company.offices[1].city is None even though both offices
733
+ reference the same city.
734
+ """
735
+ db.define(City)
736
+ db.define(Company)
737
+ db.define(Office)
738
+
739
+ # Create a city
740
+ san_francisco = City.insert(name="San Francisco")
741
+
742
+ # Create a company
743
+ tech_corp = Company.insert(name="Tech Corp")
744
+
745
+ # Create multiple offices in the same city for the same company
746
+ office1 = Office.insert(address="123 Market St", city_id=san_francisco.id, company=tech_corp.id)
747
+ office2 = Office.insert(address="456 Mission St", city_id=san_francisco.id, company=tech_corp.id)
748
+ office3 = Office.insert(address="789 Howard St", city_id=san_francisco.id, company=tech_corp.id)
749
+
750
+ db.commit()
751
+
752
+ # 1: direct Reference (not a Relationship)
753
+
754
+ # Query company with nested join to offices and their cities
755
+ company = Company.where(id=tech_corp.id).join("offices.city_id").first_or_fail()
756
+
757
+ # All offices should be loaded
758
+ assert len(company.offices) == 3, f"Expected 3 offices, got {len(company.offices)}"
759
+
760
+ # BUG TEST: All offices should have their city relationship loaded
761
+ # Not just the first one
762
+ for idx, office in enumerate(company.offices, 1):
763
+ assert office.city_id is not None, f"Office {idx} ('{office.address}'): city relationship is None (BUG!)"
764
+ assert isinstance(office.city_id, City), (
765
+ f"Office {idx}: city ({office.city_id}) is not a City instance but a {type(office.city_id)}"
766
+ )
767
+ assert office.city_id.name == "San Francisco", (
768
+ f"Office {idx}: expected 'San Francisco', got '{office.city_id.name}'"
769
+ )
770
+ assert office.city_id.id == san_francisco.id, f"Office {idx}: city id mismatch"
771
+
772
+ # 2. alternative: city_alternative (Relationship)
773
+
774
+ # Query company with nested join to offices and their cities
775
+ company = Company.where(id=tech_corp.id).join("offices.city_alternative").first_or_fail()
776
+
777
+ # All offices should be loaded
778
+ assert len(company.offices) == 3, f"Expected 3 offices, got {len(company.offices)}"
779
+
780
+ # BUG TEST: All offices should have their city relationship loaded
781
+ # Not just the first one
782
+ for idx, office in enumerate(company.offices, 1):
783
+ assert office.city_alternative is not None, (
784
+ f"Office {idx} ('{office.address}'): city relationship is None (BUG!)"
785
+ )
786
+ assert isinstance(office.city_alternative, City), (
787
+ f"Office {idx}: city ({office.city_alternative}) is not a City instance but a {type(office.city_alternative)}"
788
+ )
789
+ assert office.city_alternative.name == "San Francisco", (
790
+ f"Office {idx}: expected 'San Francisco', got '{office.city_alternative.name}'"
791
+ )
792
+ assert office.city_alternative.id == san_francisco.id, f"Office {idx}: city id mismatch"
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
@@ -1,6 +1,6 @@
1
1
  import re
2
- import typing
3
2
  import sys
3
+ import typing
4
4
  from copy import copy
5
5
  from sqlite3 import IntegrityError
6
6
  from typing import ForwardRef
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