piccolo 1.14.0__py3-none-any.whl → 1.16.0__py3-none-any.whl

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 (43) hide show
  1. piccolo/__init__.py +1 -1
  2. piccolo/apps/user/tables.py +6 -2
  3. piccolo/query/base.py +1 -3
  4. piccolo/query/methods/refresh.py +91 -10
  5. piccolo/query/methods/select.py +3 -0
  6. piccolo/table.py +8 -2
  7. piccolo/testing/test_case.py +120 -0
  8. piccolo/utils/encoding.py +42 -1
  9. {piccolo-1.14.0.dist-info → piccolo-1.16.0.dist-info}/METADATA +1 -1
  10. {piccolo-1.14.0.dist-info → piccolo-1.16.0.dist-info}/RECORD +43 -41
  11. {piccolo-1.14.0.dist-info → piccolo-1.16.0.dist-info}/WHEEL +1 -1
  12. tests/apps/user/test_tables.py +12 -4
  13. tests/columns/m2m/test_m2m.py +7 -0
  14. tests/columns/test_array.py +2 -1
  15. tests/columns/test_bigint.py +2 -1
  16. tests/columns/test_boolean.py +1 -1
  17. tests/columns/test_bytea.py +1 -1
  18. tests/columns/test_choices.py +2 -1
  19. tests/columns/test_date.py +1 -1
  20. tests/columns/test_double_precision.py +1 -1
  21. tests/columns/test_interval.py +1 -1
  22. tests/columns/test_json.py +1 -1
  23. tests/columns/test_jsonb.py +2 -1
  24. tests/columns/test_numeric.py +1 -1
  25. tests/columns/test_primary_key.py +1 -1
  26. tests/columns/test_readable.py +1 -1
  27. tests/columns/test_real.py +1 -1
  28. tests/columns/test_reference.py +1 -1
  29. tests/columns/test_reserved_column_names.py +1 -1
  30. tests/columns/test_smallint.py +2 -2
  31. tests/columns/test_time.py +2 -1
  32. tests/columns/test_timestamp.py +1 -1
  33. tests/columns/test_timestamptz.py +1 -1
  34. tests/columns/test_uuid.py +1 -1
  35. tests/columns/test_varchar.py +2 -2
  36. tests/query/functions/base.py +1 -1
  37. tests/query/functions/test_datetime.py +2 -1
  38. tests/query/functions/test_math.py +1 -1
  39. tests/table/test_refresh.py +218 -7
  40. tests/testing/test_test_case.py +65 -0
  41. {piccolo-1.14.0.dist-info → piccolo-1.16.0.dist-info}/LICENSE +0 -0
  42. {piccolo-1.14.0.dist-info → piccolo-1.16.0.dist-info}/entry_points.txt +0 -0
  43. {piccolo-1.14.0.dist-info → piccolo-1.16.0.dist-info}/top_level.txt +0 -0
piccolo/__init__.py CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "1.14.0"
1
+ __VERSION__ = "1.16.0"
@@ -91,10 +91,14 @@ class BaseUser(Table, tablename="piccolo_user"):
91
91
  raise ValueError("A password must be provided.")
92
92
 
93
93
  if len(password) < cls._min_password_length:
94
- raise ValueError("The password is too short.")
94
+ raise ValueError(
95
+ f"The password is too short. (min {cls._min_password_length})"
96
+ )
95
97
 
96
98
  if len(password) > cls._max_password_length:
97
- raise ValueError("The password is too long.")
99
+ raise ValueError(
100
+ f"The password is too long. (max {cls._max_password_length})"
101
+ )
98
102
 
99
103
  if password.startswith("pbkdf2_sha256"):
100
104
  logger.warning(
piccolo/query/base.py CHANGED
@@ -79,9 +79,7 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
79
79
  if column._alias is not None:
80
80
  json_column_names.append(column._alias)
81
81
  elif len(column._meta.call_chain) > 0:
82
- json_column_names.append(
83
- column._meta.get_default_alias().replace("$", ".")
84
- )
82
+ json_column_names.append(column._meta.get_default_alias())
85
83
  else:
86
84
  json_column_names.append(column._meta.name)
87
85
 
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import typing as t
4
- from dataclasses import dataclass
5
4
 
5
+ from piccolo.utils.encoding import JSONDict
6
6
  from piccolo.utils.sync import run_sync
7
7
 
8
8
  if t.TYPE_CHECKING: # pragma: no cover
@@ -10,7 +10,6 @@ if t.TYPE_CHECKING: # pragma: no cover
10
10
  from piccolo.table import Table
11
11
 
12
12
 
13
- @dataclass
14
13
  class Refresh:
15
14
  """
16
15
  Used to refresh :class:`Table <piccolo.table.Table>` instances with the
@@ -22,11 +21,33 @@ class Refresh:
22
21
  :param columns:
23
22
  Which columns to refresh - it not specified, then all columns are
24
23
  refreshed.
24
+ :param load_json:
25
+ Whether to load ``JSON`` / ``JSONB`` columns as objects, instead of
26
+ just a string.
25
27
 
26
28
  """
27
29
 
28
- instance: Table
29
- columns: t.Optional[t.Sequence[Column]] = None
30
+ def __init__(
31
+ self,
32
+ instance: Table,
33
+ columns: t.Optional[t.Sequence[Column]] = None,
34
+ load_json: bool = False,
35
+ ):
36
+ self.instance = instance
37
+
38
+ if columns:
39
+ for column in columns:
40
+ if len(column._meta.call_chain) > 0:
41
+ raise ValueError(
42
+ "We can't currently selectively refresh certain "
43
+ "columns on child objects (e.g. Concert.band_1.name). "
44
+ "Please just specify top level columns (e.g. "
45
+ "Concert.band_1), and the entire child object will be "
46
+ "refreshed."
47
+ )
48
+
49
+ self.columns = columns
50
+ self.load_json = load_json
30
51
 
31
52
  @property
32
53
  def _columns(self) -> t.Sequence[Column]:
@@ -40,6 +61,63 @@ class Refresh:
40
61
  i for i in self.instance._meta.columns if not i._meta.primary_key
41
62
  ]
42
63
 
64
+ def _get_columns(self, instance: Table, columns: t.Sequence[Column]):
65
+ """
66
+ If `prefetch` was used on the object, for example::
67
+
68
+ >>> await Band.objects(Band.manager)
69
+
70
+ We should also update the prefetched object.
71
+
72
+ It works multiple level deep. If we refresh this::
73
+
74
+ >>> await Album.objects(Album.band.manager).first()
75
+
76
+ It will update the nested `band` object, and also the `manager`
77
+ object.
78
+
79
+ """
80
+ from piccolo.columns.column_types import ForeignKey
81
+ from piccolo.table import Table
82
+
83
+ select_columns = []
84
+
85
+ for column in columns:
86
+ if isinstance(column, ForeignKey) and isinstance(
87
+ (child_instance := getattr(instance, column._meta.name)),
88
+ Table,
89
+ ):
90
+ select_columns.extend(
91
+ self._get_columns(
92
+ child_instance,
93
+ # Fetch all columns (even the primary key, just in
94
+ # case the foreign key now references a different row).
95
+ column.all_columns(),
96
+ )
97
+ )
98
+ else:
99
+ select_columns.append(column)
100
+
101
+ return select_columns
102
+
103
+ def _update_instance(self, instance: Table, data_dict: t.Dict):
104
+ """
105
+ Update the table instance. It is called recursively, if the instance
106
+ has child instances.
107
+ """
108
+ for key, value in data_dict.items():
109
+ if isinstance(value, dict) and not isinstance(value, JSONDict):
110
+ # If the value is a dict, then it's a child instance.
111
+ if all(i is None for i in value.values()):
112
+ # If all values in the nested object are None, then we can
113
+ # safely assume that the object itself is null, as the
114
+ # primary key value must be null.
115
+ setattr(instance, key, None)
116
+ else:
117
+ self._update_instance(getattr(instance, key), value)
118
+ else:
119
+ setattr(instance, key, value)
120
+
43
121
  async def run(
44
122
  self, in_pool: bool = True, node: t.Optional[str] = None
45
123
  ) -> Table:
@@ -54,7 +132,6 @@ class Refresh:
54
132
  Modifies the instance in place, but also returns it as a convenience.
55
133
 
56
134
  """
57
-
58
135
  instance = self.instance
59
136
 
60
137
  if not instance._exists_in_db:
@@ -71,20 +148,24 @@ class Refresh:
71
148
  if not columns:
72
149
  raise ValueError("No columns to fetch.")
73
150
 
74
- updated_values = (
75
- await instance.__class__.select(*columns)
151
+ select_columns = self._get_columns(
152
+ instance=self.instance, columns=columns
153
+ )
154
+
155
+ data_dict = (
156
+ await instance.__class__.select(*select_columns)
76
157
  .where(pk_column == primary_key_value)
158
+ .output(nested=True, load_json=self.load_json)
77
159
  .first()
78
160
  .run(node=node, in_pool=in_pool)
79
161
  )
80
162
 
81
- if updated_values is None:
163
+ if data_dict is None:
82
164
  raise ValueError(
83
165
  "The object doesn't exist in the database any more."
84
166
  )
85
167
 
86
- for key, value in updated_values.items():
87
- setattr(instance, key, value)
168
+ self._update_instance(instance=instance, data_dict=data_dict)
88
169
 
89
170
  return instance
90
171
 
@@ -406,6 +406,9 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
406
406
  def output(self: Self, *, load_json: bool, as_list: bool) -> SelectJSON: # type: ignore # noqa: E501
407
407
  ...
408
408
 
409
+ @t.overload
410
+ def output(self: Self, *, load_json: bool, nested: bool) -> Self: ...
411
+
409
412
  @t.overload
410
413
  def output(self: Self, *, nested: bool) -> Self: ...
411
414
 
piccolo/table.py CHANGED
@@ -541,7 +541,9 @@ class Table(metaclass=TableMetaclass):
541
541
  )
542
542
 
543
543
  def refresh(
544
- self, columns: t.Optional[t.Sequence[Column]] = None
544
+ self,
545
+ columns: t.Optional[t.Sequence[Column]] = None,
546
+ load_json: bool = False,
545
547
  ) -> Refresh:
546
548
  """
547
549
  Used to fetch the latest data for this instance from the database.
@@ -551,6 +553,10 @@ class Table(metaclass=TableMetaclass):
551
553
  If you only want to refresh certain columns, specify them here.
552
554
  Otherwise all columns are refreshed.
553
555
 
556
+ :param load_json:
557
+ Whether to load ``JSON`` / ``JSONB`` columns as objects, instead of
558
+ just a string.
559
+
554
560
  Example usage::
555
561
 
556
562
  # Get an instance from the database.
@@ -564,7 +570,7 @@ class Table(metaclass=TableMetaclass):
564
570
  instance.refresh().run_sync()
565
571
 
566
572
  """
567
- return Refresh(instance=self, columns=columns)
573
+ return Refresh(instance=self, columns=columns, load_json=load_json)
568
574
 
569
575
  @t.overload
570
576
  def get_related(
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+ from unittest import IsolatedAsyncioTestCase, TestCase
5
+
6
+ from piccolo.engine import Engine, engine_finder
7
+ from piccolo.table import (
8
+ Table,
9
+ create_db_tables,
10
+ create_db_tables_sync,
11
+ drop_db_tables,
12
+ drop_db_tables_sync,
13
+ )
14
+
15
+
16
+ class TableTest(TestCase):
17
+ """
18
+ Identical to :class:`AsyncTableTest <piccolo.testing.test_case.AsyncTableTest>`,
19
+ except it only work for sync tests. Only use this if you can't make your
20
+ tests async (perhaps you're on Python 3.7 where ``IsolatedAsyncioTestCase``
21
+ isn't available).
22
+
23
+ For example::
24
+
25
+ class TestBand(TableTest):
26
+ tables = [Band]
27
+
28
+ def test_band(self):
29
+ ...
30
+
31
+ """ # noqa: E501
32
+
33
+ tables: t.List[t.Type[Table]]
34
+
35
+ def setUp(self) -> None:
36
+ create_db_tables_sync(*self.tables)
37
+
38
+ def tearDown(self) -> None:
39
+ drop_db_tables_sync(*self.tables)
40
+
41
+
42
+ class AsyncTableTest(IsolatedAsyncioTestCase):
43
+ """
44
+ Used for tests where we need to create Piccolo tables - they will
45
+ automatically be created and dropped.
46
+
47
+ For example::
48
+
49
+ class TestBand(AsyncTableTest):
50
+ tables = [Band]
51
+
52
+ async def test_band(self):
53
+ ...
54
+
55
+ """
56
+
57
+ tables: t.List[t.Type[Table]]
58
+
59
+ async def asyncSetUp(self) -> None:
60
+ await create_db_tables(*self.tables)
61
+
62
+ async def asyncTearDown(self) -> None:
63
+ await drop_db_tables(*self.tables)
64
+
65
+
66
+ class AsyncTransactionTest(IsolatedAsyncioTestCase):
67
+ """
68
+ Wraps each test in a transaction, which is automatically rolled back when
69
+ the test finishes.
70
+
71
+ .. warning::
72
+ Python 3.11 and above only.
73
+
74
+ If your test suite just contains ``AsyncTransactionTest`` tests, then you
75
+ can setup your database tables once before your test suite runs. Any
76
+ changes made to your tables by the tests will be rolled back automatically.
77
+
78
+ Here's an example::
79
+
80
+ from piccolo.testing.test_case import AsyncTransactionTest
81
+
82
+
83
+ class TestBandEndpoint(AsyncTransactionTest):
84
+
85
+ async def test_band_response(self):
86
+ \"\"\"
87
+ Make sure the endpoint returns a 200.
88
+ \"\"\"
89
+ band = Band({Band.name: "Pythonistas"})
90
+ await band.save()
91
+
92
+ # Using an API testing client, like httpx:
93
+ response = await client.get(f"/bands/{band.id}/")
94
+ self.assertEqual(response.status_code, 200)
95
+
96
+ We add a ``Band`` to the database, but any subsequent tests won't see it,
97
+ as the changes are rolled back automatically.
98
+
99
+ """
100
+
101
+ # We use `engine_finder` to find the current `Engine`, but you can
102
+ # explicity set it here if you prefer:
103
+ #
104
+ # class MyTest(AsyncTransactionTest):
105
+ # db = DB
106
+ #
107
+ # ...
108
+ #
109
+ db: t.Optional[Engine] = None
110
+
111
+ async def asyncSetUp(self) -> None:
112
+ db = self.db or engine_finder()
113
+ assert db is not None
114
+ self.transaction = db.transaction()
115
+ # This is only available in Python 3.11 and above:
116
+ await self.enterAsyncContext(cm=self.transaction) # type: ignore
117
+
118
+ async def asyncTearDown(self):
119
+ await super().asyncTearDown()
120
+ await self.transaction.rollback()
piccolo/utils/encoding.py CHANGED
@@ -29,5 +29,46 @@ def dump_json(data: t.Any, pretty: bool = False) -> str:
29
29
  return json.dumps(data, **params) # type: ignore
30
30
 
31
31
 
32
+ class JSONDict(dict):
33
+ """
34
+ Once we have parsed a JSON string into a dictionary, we can't distinguish
35
+ it from other dictionaries.
36
+
37
+ Sometimes we might want to - for example::
38
+
39
+ >>> await Album.select(
40
+ ... Album.all_columns(),
41
+ ... Album.recording_studio.all_columns()
42
+ ... ).output(
43
+ ... nested=True,
44
+ ... load_json=True
45
+ ... )
46
+
47
+ [{
48
+ 'id': 1,
49
+ 'band': 1,
50
+ 'name': 'Awesome album 1',
51
+ 'recorded_at': {
52
+ 'id': 1,
53
+ 'facilities': {'restaurant': True, 'mixing_desk': True},
54
+ 'name': 'Abbey Road'
55
+ },
56
+ 'release_date': datetime.date(2021, 1, 1)
57
+ }]
58
+
59
+ Facilities could be mistaken for a table.
60
+
61
+ """
62
+
63
+ ...
64
+
65
+
32
66
  def load_json(data: str) -> t.Any:
33
- return orjson.loads(data) if ORJSON else json.loads(data) # type: ignore
67
+ response = (
68
+ orjson.loads(data) if ORJSON else json.loads(data) # type: ignore
69
+ )
70
+
71
+ if isinstance(response, dict):
72
+ return JSONDict(**response)
73
+
74
+ return response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piccolo
3
- Version: 1.14.0
3
+ Version: 1.16.0
4
4
  Summary: A fast, user friendly ORM and query builder which supports asyncio.
5
5
  Home-page: https://github.com/piccolo-orm/piccolo
6
6
  Author: Daniel Townsend
@@ -1,10 +1,10 @@
1
- piccolo/__init__.py,sha256=vQd7XyktulZvg_rtkaOpG-2W02Gb_wFd8aev4i-MHeA,23
1
+ piccolo/__init__.py,sha256=ysxGt_oXPqT-SJ1UbC7NdyUSaoEEi94z0BdzyvNNY2Y,23
2
2
  piccolo/custom_types.py,sha256=7HMQAze-5mieNLfbQ5QgbRQgR2abR7ol0qehv2SqROY,604
3
3
  piccolo/main.py,sha256=1VsFV67FWTUikPTysp64Fmgd9QBVa_9wcwKfwj2UCEA,5117
4
4
  piccolo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  piccolo/querystring.py,sha256=yZdURtiVSlxkkEVJoFKAmL2OfNxrUr8xfsfuBBB7IuY,9662
6
6
  piccolo/schema.py,sha256=qNNy4tG_HqnXR9t3hHMgYXtGxHabwQAhUpc6RKLJ_gE,7960
7
- piccolo/table.py,sha256=IfXT9rtm1sBN-u_A8-_0gj6fJf3_RGSPtWMJa-FX9jw,49569
7
+ piccolo/table.py,sha256=nS3zuhGNPZ4H9s_E3ieFbrE_u1Tr6TenBVw_nrrtmRQ,49766
8
8
  piccolo/table_reflection.py,sha256=jrN1nHerDJ4tU09GtNN3hz7ap-7rXnSUjljFO6LB2H0,7094
9
9
  piccolo/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  piccolo/apps/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -104,7 +104,7 @@ piccolo/apps/tester/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
104
104
  piccolo/apps/tester/commands/run.py,sha256=phFxim2ogARAviW-YT11y9F-L5SJxSioAIepUzQeAWU,2431
105
105
  piccolo/apps/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
106
  piccolo/apps/user/piccolo_app.py,sha256=yfw1J9FEnBWdgGdUdNMnqp06Wzm5_s9TO2r5KLchrLM,842
107
- piccolo/apps/user/tables.py,sha256=ZSXtOC9OANCKC6cq5ZCj3CqRwraibFDwtf1VVIBfmKQ,8954
107
+ piccolo/apps/user/tables.py,sha256=KgPqONdl1SDV-3cGq5aS-za_v_5_TGHQqNFYXF0M3Jw,9082
108
108
  piccolo/apps/user/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
109
109
  piccolo/apps/user/commands/change_password.py,sha256=F7mlhtTUY7uENSK8vds5oibIDJTz7QBQdrl7EB-lF9Q,649
110
110
  piccolo/apps/user/commands/change_permissions.py,sha256=LScsKJUMJqIi54cZ1SgS9Wb356KB0t7smu94FDXLfVk,1463
@@ -146,7 +146,7 @@ piccolo/engine/finder.py,sha256=GjzBNtzRzH79fjtRn7OI3nZiOXE8JfoQWAvHVPrPNx4,507
146
146
  piccolo/engine/postgres.py,sha256=DekL3KafCdzSAEQ6_EgOiUB1ERXh2xpePYwI9QvmN-c,18955
147
147
  piccolo/engine/sqlite.py,sha256=KwJc3UttBP_8qSREbLJshqEfROF17ENf0Ju9BwI5_so,25236
148
148
  piccolo/query/__init__.py,sha256=bcsMV4813rMRAIqGv4DxI4eyO4FmpXkDv9dfTk5pt3A,699
149
- piccolo/query/base.py,sha256=2eyfgqdzl5bKQBRpmxil_HoXpCEDGGrh84q62ZmfBNk,14680
149
+ piccolo/query/base.py,sha256=iI9Fv3oOw7T4ZWZvRKRwdtClvQtSaAepslH24vwxZVA,14616
150
150
  piccolo/query/mixins.py,sha256=EFEFb9It4y1mR6_JXLn139h5M9KgeP750STYy5M4MLs,21951
151
151
  piccolo/query/proxy.py,sha256=Yq4jNc7IWJvdeO3u7_7iPyRy2WhVj8KsIUcIYHBIi9Q,1839
152
152
  piccolo/query/functions/__init__.py,sha256=pZkzOIh7Sg9HPNOeegOwAS46Oxt31ATlSVmwn-lxCbc,605
@@ -168,16 +168,17 @@ piccolo/query/methods/indexes.py,sha256=J-QUqaBJwpgahskUH0Cu0Mq7zEKcfVAtDsUVIVX-
168
168
  piccolo/query/methods/insert.py,sha256=ssLJ_wn08KnOwwr7t-VILyn1P4hrvM63CfPIcAJWT5k,4701
169
169
  piccolo/query/methods/objects.py,sha256=iahDUziUtlx7pJ2uBAhdm3hCTmg2AS9C8cal1my5KR0,11705
170
170
  piccolo/query/methods/raw.py,sha256=VhYpCB52mZk4zqFTsqK5CHKTDGskUjISXTBV7UjohmA,600
171
- piccolo/query/methods/refresh.py,sha256=P1Eo_HYU_L7kcGM_cvDDgyLi1boCXY7Pc4tv_eDAzvc,2769
172
- piccolo/query/methods/select.py,sha256=KDbLRkADM9xNQsoNYN_eNSwapFACQCUNiCvw0B5tmko,21315
171
+ piccolo/query/methods/refresh.py,sha256=wg1zghKfwz-VmqK4uWa4GNMiDtK-skTqow591Hb3ONM,5854
172
+ piccolo/query/methods/select.py,sha256=UH-y2g3Ub7bEowfLObrrhw0W-HepTXWCmuMPhk13roE,21406
173
173
  piccolo/query/methods/table_exists.py,sha256=0yb3n6Jd2ovSBWlZ-gl00K4E7Jnbj7J8qAAX5d7hvNk,1259
174
174
  piccolo/query/methods/update.py,sha256=LfWqIXEl1aecc0rkVssTFmwyD6wXGhlKcTrUVhtlEsw,3705
175
175
  piccolo/testing/__init__.py,sha256=pRFSqRInfx95AakOq54atmvqoB-ue073q2aR8u8zR40,83
176
176
  piccolo/testing/model_builder.py,sha256=lVEiEe71xrH8SSjzFc2l0s-VaCXHeg9Bo5oAYOEbLrI,6545
177
177
  piccolo/testing/random_builder.py,sha256=0LkGpanQ7P1R82gLIMQyK9cm1LdZkPvxbShTEf3jeH4,2128
178
+ piccolo/testing/test_case.py,sha256=JHQCIAeuO6H2ZbcFHQUhlvUWVFjzKoakDlqVYJvYhtg,3281
178
179
  piccolo/utils/__init__.py,sha256=SDFFraauI9Op8dCRkreQv1dwUcab8Mi1eC-n0EwlTy8,36
179
180
  piccolo/utils/dictionary.py,sha256=8vRPxgaXadDVhqihP1UxL7nUBgM6Gpe_Eu3xJq7zzGM,1886
180
- piccolo/utils/encoding.py,sha256=W34oj1F2f8zeirMceHZnAnJL2T8rPoiqXt-DJ-hzRGk,835
181
+ piccolo/utils/encoding.py,sha256=CtSODJOkT3TVHfGlTDXozDsClBCJbGGqluc6_UlJ-7c,1761
181
182
  piccolo/utils/lazy_loader.py,sha256=T8GChEqtKWcBegn-6g_BQ7hOg2Xu1bedFh7Z8E7xcOY,1912
182
183
  piccolo/utils/list.py,sha256=4hPGiksJWxL226W7gyYBcqVGgMTgVa2jP8zvalc3zw8,1541
183
184
  piccolo/utils/naming.py,sha256=d7_mMscguK799RMhxFDifRgn8Ply5wiy2k1KkP22WUs,276
@@ -237,43 +238,43 @@ tests/apps/sql_shell/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
237
238
  tests/apps/sql_shell/commands/test_run.py,sha256=6p0nqCoG_qNLrKeBuHspmer_SrMwEF-vfp9LbPj2W2E,425
238
239
  tests/apps/tester/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
239
240
  tests/apps/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
240
- tests/apps/user/test_tables.py,sha256=E9mGAOiDjVtIJYqZzlvPU_JFz6RZzGAVeE0Lk5VjGb4,9523
241
+ tests/apps/user/test_tables.py,sha256=5ImGAWpIKxlqweGeJRMiVOHaBJxRwSzsp3GZ7x6lzCo,9807
241
242
  tests/apps/user/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
242
243
  tests/apps/user/commands/test_change_password.py,sha256=IEIlnny68jt4UFOaUOwHk9C7WHED51wYoSNX-YIi7rU,1051
243
244
  tests/apps/user/commands/test_change_permissions.py,sha256=uVKEiT1EKot3VA2TDETdQ1hsWL-83rLQrJl4jIxPgqo,2108
244
245
  tests/apps/user/commands/test_create.py,sha256=iJ3Tti62rHwvdcTwNXrc5JPam6vR1qxKRdMN456vm3o,2250
245
246
  tests/apps/user/commands/test_list.py,sha256=ipPfGdW6fH7q-Jc7JcYUvlioGmH9GQU0WImZGC2m-XQ,2840
246
247
  tests/columns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
247
- tests/columns/test_array.py,sha256=QixxML6vRq-PUliqbZSKd_LE5T4fEj5nq5e1NpOhng0,11590
248
+ tests/columns/test_array.py,sha256=niIkAPRQp49bkpbctaMgpcDai4TMAbM9qkrTGhPFsQY,11627
248
249
  tests/columns/test_base.py,sha256=CTqCNcrqAJTjLXe3MCZgTczrmB3jcVRcOpU4FilpLoQ,3918
249
- tests/columns/test_bigint.py,sha256=KqIw6UP3vGs3cyL1I1An8RfXMmjZvfSIuKYhgkqX5zI,1021
250
- tests/columns/test_boolean.py,sha256=dFqyUnzXU8JSflI511Vt3Gxk62lpIVgH37vnDEz0E2M,1611
251
- tests/columns/test_bytea.py,sha256=-XCXTKboPSfowhU7nceRnkhVCJ60Kus52b3f67xAlAo,1367
252
- tests/columns/test_choices.py,sha256=xDWgcguCH4pu8w3JpIcSyurpghPsbc4OQmzjup2iTIw,4029
250
+ tests/columns/test_bigint.py,sha256=KzfiItv0cYQZR35V_TL485AitDnOZQfTzC3C2YCLSn4,1058
251
+ tests/columns/test_boolean.py,sha256=qm0tg-gKcoYHNV1WVFxgbG4CsngxAJlg-hsGeTNklN8,1626
252
+ tests/columns/test_bytea.py,sha256=doN8S1eFVU4ntSXIg4IgMSZcbvqW1WJ-AEm3OjKLGkI,1382
253
+ tests/columns/test_choices.py,sha256=q8TLe7nvGERXyGO_XEryEBR-DuWwFY1jPpscsrXjdXo,4066
253
254
  tests/columns/test_combination.py,sha256=BuBwR7k5X1EkOWraZpjqU6gvtb6ow_k-7N1KQBiW2RA,1681
254
- tests/columns/test_date.py,sha256=WnwtCS81mYsDpwDrMnZRDjHA7ag-bxMPundRxtTlv1A,1015
255
+ tests/columns/test_date.py,sha256=QLC6kJMQwM-1mbUP4ksJVM7P8WwjzGZyynH3rHHdSew,1030
255
256
  tests/columns/test_db_column_name.py,sha256=v0QFOQp_atqzMB1n40simVwHeBDi5nyN1N2bSPX5k6w,7670
256
257
  tests/columns/test_defaults.py,sha256=rwlU1fXt3cCl7C51eLlZXqgWkE-K5W0pHvTrwkAKyCo,2896
257
- tests/columns/test_double_precision.py,sha256=Nx1vgmQqwn1PgBC6KcPO9yHKe9v8lCQXrEA1GcUJIn4,529
258
+ tests/columns/test_double_precision.py,sha256=7rhcSfDkb2fBh_zEG4UGwD_GW1sy6U9-8NooHuCS09Q,544
258
259
  tests/columns/test_get_sql_value.py,sha256=mKgsInN374jzV99y9mg_ZiG-AvnJgz36SZi89xL7RZM,1768
259
- tests/columns/test_interval.py,sha256=R3fyC7IX35neiu64XR7KX58ylpPzeU2zSEbaKdbW4I4,2765
260
- tests/columns/test_json.py,sha256=nbNXo0CISU8w3CjIF7nxBmRfEdenGFmc0o9YfhMvpKg,3467
261
- tests/columns/test_jsonb.py,sha256=Np3FZf3AspTGRWCCMrTYEsw6uTJPI8u3SZkOvH5ERRg,6690
262
- tests/columns/test_numeric.py,sha256=WrilQrWc-_g8DGLUvmllSBL01xqYqDORt3ypplnRizA,736
263
- tests/columns/test_primary_key.py,sha256=hXnTF-kFgrFNjawvJstmR3YQCM59qJ4NWkMD9vRY8FI,4871
264
- tests/columns/test_readable.py,sha256=OFK2n_k-eqRLdjEg1X2aOFPNwhtGDPNBli4DAh3i6vw,743
265
- tests/columns/test_real.py,sha256=bSjACkbnJzrN9dEWVb0UdWqSDfxdGz5HMWc8PUsJUl4,496
266
- tests/columns/test_reference.py,sha256=d7vm7AHinISA-HjlA8zyPCRaAGPtnc5NOXUvK4SeQFg,2329
267
- tests/columns/test_reserved_column_names.py,sha256=BpqN49n_AaiPjaaJ43hLhFXQHb8NV6ig8nvgKvN0Smc,1404
268
- tests/columns/test_smallint.py,sha256=t-LNX7XXy-k5_lhGXwELyC0LB9Iq4WrBVQdvEYf5EaQ,984
269
- tests/columns/test_time.py,sha256=qyG4jjSO_B2DfkjQo3nIff2T201c75EI7M6CYspK5QM,1560
270
- tests/columns/test_timestamp.py,sha256=lkb1pQW41kQ_c7zLjPfls7EgnIMnvG9AK3ePb5izJU0,1671
271
- tests/columns/test_timestamptz.py,sha256=Py27sx45-xQElhuLVxNtZjBxz3t6VJUbyUGW3H4W7eU,2615
272
- tests/columns/test_uuid.py,sha256=ifCyIrO6usYO33WM0FgwuPPhYzs4zVKD3MViIT61J7c,357
273
- tests/columns/test_varchar.py,sha256=Oyt8t7msCfIiZdU9B23zjlV6atFv4sX3iMSDJc565Oc,681
260
+ tests/columns/test_interval.py,sha256=2M18pfoGxLLosEvwTmuC4zQkM6jWwU0Nv2fqViW3xOs,2780
261
+ tests/columns/test_json.py,sha256=_cziJvw2uT8e_4u9lKhmU56lgQeE7bEqCXYf6AzfChA,3482
262
+ tests/columns/test_jsonb.py,sha256=eJQoxpyuQ4yrX-GMJhRkZntyd4tX6M6RhAxYn2-ISII,6727
263
+ tests/columns/test_numeric.py,sha256=AkTvdvjSsfRsMM79tx4AskUpsTizGBLMY_tC2OII9U4,751
264
+ tests/columns/test_primary_key.py,sha256=foNG9eTQUJ5yiEVQ7faIEMycW_VuZ7vgzknYXaZ-QXM,4886
265
+ tests/columns/test_readable.py,sha256=xKVfJuxZcfyncNVKXNryl2WFREX655jwD9DxiLArQiU,758
266
+ tests/columns/test_real.py,sha256=O_lwiNU4RIHSMY33QuWT0WTvNzV-2ATYYtdbI46E42c,511
267
+ tests/columns/test_reference.py,sha256=-pDKZl0kjRHz17U-_bldHD4rpg7Ga27lIzXMKOazbWA,2344
268
+ tests/columns/test_reserved_column_names.py,sha256=S39hq9Ex8QXlbnfjlPLtzwaxICTkXe43Nr2um1S37jI,1419
269
+ tests/columns/test_smallint.py,sha256=5tm8BpyUmun7uIROaKBgSb27dGu-waqtQAAvpvS66ek,1024
270
+ tests/columns/test_time.py,sha256=eQ4S-FMhULvkp6OwQaMbheBgQUVFCJ9JGMsKFpsM_vg,1597
271
+ tests/columns/test_timestamp.py,sha256=vD7F0J_8rZkX_PngfjaMcs5Ugq3lM8GUJXzU7cQLNZM,1686
272
+ tests/columns/test_timestamptz.py,sha256=P7zblPC6Fjjdk6iOhVUGIKnFFzbbUPVNSY98qbuqE7U,2630
273
+ tests/columns/test_uuid.py,sha256=taFYNvRZjQztMPbTQHYtwQutvcLnKPt6_aUxsf2o04Q,372
274
+ tests/columns/test_varchar.py,sha256=fbwBdimHoGaylfrqkFIgQ5m2q80umSoUNHIwofM6j_c,721
274
275
  tests/columns/m2m/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
275
276
  tests/columns/m2m/base.py,sha256=uH92ECQuY5AjpQXPySwrlruZPZzB4LH2V2FUSXmHRLQ,14563
276
- tests/columns/m2m/test_m2m.py,sha256=LtNsHQ8xAzBFLiZVZhWEB56zu25FnaWtzJ62FZH3heI,12647
277
+ tests/columns/m2m/test_m2m.py,sha256=0ObmIHUJF6CZoNBznc5xXVr5_BbGBqOmWwtpg8IcPt4,13055
277
278
  tests/columns/m2m/test_m2m_schema.py,sha256=oxu7eAjFFpDjnq9Eq-5OTNmlnsEIMFWx18OItfpVs-s,339
278
279
  tests/conf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
279
280
  tests/conf/example.py,sha256=K8sTttLpEac8rQlOLDY500IGkHj3P3NoyFbCMnT1EqY,347
@@ -303,10 +304,10 @@ tests/query/test_gather.py,sha256=okWANrBoh0Ut1RomWoffiWNpFqiITF6qti-Aa3uYtRk,73
303
304
  tests/query/test_querystring.py,sha256=QrqyjwUlFlf5LrsJ7DgjCruq811I0UvrDFPud6rfZNI,5019
304
305
  tests/query/test_slots.py,sha256=I9ZjAYqAJNSFAWg9UyAqy7bm-Z52KiyQ2C_yHk2qqqI,1010
305
306
  tests/query/functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
306
- tests/query/functions/base.py,sha256=sCIolQlvOMlnrLJJBAfJV-Tg8dCzkGnCs83JqRQpKkk,500
307
- tests/query/functions/test_datetime.py,sha256=247Vpbyuq0s-SU-883t6W5ghF8xiz44LzdHd2nJ0mZ4,2963
307
+ tests/query/functions/base.py,sha256=XbLpoSp05PPbGl7aSPW29GGrDKL2B-KsCxji7GhhPNI,515
308
+ tests/query/functions/test_datetime.py,sha256=X_5LR9XouPiQL-GQWen-NtokunCBHDuZ5gk9elNJNd4,3000
308
309
  tests/query/functions/test_functions.py,sha256=510fqRrOrAZ9NyFoZtlF6lIdiiLriWhZ7vvveWZ8rsc,1984
309
- tests/query/functions/test_math.py,sha256=nrHrThxurZIvbqlN4mjfxtKqLdBxOrWoeIZ_QCRJuTs,1271
310
+ tests/query/functions/test_math.py,sha256=wHaGQdEKISI8WeG9zGVNv62IrWSp4mMH3dPxVxoHy3s,1286
310
311
  tests/query/functions/test_string.py,sha256=RMojkBUzw1Ikrb3nTa7VjJ4FsKfrjpuHUyxQDA-F5Cs,1800
311
312
  tests/query/functions/test_type_conversion.py,sha256=WeYR9UfJnbidle07-akQ1g9hFCd93qT8xUhDF3c58n4,3235
312
313
  tests/query/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -336,7 +337,7 @@ tests/table/test_objects.py,sha256=bir86ks-Ngy8x9Eu9bekOrh6twBYdEkIgTdbBWY6x9s,8
336
337
  tests/table/test_output.py,sha256=ZnpPbgVp79JcB6E_ooWQxOpOlhkwNUlMxC-1LSIEc2Y,4304
337
338
  tests/table/test_raw.py,sha256=9PTvYngQi41nYd5lKzkJdTqsEcwrdOXcvZjq-W26CwQ,1683
338
339
  tests/table/test_ref.py,sha256=eYNRnYHzNMXuMbV3B1ca5EidpIg4500q6hr1ccuVaso,269
339
- tests/table/test_refresh.py,sha256=ZXGLGHeMZcWnhZPB4eCasv1RkojPt6nUbxaE7WlyJbo,2804
340
+ tests/table/test_refresh.py,sha256=-BaLS6fZiR2RtQaFa7D9WGBjrbrss1-tt5xz1NE_m8E,9250
340
341
  tests/table/test_repr.py,sha256=uahz3_GffGQrf2mDE-4-Pu4AmSLBAyso6-9rbohCl58,446
341
342
  tests/table/test_select.py,sha256=jgeiahIlNFVijxYb3a54g1sJWVfH3llaYrsTBmdicrs,40390
342
343
  tests/table/test_str.py,sha256=eztWNULcjARR1fr9X5n4tojhDNgDfatVyNHwuYrzHAo,1731
@@ -353,6 +354,7 @@ tests/table/instance/test_to_dict.py,sha256=gkiYkmcI5qcy5E-ERWWmO-Q8uyVSFfcpJ8d5
353
354
  tests/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
354
355
  tests/testing/test_model_builder.py,sha256=bgs-cOYXtnrfgWydU2sZvs2N_Lt2T7La7AAMfIsOev4,6206
355
356
  tests/testing/test_random_builder.py,sha256=Upz9P1bhICVo0udI6Li-5eEdrXKbv8rMMLe0uK6pqB0,1694
357
+ tests/testing/test_test_case.py,sha256=qyDWYT44EZNyuWhaZXgSOpX48RaRw5u4FgNi87FYt2k,1691
356
358
  tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
357
359
  tests/utils/test_dictionary.py,sha256=GdWujlYQy6t09p2aQHPibkkPNbBYwkFwomKrVnztJTo,1480
358
360
  tests/utils/test_encoding.py,sha256=x1CHLm0rRheZaXz7at3SgkgcrZOmtMB-NtvJC77wQKE,326
@@ -365,9 +367,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
365
367
  tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
366
368
  tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
367
369
  tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
368
- piccolo-1.14.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
369
- piccolo-1.14.0.dist-info/METADATA,sha256=4rDBz-Wbqp-MdHwcZd3LaxJToiNqW5hVFQkCOkCD8LU,5178
370
- piccolo-1.14.0.dist-info/WHEEL,sha256=rWxmBtp7hEUqVLOnTaDOPpR-cZpCDkzhhcBce-Zyd5k,91
371
- piccolo-1.14.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
372
- piccolo-1.14.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
373
- piccolo-1.14.0.dist-info/RECORD,,
370
+ piccolo-1.16.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
371
+ piccolo-1.16.0.dist-info/METADATA,sha256=qsjW9UTKSwkT62sKfdGnTFyllEcn8bE1yKIj54iu4Ek,5178
372
+ piccolo-1.16.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
373
+ piccolo-1.16.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
374
+ piccolo-1.16.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
375
+ piccolo-1.16.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.0.4)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -109,7 +109,7 @@ class TestLogin(TestCase):
109
109
  BaseUser.update_password_sync(username, malicious_password)
110
110
  self.assertEqual(
111
111
  manager.exception.__str__(),
112
- "The password is too long.",
112
+ f"The password is too long. (max {BaseUser._max_password_length})",
113
113
  )
114
114
 
115
115
  # Test short passwords
@@ -118,7 +118,10 @@ class TestLogin(TestCase):
118
118
  BaseUser.update_password_sync(username, short_password)
119
119
  self.assertEqual(
120
120
  manager.exception.__str__(),
121
- "The password is too short.",
121
+ (
122
+ "The password is too short. (min "
123
+ f"{BaseUser._min_password_length})"
124
+ ),
122
125
  )
123
126
 
124
127
  # Test no password
@@ -205,7 +208,11 @@ class TestCreateUser(TestCase):
205
208
  BaseUser.create_user_sync(username="bob", password="abc")
206
209
 
207
210
  self.assertEqual(
208
- manager.exception.__str__(), "The password is too short."
211
+ manager.exception.__str__(),
212
+ (
213
+ "The password is too short. (min "
214
+ f"{BaseUser._min_password_length})"
215
+ ),
209
216
  )
210
217
 
211
218
  def test_long_password_error(self):
@@ -216,7 +223,8 @@ class TestCreateUser(TestCase):
216
223
  )
217
224
 
218
225
  self.assertEqual(
219
- manager.exception.__str__(), "The password is too long."
226
+ manager.exception.__str__(),
227
+ f"The password is too long. (max {BaseUser._max_password_length})",
220
228
  )
221
229
 
222
230
  def test_no_username_error(self):
@@ -4,6 +4,7 @@ import decimal
4
4
  import uuid
5
5
  from unittest import TestCase
6
6
 
7
+ from piccolo.utils.encoding import JSONDict
7
8
  from tests.base import engines_skip
8
9
 
9
10
  try:
@@ -376,6 +377,9 @@ class TestM2MComplexSchema(TestCase):
376
377
 
377
378
  if isinstance(column, UUID):
378
379
  self.assertIn(type(returned_value), (uuid.UUID, asyncpgUUID))
380
+ elif isinstance(column, (JSON, JSONB)):
381
+ self.assertEqual(type(returned_value), JSONDict)
382
+ self.assertEqual(original_value, returned_value)
379
383
  else:
380
384
  self.assertEqual(
381
385
  type(original_value),
@@ -401,6 +405,9 @@ class TestM2MComplexSchema(TestCase):
401
405
  if isinstance(column, UUID):
402
406
  self.assertIn(type(returned_value), (uuid.UUID, asyncpgUUID))
403
407
  self.assertEqual(str(original_value), str(returned_value))
408
+ elif isinstance(column, (JSON, JSONB)):
409
+ self.assertEqual(type(returned_value), JSONDict)
410
+ self.assertEqual(original_value, returned_value)
404
411
  else:
405
412
  self.assertEqual(
406
413
  type(original_value),
@@ -14,7 +14,8 @@ from piccolo.columns.column_types import (
14
14
  )
15
15
  from piccolo.querystring import QueryString
16
16
  from piccolo.table import Table
17
- from tests.base import TableTest, engines_only, engines_skip, sqlite_only
17
+ from piccolo.testing.test_case import TableTest
18
+ from tests.base import engines_only, engines_skip, sqlite_only
18
19
 
19
20
 
20
21
  class MyTable(Table):
@@ -2,7 +2,8 @@ import os
2
2
 
3
3
  from piccolo.columns.column_types import BigInt
4
4
  from piccolo.table import Table
5
- from tests.base import TableTest, engines_only
5
+ from piccolo.testing.test_case import TableTest
6
+ from tests.base import engines_only
6
7
 
7
8
 
8
9
  class MyTable(Table):
@@ -2,7 +2,7 @@ import typing as t
2
2
 
3
3
  from piccolo.columns.column_types import Boolean
4
4
  from piccolo.table import Table
5
- from tests.base import TableTest
5
+ from piccolo.testing.test_case import TableTest
6
6
 
7
7
 
8
8
  class MyTable(Table):
@@ -1,6 +1,6 @@
1
1
  from piccolo.columns.column_types import Bytea
2
2
  from piccolo.table import Table
3
- from tests.base import TableTest
3
+ from piccolo.testing.test_case import TableTest
4
4
 
5
5
 
6
6
  class MyTable(Table):
@@ -2,7 +2,8 @@ import enum
2
2
 
3
3
  from piccolo.columns.column_types import Array, Varchar
4
4
  from piccolo.table import Table
5
- from tests.base import TableTest, engines_only
5
+ from piccolo.testing.test_case import TableTest
6
+ from tests.base import engines_only
6
7
  from tests.example_apps.music.tables import Shirt
7
8
 
8
9
 
@@ -3,7 +3,7 @@ import datetime
3
3
  from piccolo.columns.column_types import Date
4
4
  from piccolo.columns.defaults.date import DateNow
5
5
  from piccolo.table import Table
6
- from tests.base import TableTest
6
+ from piccolo.testing.test_case import TableTest
7
7
 
8
8
 
9
9
  class MyTable(Table):
@@ -1,6 +1,6 @@
1
1
  from piccolo.columns.column_types import DoublePrecision
2
2
  from piccolo.table import Table
3
- from tests.base import TableTest
3
+ from piccolo.testing.test_case import TableTest
4
4
 
5
5
 
6
6
  class MyTable(Table):
@@ -3,7 +3,7 @@ import datetime
3
3
  from piccolo.columns.column_types import Interval
4
4
  from piccolo.columns.defaults.interval import IntervalCustom
5
5
  from piccolo.table import Table
6
- from tests.base import TableTest
6
+ from piccolo.testing.test_case import TableTest
7
7
 
8
8
 
9
9
  class MyTable(Table):
@@ -1,6 +1,6 @@
1
1
  from piccolo.columns.column_types import JSON
2
2
  from piccolo.table import Table
3
- from tests.base import TableTest
3
+ from piccolo.testing.test_case import TableTest
4
4
 
5
5
 
6
6
  class MyTable(Table):
@@ -1,6 +1,7 @@
1
1
  from piccolo.columns.column_types import JSONB, ForeignKey, Varchar
2
2
  from piccolo.table import Table
3
- from tests.base import TableTest, engines_only, engines_skip
3
+ from piccolo.testing.test_case import TableTest
4
+ from tests.base import engines_only, engines_skip
4
5
 
5
6
 
6
7
  class RecordingStudio(Table):
@@ -2,7 +2,7 @@ from decimal import Decimal
2
2
 
3
3
  from piccolo.columns.column_types import Numeric
4
4
  from piccolo.table import Table
5
- from tests.base import TableTest
5
+ from piccolo.testing.test_case import TableTest
6
6
 
7
7
 
8
8
  class MyTable(Table):
@@ -8,7 +8,7 @@ from piccolo.columns.column_types import (
8
8
  Varchar,
9
9
  )
10
10
  from piccolo.table import Table
11
- from tests.base import TableTest
11
+ from piccolo.testing.test_case import TableTest
12
12
 
13
13
 
14
14
  class MyTableDefaultPrimaryKey(Table):
@@ -1,7 +1,7 @@
1
1
  from piccolo import columns
2
2
  from piccolo.columns.readable import Readable
3
3
  from piccolo.table import Table
4
- from tests.base import TableTest
4
+ from piccolo.testing.test_case import TableTest
5
5
 
6
6
 
7
7
  class MyTable(Table):
@@ -1,6 +1,6 @@
1
1
  from piccolo.columns.column_types import Real
2
2
  from piccolo.table import Table
3
- from tests.base import TableTest
3
+ from piccolo.testing.test_case import TableTest
4
4
 
5
5
 
6
6
  class MyTable(Table):
@@ -8,7 +8,7 @@ from unittest import TestCase
8
8
  from piccolo.columns import ForeignKey, Varchar
9
9
  from piccolo.columns.reference import LazyTableReference
10
10
  from piccolo.table import Table
11
- from tests.base import TableTest
11
+ from piccolo.testing.test_case import TableTest
12
12
 
13
13
 
14
14
  class Band(Table):
@@ -1,6 +1,6 @@
1
1
  from piccolo.columns.column_types import Integer, Varchar
2
2
  from piccolo.table import Table
3
- from tests.base import TableTest
3
+ from piccolo.testing.test_case import TableTest
4
4
 
5
5
 
6
6
  class Concert(Table):
@@ -2,8 +2,8 @@ import os
2
2
 
3
3
  from piccolo.columns.column_types import SmallInt
4
4
  from piccolo.table import Table
5
-
6
- from ..base import TableTest, engines_only
5
+ from piccolo.testing.test_case import TableTest
6
+ from tests.base import engines_only
7
7
 
8
8
 
9
9
  class MyTable(Table):
@@ -4,7 +4,8 @@ from functools import partial
4
4
  from piccolo.columns.column_types import Time
5
5
  from piccolo.columns.defaults.time import TimeNow
6
6
  from piccolo.table import Table
7
- from tests.base import TableTest, engines_skip
7
+ from piccolo.testing.test_case import TableTest
8
+ from tests.base import engines_skip
8
9
 
9
10
 
10
11
  class MyTable(Table):
@@ -3,7 +3,7 @@ import datetime
3
3
  from piccolo.columns.column_types import Timestamp
4
4
  from piccolo.columns.defaults.timestamp import TimestampNow
5
5
  from piccolo.table import Table
6
- from tests.base import TableTest
6
+ from piccolo.testing.test_case import TableTest
7
7
 
8
8
 
9
9
  class MyTable(Table):
@@ -9,7 +9,7 @@ from piccolo.columns.defaults.timestamptz import (
9
9
  TimestamptzOffset,
10
10
  )
11
11
  from piccolo.table import Table
12
- from tests.base import TableTest
12
+ from piccolo.testing.test_case import TableTest
13
13
 
14
14
 
15
15
  class MyTable(Table):
@@ -2,7 +2,7 @@ import uuid
2
2
 
3
3
  from piccolo.columns.column_types import UUID
4
4
  from piccolo.table import Table
5
- from tests.base import TableTest
5
+ from piccolo.testing.test_case import TableTest
6
6
 
7
7
 
8
8
  class MyTable(Table):
@@ -1,7 +1,7 @@
1
1
  from piccolo.columns.column_types import Varchar
2
2
  from piccolo.table import Table
3
-
4
- from ..base import TableTest, engines_only
3
+ from piccolo.testing.test_case import TableTest
4
+ from tests.base import engines_only
5
5
 
6
6
 
7
7
  class MyTable(Table):
@@ -1,4 +1,4 @@
1
- from tests.base import TableTest
1
+ from piccolo.testing.test_case import TableTest
2
2
  from tests.example_apps.music.tables import Band, Manager
3
3
 
4
4
 
@@ -12,7 +12,8 @@ from piccolo.query.functions.datetime import (
12
12
  Year,
13
13
  )
14
14
  from piccolo.table import Table
15
- from tests.base import TableTest, engines_only, sqlite_only
15
+ from piccolo.testing.test_case import TableTest
16
+ from tests.base import engines_only, sqlite_only
16
17
 
17
18
 
18
19
  class Concert(Table):
@@ -3,7 +3,7 @@ import decimal
3
3
  from piccolo.columns import Numeric
4
4
  from piccolo.query.functions.math import Abs, Ceil, Floor, Round
5
5
  from piccolo.table import Table
6
- from tests.base import TableTest
6
+ from piccolo.testing.test_case import TableTest
7
7
 
8
8
 
9
9
  class Ticket(Table):
@@ -1,5 +1,14 @@
1
+ import typing as t
2
+
3
+ from piccolo.testing.test_case import TableTest
1
4
  from tests.base import DBTestCase
2
- from tests.example_apps.music.tables import Band
5
+ from tests.example_apps.music.tables import (
6
+ Band,
7
+ Concert,
8
+ Manager,
9
+ RecordingStudio,
10
+ Venue,
11
+ )
3
12
 
4
13
 
5
14
  class TestRefresh(DBTestCase):
@@ -24,9 +33,55 @@ class TestRefresh(DBTestCase):
24
33
  # Refresh `band`, and make sure it has the correct data.
25
34
  band.refresh().run_sync()
26
35
 
27
- self.assertTrue(band.name == "Pythonistas!!!")
28
- self.assertTrue(band.popularity == 8000)
29
- self.assertTrue(band.id == initial_data["id"])
36
+ self.assertEqual(band.name, "Pythonistas!!!")
37
+ self.assertEqual(band.popularity, 8000)
38
+ self.assertEqual(band.id, initial_data["id"])
39
+
40
+ def test_refresh_with_prefetch(self) -> None:
41
+ """
42
+ Make sure ``refresh`` works, when the object used prefetch to get
43
+ nested objets (the nested objects should be updated too).
44
+ """
45
+ band = (
46
+ Band.objects(Band.manager)
47
+ .where(Band.name == "Pythonistas")
48
+ .first()
49
+ .run_sync()
50
+ )
51
+ assert band is not None
52
+
53
+ # Modify the data in the database.
54
+ Manager.update({Manager.name: "Guido!!!"}).where(
55
+ Manager.name == "Guido"
56
+ ).run_sync()
57
+
58
+ # Refresh `band`, and make sure it has the correct data.
59
+ band.refresh().run_sync()
60
+
61
+ self.assertEqual(band.manager.name, "Guido!!!")
62
+
63
+ def test_refresh_with_prefetch_multiple_layers_deep(self) -> None:
64
+ """
65
+ Make sure ``refresh`` works, when the object used prefetch to get
66
+ nested objets (the nested objects should be updated too).
67
+ """
68
+ band = (
69
+ Band.objects(Band.manager)
70
+ .where(Band.name == "Pythonistas")
71
+ .first()
72
+ .run_sync()
73
+ )
74
+ assert band is not None
75
+
76
+ # Modify the data in the database.
77
+ Manager.update({Manager.name: "Guido!!!"}).where(
78
+ Manager.name == "Guido"
79
+ ).run_sync()
80
+
81
+ # Refresh `band`, and make sure it has the correct data.
82
+ band.refresh().run_sync()
83
+
84
+ self.assertEqual(band.manager.name, "Guido!!!")
30
85
 
31
86
  def test_columns(self) -> None:
32
87
  """
@@ -50,9 +105,9 @@ class TestRefresh(DBTestCase):
50
105
  )
51
106
  query.run_sync()
52
107
 
53
- self.assertTrue(band.name == "Pythonistas!!!")
54
- self.assertTrue(band.popularity == initial_data["popularity"])
55
- self.assertTrue(band.id == initial_data["id"])
108
+ self.assertEqual(band.name, "Pythonistas!!!")
109
+ self.assertEqual(band.popularity, initial_data["popularity"])
110
+ self.assertEqual(band.id, initial_data["id"])
56
111
 
57
112
  def test_error_when_not_in_db(self) -> None:
58
113
  """
@@ -85,3 +140,159 @@ class TestRefresh(DBTestCase):
85
140
  "The instance's primary key value isn't defined.",
86
141
  str(manager.exception),
87
142
  )
143
+
144
+
145
+ class TestRefreshWithPrefetch(TableTest):
146
+
147
+ tables = [Manager, Band, Concert, Venue]
148
+
149
+ def setUp(self):
150
+ super().setUp()
151
+
152
+ self.manager = Manager({Manager.name: "Guido"})
153
+ self.manager.save().run_sync()
154
+
155
+ self.band = Band(
156
+ {Band.name: "Pythonistas", Band.manager: self.manager}
157
+ )
158
+ self.band.save().run_sync()
159
+
160
+ self.concert = Concert({Concert.band_1: self.band})
161
+ self.concert.save().run_sync()
162
+
163
+ def test_single_layer(self) -> None:
164
+ """
165
+ Make sure ``refresh`` works, when the object used prefetch to get
166
+ nested objects (the nested objects should be updated too).
167
+ """
168
+ band = (
169
+ Band.objects(Band.manager)
170
+ .where(Band.name == "Pythonistas")
171
+ .first()
172
+ .run_sync()
173
+ )
174
+ assert band is not None
175
+
176
+ # Modify the data in the database.
177
+ self.manager.name = "Guido!!!"
178
+ self.manager.save().run_sync()
179
+
180
+ # Refresh `band`, and make sure it has the correct data.
181
+ band.refresh().run_sync()
182
+ self.assertEqual(band.manager.name, "Guido!!!")
183
+
184
+ def test_multiple_layers(self) -> None:
185
+ """
186
+ Make sure ``refresh`` works when ``prefetch`` was used to fetch objects
187
+ multiple layers deep.
188
+ """
189
+ concert = (
190
+ Concert.objects(Concert.band_1._.manager)
191
+ .where(Concert.band_1._.name == "Pythonistas")
192
+ .first()
193
+ .run_sync()
194
+ )
195
+ assert concert is not None
196
+
197
+ # Modify the data in the database.
198
+ self.manager.name = "Guido!!!"
199
+ self.manager.save().run_sync()
200
+
201
+ concert.refresh().run_sync()
202
+ self.assertEqual(concert.band_1.manager.name, "Guido!!!")
203
+
204
+ def test_updated_foreign_key(self) -> None:
205
+ """
206
+ If a foreign key now references a different row, make sure this
207
+ is refreshed correctly.
208
+ """
209
+ band = (
210
+ Band.objects(Band.manager)
211
+ .where(Band.name == "Pythonistas")
212
+ .first()
213
+ .run_sync()
214
+ )
215
+ assert band is not None
216
+
217
+ # Assign a different manager to the band
218
+ new_manager = Manager({Manager.name: "New Manager"})
219
+ new_manager.save().run_sync()
220
+ Band.update({Band.manager: new_manager.id}, force=True).run_sync()
221
+
222
+ # Refresh `band`, and make sure it references the new manager.
223
+ band.refresh().run_sync()
224
+ self.assertEqual(band.manager.id, new_manager.id)
225
+ self.assertEqual(band.manager.name, "New Manager")
226
+
227
+ def test_foreign_key_set_to_null(self):
228
+ """
229
+ Make sure that if the foreign key was set to null, that ``refresh``
230
+ sets the nested object to ``None``.
231
+ """
232
+ band = (
233
+ Band.objects(Band.manager)
234
+ .where(Band.name == "Pythonistas")
235
+ .first()
236
+ .run_sync()
237
+ )
238
+ assert band is not None
239
+
240
+ # Remove the manager from band
241
+ Band.update({Band.manager: None}, force=True).run_sync()
242
+
243
+ # Refresh `band`, and make sure the foreign key value is now `None`,
244
+ # instead of a nested object.
245
+ band.refresh().run_sync()
246
+ self.assertIsNone(band.manager)
247
+
248
+ def test_exception(self) -> None:
249
+ """
250
+ We don't currently let the user refresh specific fields from nested
251
+ objects - an exception should be raised.
252
+ """
253
+ with self.assertRaises(ValueError):
254
+ self.concert.refresh(columns=[Concert.band_1._.manager]).run_sync()
255
+
256
+ # Shouldn't raise an exception:
257
+ self.concert.refresh(columns=[Concert.band_1]).run_sync()
258
+
259
+
260
+ class TestRefreshWithLoadJSON(TableTest):
261
+
262
+ tables = [RecordingStudio]
263
+
264
+ def setUp(self):
265
+ super().setUp()
266
+
267
+ self.recording_studio = RecordingStudio(
268
+ {RecordingStudio.facilities: {"piano": True}}
269
+ )
270
+ self.recording_studio.save().run_sync()
271
+
272
+ def test_load_json(self):
273
+ """
274
+ Make sure we can refresh an object, and load the JSON as a Python
275
+ object.
276
+ """
277
+ RecordingStudio.update(
278
+ {RecordingStudio.facilities: {"electric piano": True}},
279
+ force=True,
280
+ ).run_sync()
281
+
282
+ # Refresh without load_json:
283
+ self.recording_studio.refresh().run_sync()
284
+
285
+ self.assertEqual(
286
+ # Remove the white space, because some versions of Python add
287
+ # whitespace around JSON, and some don't.
288
+ self.recording_studio.facilities.replace(" ", ""),
289
+ '{"electricpiano":true}',
290
+ )
291
+
292
+ # Refresh with load_json:
293
+ self.recording_studio.refresh(load_json=True).run_sync()
294
+
295
+ self.assertDictEqual(
296
+ t.cast(dict, self.recording_studio.facilities),
297
+ {"electric piano": True},
298
+ )
@@ -0,0 +1,65 @@
1
+ import sys
2
+
3
+ import pytest
4
+
5
+ from piccolo.engine import engine_finder
6
+ from piccolo.testing.test_case import (
7
+ AsyncTableTest,
8
+ AsyncTransactionTest,
9
+ TableTest,
10
+ )
11
+ from tests.example_apps.music.tables import Band, Manager
12
+
13
+
14
+ class TestTableTest(TableTest):
15
+ """
16
+ Make sure the tables are created automatically.
17
+ """
18
+
19
+ tables = [Band, Manager]
20
+
21
+ async def test_tables_created(self):
22
+ self.assertTrue(Band.table_exists().run_sync())
23
+ self.assertTrue(Manager.table_exists().run_sync())
24
+
25
+
26
+ class TestAsyncTableTest(AsyncTableTest):
27
+ """
28
+ Make sure the tables are created automatically in async tests.
29
+ """
30
+
31
+ tables = [Band, Manager]
32
+
33
+ async def test_tables_created(self):
34
+ self.assertTrue(await Band.table_exists())
35
+ self.assertTrue(await Manager.table_exists())
36
+
37
+
38
+ @pytest.mark.skipif(sys.version_info <= (3, 11), reason="Python 3.11 required")
39
+ class TestAsyncTransaction(AsyncTransactionTest):
40
+ """
41
+ Make sure that the test exists within a transaction.
42
+ """
43
+
44
+ async def test_transaction_exists(self):
45
+ db = engine_finder()
46
+ assert db is not None
47
+ self.assertTrue(db.transaction_exists())
48
+
49
+
50
+ @pytest.mark.skipif(sys.version_info <= (3, 11), reason="Python 3.11 required")
51
+ class TestAsyncTransactionRolledBack(AsyncTransactionTest):
52
+ """
53
+ Make sure that the changes get rolled back automatically.
54
+ """
55
+
56
+ async def asyncTearDown(self):
57
+ await super().asyncTearDown()
58
+
59
+ assert Manager.table_exists().run_sync() is False
60
+
61
+ async def test_insert_data(self):
62
+ await Manager.create_table()
63
+
64
+ manager = Manager({Manager.name: "Guido"})
65
+ await manager.save()