piccolo 1.19.1__py3-none-any.whl → 1.21.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.
piccolo/__init__.py CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "1.19.1"
1
+ __VERSION__ = "1.21.0"
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import typing as t
4
4
 
5
- from piccolo.columns.column_types import ForeignKey
5
+ from piccolo.columns.column_types import ForeignKey, ReferencedTable
6
6
  from piccolo.columns.combination import And, Where
7
7
  from piccolo.custom_types import Combinable, TableInstance
8
8
  from piccolo.engine.base import BaseBatch
@@ -231,6 +231,63 @@ class UpdateSelf:
231
231
  return run_sync(self.run(*args, **kwargs))
232
232
 
233
233
 
234
+ class GetRelated(t.Generic[ReferencedTable]):
235
+
236
+ def __init__(self, row: Table, foreign_key: ForeignKey[ReferencedTable]):
237
+ self.row = row
238
+ self.foreign_key = foreign_key
239
+
240
+ async def run(
241
+ self,
242
+ node: t.Optional[str] = None,
243
+ in_pool: bool = True,
244
+ ) -> t.Optional[ReferencedTable]:
245
+ if not self.row._exists_in_db:
246
+ raise ValueError("The object doesn't exist in the database.")
247
+
248
+ root_table = self.row.__class__
249
+
250
+ data = (
251
+ await root_table.select(
252
+ *[
253
+ i.as_alias(i._meta.name)
254
+ for i in self.foreign_key.all_columns()
255
+ ]
256
+ )
257
+ .where(
258
+ root_table._meta.primary_key
259
+ == getattr(self.row, root_table._meta.primary_key._meta.name)
260
+ )
261
+ .first()
262
+ .run(node=node, in_pool=in_pool)
263
+ )
264
+
265
+ # Make sure that some values were returned:
266
+ if data is None or not any(data.values()):
267
+ return None
268
+
269
+ references = t.cast(
270
+ t.Type[ReferencedTable],
271
+ self.foreign_key._foreign_key_meta.resolved_references,
272
+ )
273
+
274
+ referenced_object = references(**data)
275
+ referenced_object._exists_in_db = True
276
+ return referenced_object
277
+
278
+ def __await__(
279
+ self,
280
+ ) -> t.Generator[None, None, t.Optional[ReferencedTable]]:
281
+ """
282
+ If the user doesn't explicity call .run(), proxy to it as a
283
+ convenience.
284
+ """
285
+ return self.run().__await__()
286
+
287
+ def run_sync(self, *args, **kwargs) -> t.Optional[ReferencedTable]:
288
+ return run_sync(self.run(*args, **kwargs))
289
+
290
+
234
291
  ###############################################################################
235
292
 
236
293
 
@@ -534,7 +534,7 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
534
534
  _joins.append(
535
535
  f'LEFT JOIN {right_tablename} "{table_alias}"'
536
536
  " ON "
537
- f'({left_tablename}."{key._meta.name}" = "{table_alias}"."{pk_name}")' # noqa: E501
537
+ f'({left_tablename}."{key._meta.db_column_name}" = "{table_alias}"."{pk_name}")' # noqa: E501
538
538
  )
539
539
 
540
540
  joins.extend(_joins)
piccolo/table.py CHANGED
@@ -46,7 +46,7 @@ from piccolo.query import (
46
46
  )
47
47
  from piccolo.query.methods.create_index import CreateIndex
48
48
  from piccolo.query.methods.indexes import Indexes
49
- from piccolo.query.methods.objects import First, UpdateSelf
49
+ from piccolo.query.methods.objects import GetRelated, UpdateSelf
50
50
  from piccolo.query.methods.refresh import Refresh
51
51
  from piccolo.querystring import QueryString
52
52
  from piccolo.utils import _camel_to_snake
@@ -612,14 +612,14 @@ class Table(metaclass=TableMetaclass):
612
612
  @t.overload
613
613
  def get_related(
614
614
  self, foreign_key: ForeignKey[ReferencedTable]
615
- ) -> First[ReferencedTable]: ...
615
+ ) -> GetRelated[ReferencedTable]: ...
616
616
 
617
617
  @t.overload
618
- def get_related(self, foreign_key: str) -> First[Table]: ...
618
+ def get_related(self, foreign_key: str) -> GetRelated[Table]: ...
619
619
 
620
620
  def get_related(
621
621
  self, foreign_key: t.Union[str, ForeignKey[ReferencedTable]]
622
- ) -> t.Union[First[Table], First[ReferencedTable]]:
622
+ ) -> GetRelated[ReferencedTable]:
623
623
  """
624
624
  Used to fetch a ``Table`` instance, for the target of a foreign key.
625
625
 
@@ -630,8 +630,8 @@ class Table(metaclass=TableMetaclass):
630
630
  >>> print(manager.name)
631
631
  'Guido'
632
632
 
633
- It can only follow foreign keys one level currently.
634
- i.e. ``Band.manager``, but not ``Band.manager.x.y.z``.
633
+ It can only follow foreign keys multiple levels deep. For example,
634
+ ``Concert.band_1.manager``.
635
635
 
636
636
  """
637
637
  if isinstance(foreign_key, str):
@@ -645,18 +645,7 @@ class Table(metaclass=TableMetaclass):
645
645
  "ForeignKey column."
646
646
  )
647
647
 
648
- column_name = foreign_key._meta.name
649
-
650
- references = foreign_key._foreign_key_meta.resolved_references
651
-
652
- return (
653
- references.objects()
654
- .where(
655
- foreign_key._foreign_key_meta.resolved_target_column
656
- == getattr(self, column_name)
657
- )
658
- .first()
659
- )
648
+ return GetRelated(foreign_key=foreign_key, row=self)
660
649
 
661
650
  def get_m2m(self, m2m: M2M) -> M2MGetRelated:
662
651
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piccolo
3
- Version: 1.19.1
3
+ Version: 1.21.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=JP09HcvsWvSb_xYAg-Jfrxz8qqh31rbqbIN3dcneflw,23
1
+ piccolo/__init__.py,sha256=lfg4osIcO7gnlgKb0zY1KT8t1Ngd2pATD-6YnBkIDcU,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=rwLkom0-4ahOTVon9JDoZ92cp5d3nuwo24ES8attxCM,50896
7
+ piccolo/table.py,sha256=UvEbagMYRkTbyFHTUwUshZlL_dC4UKDP7vUOwF8OXmg,50593
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
@@ -166,10 +166,10 @@ piccolo/query/methods/drop_index.py,sha256=5x3vHpoOmQ1SMhj6L7snKXX6M9l9j1E1PFSO6
166
166
  piccolo/query/methods/exists.py,sha256=lTMjtrFPFygZmaPV3sfQKXc3K0sVqJ2S6PDc3fRK6YQ,1203
167
167
  piccolo/query/methods/indexes.py,sha256=J-QUqaBJwpgahskUH0Cu0Mq7zEKcfVAtDsUVIVX-C4c,943
168
168
  piccolo/query/methods/insert.py,sha256=ssLJ_wn08KnOwwr7t-VILyn1P4hrvM63CfPIcAJWT5k,4701
169
- piccolo/query/methods/objects.py,sha256=kWHSgnXFVYe-x6tIHstbZvZic6mQCCGES-L_2Icbg-I,13910
169
+ piccolo/query/methods/objects.py,sha256=BfCOIbNMj7FWkmK5STaINkfDFmwzZvDZi60jCumjs1o,15627
170
170
  piccolo/query/methods/raw.py,sha256=wQWR8b-yA_Gr-5lqRMZe9BOAAMBAw8CqTx37qVYvM1A,987
171
171
  piccolo/query/methods/refresh.py,sha256=wg1zghKfwz-VmqK4uWa4GNMiDtK-skTqow591Hb3ONM,5854
172
- piccolo/query/methods/select.py,sha256=jNR7CsEPUarffYMsXytKm7iScrQ_nqpRX-5mTPrXfjg,22414
172
+ piccolo/query/methods/select.py,sha256=41OW-DIE_wr5VdxSusMKNT2aUhzQsCwK2Qh1XqgXHg0,22424
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
@@ -253,7 +253,7 @@ tests/columns/test_bytea.py,sha256=doN8S1eFVU4ntSXIg4IgMSZcbvqW1WJ-AEm3OjKLGkI,1
253
253
  tests/columns/test_choices.py,sha256=q8TLe7nvGERXyGO_XEryEBR-DuWwFY1jPpscsrXjdXo,4066
254
254
  tests/columns/test_combination.py,sha256=BuBwR7k5X1EkOWraZpjqU6gvtb6ow_k-7N1KQBiW2RA,1681
255
255
  tests/columns/test_date.py,sha256=QLC6kJMQwM-1mbUP4ksJVM7P8WwjzGZyynH3rHHdSew,1030
256
- tests/columns/test_db_column_name.py,sha256=v0QFOQp_atqzMB1n40simVwHeBDi5nyN1N2bSPX5k6w,7670
256
+ tests/columns/test_db_column_name.py,sha256=0wz6y4GNGy4nhMdHmYzEnChQGpK2UhWFFKrnmmML3Mk,9027
257
257
  tests/columns/test_defaults.py,sha256=rwlU1fXt3cCl7C51eLlZXqgWkE-K5W0pHvTrwkAKyCo,2896
258
258
  tests/columns/test_double_precision.py,sha256=7rhcSfDkb2fBh_zEG4UGwD_GW1sy6U9-8NooHuCS09Q,544
259
259
  tests/columns/test_get_sql_value.py,sha256=mKgsInN374jzV99y9mg_ZiG-AvnJgz36SZi89xL7RZM,1768
@@ -346,7 +346,7 @@ tests/table/test_update.py,sha256=Cqi0xX3kEuJ0k-x_emPGB3arXuGWZ9e3CJ3HPFnw9Zw,20
346
346
  tests/table/test_update_self.py,sha256=im6HcM-WLkEhZP0vTL42tYEJZyAZG6gDOxCnbikCBD4,907
347
347
  tests/table/instance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
348
348
  tests/table/instance/test_create.py,sha256=JD0l7L9dDK1FKPhUs6WC_B2bruPR1qQ8aIqXpEbfiUg,1105
349
- tests/table/instance/test_get_related.py,sha256=dAT89KHuO_XEMY6h0ngDHrXp_nJsMoSToTbH_IRclTE,1275
349
+ tests/table/instance/test_get_related.py,sha256=eracFunh4Qlj5BEkI7OsrOyefRZM0rxrXnFX92VL1ZE,3285
350
350
  tests/table/instance/test_get_related_readable.py,sha256=QDMMZykxPsTWcsl8ZIZtmQVLwSGCw7QBilLepAAAnWg,4694
351
351
  tests/table/instance/test_instantiate.py,sha256=jvtaqSa_zN1lHQiykN4EnwitZqkWAbXle5IJtyhKuHY,958
352
352
  tests/table/instance/test_remove.py,sha256=Zv22ZZqot61rjCVWL1PHDf1oxELcBnmMXx1gsST6j80,648
@@ -368,9 +368,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
368
368
  tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
369
369
  tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
370
370
  tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
371
- piccolo-1.19.1.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
372
- piccolo-1.19.1.dist-info/METADATA,sha256=owDJXe8zA0Tiloy78LB-2NPyDCxfM7qfFWOeDnXGNO8,5178
373
- piccolo-1.19.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
374
- piccolo-1.19.1.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
375
- piccolo-1.19.1.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
376
- piccolo-1.19.1.dist-info/RECORD,,
371
+ piccolo-1.21.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
372
+ piccolo-1.21.0.dist-info/METADATA,sha256=CMfZm7IYerdMTQLFwzuyCPWZMlrGf1G_shqoq2LlnGM,5178
373
+ piccolo-1.21.0.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
374
+ piccolo-1.21.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
375
+ piccolo-1.21.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
376
+ piccolo-1.21.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,12 +1,20 @@
1
- from piccolo.columns.column_types import Integer, Serial, Varchar
2
- from piccolo.table import Table
1
+ import typing as t
2
+
3
+ from piccolo.columns.column_types import ForeignKey, Integer, Serial, Varchar
4
+ from piccolo.table import Table, create_db_tables_sync, drop_db_tables_sync
3
5
  from tests.base import DBTestCase, engine_is, engines_only, engines_skip
4
6
 
5
7
 
8
+ class Manager(Table):
9
+ id: Serial
10
+ name = Varchar()
11
+
12
+
6
13
  class Band(Table):
7
14
  id: Serial
8
15
  name = Varchar(db_column_name="regrettable_column_name")
9
16
  popularity = Integer()
17
+ manager = ForeignKey(Manager, db_column_name="manager_fk")
10
18
 
11
19
 
12
20
  class TestDBColumnName(DBTestCase):
@@ -22,10 +30,15 @@ class TestDBColumnName(DBTestCase):
22
30
  """
23
31
 
24
32
  def setUp(self):
25
- Band.create_table().run_sync()
33
+ create_db_tables_sync(Band, Manager)
26
34
 
27
35
  def tearDown(self):
28
- Band.alter().drop_table().run_sync()
36
+ drop_db_tables_sync(Band, Manager)
37
+
38
+ def insert_band(self, manager: t.Optional[Manager] = None) -> Band:
39
+ band = Band(name="Pythonistas", popularity=1000, manager=manager)
40
+ band.save().run_sync()
41
+ return band
29
42
 
30
43
  @engines_only("postgres", "cockroach")
31
44
  def test_column_name_correct(self):
@@ -45,8 +58,7 @@ class TestDBColumnName(DBTestCase):
45
58
  """
46
59
  Make sure save queries work correctly.
47
60
  """
48
- band = Band(name="Pythonistas", popularity=1000)
49
- band.save().run_sync()
61
+ self.insert_band()
50
62
 
51
63
  band_from_db = Band.objects().first().run_sync()
52
64
  assert band_from_db is not None
@@ -56,11 +68,7 @@ class TestDBColumnName(DBTestCase):
56
68
  """
57
69
  Make sure create queries work correctly.
58
70
  """
59
- band = (
60
- Band.objects()
61
- .create(name="Pythonistas", popularity=1000)
62
- .run_sync()
63
- )
71
+ band = self.insert_band()
64
72
  self.assertEqual(band.name, "Pythonistas")
65
73
 
66
74
  band_from_db = Band.objects().first().run_sync()
@@ -74,7 +82,7 @@ class TestDBColumnName(DBTestCase):
74
82
  name to it's alias, but it's hard to predict what behaviour the user
75
83
  wants.
76
84
  """
77
- Band.objects().create(name="Pythonistas", popularity=1000).run_sync()
85
+ self.insert_band()
78
86
 
79
87
  # Make sure we can select all columns
80
88
  bands = Band.select().run_sync()
@@ -86,6 +94,7 @@ class TestDBColumnName(DBTestCase):
86
94
  "id": bands[0]["id"],
87
95
  "regrettable_column_name": "Pythonistas",
88
96
  "popularity": 1000,
97
+ "manager_fk": None,
89
98
  }
90
99
  ],
91
100
  )
@@ -97,6 +106,7 @@ class TestDBColumnName(DBTestCase):
97
106
  "id": 1,
98
107
  "regrettable_column_name": "Pythonistas",
99
108
  "popularity": 1000,
109
+ "manager_fk": None,
100
110
  }
101
111
  ],
102
112
  )
@@ -123,11 +133,36 @@ class TestDBColumnName(DBTestCase):
123
133
  ],
124
134
  )
125
135
 
136
+ def test_join(self):
137
+ """
138
+ Make sure that foreign keys with a ``db_column_name`` specified still
139
+ work for joins.
140
+
141
+ https://github.com/piccolo-orm/piccolo/issues/1101
142
+
143
+ """
144
+ manager = Manager.objects().create(name="Guido").run_sync()
145
+ band = self.insert_band(manager=manager)
146
+
147
+ bands = Band.select().where(Band.manager.name == "Guido").run_sync()
148
+
149
+ self.assertListEqual(
150
+ bands,
151
+ [
152
+ {
153
+ "id": band.id,
154
+ "manager_fk": manager.id,
155
+ "popularity": 1000,
156
+ "regrettable_column_name": "Pythonistas",
157
+ }
158
+ ],
159
+ )
160
+
126
161
  def test_update(self):
127
162
  """
128
163
  Make sure update queries work correctly.
129
164
  """
130
- Band.objects().create(name="Pythonistas", popularity=1000).run_sync()
165
+ self.insert_band()
131
166
 
132
167
  Band.update({Band.name: "Pythonistas 2"}, force=True).run_sync()
133
168
 
@@ -140,6 +175,7 @@ class TestDBColumnName(DBTestCase):
140
175
  "id": bands[0]["id"],
141
176
  "regrettable_column_name": "Pythonistas 2",
142
177
  "popularity": 1000,
178
+ "manager_fk": None,
143
179
  }
144
180
  ],
145
181
  )
@@ -151,6 +187,7 @@ class TestDBColumnName(DBTestCase):
151
187
  "id": 1,
152
188
  "regrettable_column_name": "Pythonistas 2",
153
189
  "popularity": 1000,
190
+ "manager_fk": None,
154
191
  }
155
192
  ],
156
193
  )
@@ -166,6 +203,7 @@ class TestDBColumnName(DBTestCase):
166
203
  "id": bands[0]["id"],
167
204
  "regrettable_column_name": "Pythonistas 3",
168
205
  "popularity": 1000,
206
+ "manager_fk": None,
169
207
  }
170
208
  ],
171
209
  )
@@ -177,6 +215,7 @@ class TestDBColumnName(DBTestCase):
177
215
  "id": 1,
178
216
  "regrettable_column_name": "Pythonistas 3",
179
217
  "popularity": 1000,
218
+ "manager_fk": None,
180
219
  }
181
220
  ],
182
221
  )
@@ -199,11 +238,13 @@ class TestDBColumnName(DBTestCase):
199
238
  "id": 1,
200
239
  "regrettable_column_name": "Pythonistas",
201
240
  "popularity": 1000,
241
+ "manager_fk": None,
202
242
  },
203
243
  {
204
244
  "id": 2,
205
245
  "regrettable_column_name": "Rustaceans",
206
246
  "popularity": 500,
247
+ "manager_fk": None,
207
248
  },
208
249
  ],
209
250
  )
@@ -218,6 +259,7 @@ class TestDBColumnName(DBTestCase):
218
259
  "id": 1,
219
260
  "regrettable_column_name": "Pythonistas",
220
261
  "popularity": 1000,
262
+ "manager_fk": None,
221
263
  }
222
264
  ],
223
265
  )
@@ -244,11 +286,13 @@ class TestDBColumnName(DBTestCase):
244
286
  "id": result[0]["id"],
245
287
  "regrettable_column_name": "Pythonistas",
246
288
  "popularity": 1000,
289
+ "manager_fk": None,
247
290
  },
248
291
  {
249
292
  "id": result[1]["id"],
250
293
  "regrettable_column_name": "Rustaceans",
251
294
  "popularity": 500,
295
+ "manager_fk": None,
252
296
  },
253
297
  ],
254
298
  )
@@ -263,6 +307,7 @@ class TestDBColumnName(DBTestCase):
263
307
  "id": result[0]["id"],
264
308
  "regrettable_column_name": "Pythonistas",
265
309
  "popularity": 1000,
310
+ "manager_fk": None,
266
311
  }
267
312
  ],
268
313
  )
@@ -1,42 +1,98 @@
1
1
  import typing as t
2
- from unittest import TestCase
3
2
 
4
- from tests.example_apps.music.tables import Band, Manager
3
+ from piccolo.testing.test_case import AsyncTableTest
4
+ from tests.example_apps.music.tables import Band, Concert, Manager, Venue
5
5
 
6
- TABLES = [Manager, Band]
7
6
 
7
+ class TestGetRelated(AsyncTableTest):
8
+ tables = [Manager, Band, Concert, Venue]
8
9
 
9
- class TestGetRelated(TestCase):
10
- def setUp(self):
11
- for table in TABLES:
12
- table.create_table().run_sync()
10
+ async def asyncSetUp(self):
11
+ await super().asyncSetUp()
13
12
 
14
- def tearDown(self):
15
- for table in reversed(TABLES):
16
- table.alter().drop_table().run_sync()
13
+ # Setup two pairs of manager/band, so we can make sure the correct
14
+ # objects are returned.
17
15
 
18
- def test_get_related(self) -> None:
16
+ self.manager = Manager(name="Guido")
17
+ await self.manager.save()
18
+
19
+ self.band = Band(
20
+ name="Pythonistas", manager=self.manager.id, popularity=100
21
+ )
22
+ await self.band.save()
23
+
24
+ self.manager_2 = Manager(name="Graydon")
25
+ await self.manager_2.save()
26
+
27
+ self.band_2 = Band(
28
+ name="Rustaceans", manager=self.manager_2.id, popularity=100
29
+ )
30
+ await self.band_2.save()
31
+
32
+ async def test_foreign_key(self) -> None:
19
33
  """
20
34
  Make sure you can get a related object from another object instance.
21
35
  """
22
- manager = Manager(name="Guido")
23
- manager.save().run_sync()
36
+ manager = await self.band.get_related(Band.manager)
37
+ assert manager is not None
38
+ self.assertTrue(manager.id == self.manager.id)
39
+
40
+ manager_2 = await self.band_2.get_related(Band.manager)
41
+ assert manager_2 is not None
42
+ self.assertTrue(manager_2.id == self.manager_2.id)
24
43
 
25
- band = Band(name="Pythonistas", manager=manager.id, popularity=100)
26
- band.save().run_sync()
44
+ async def test_non_foreign_key(self):
45
+ """
46
+ Make sure that non-ForeignKey raise an exception.
47
+ """
48
+ with self.assertRaises(ValueError):
49
+ self.band.get_related(Band.name) # type: ignore
27
50
 
28
- _manager = band.get_related(Band.manager).run_sync()
29
- assert _manager is not None
30
- self.assertTrue(_manager.name == "Guido")
51
+ async def test_string(self):
52
+ """
53
+ Make sure it also works using a string representation of a foreign key.
54
+ """
55
+ manager = t.cast(Manager, await self.band.get_related("manager"))
56
+ self.assertTrue(manager.id == self.manager.id)
31
57
 
32
- # Test non-ForeignKey
58
+ async def test_invalid_string(self):
59
+ """
60
+ Make sure an exception is raised if the foreign key string is invalid.
61
+ """
33
62
  with self.assertRaises(ValueError):
34
- band.get_related(Band.name) # type: ignore
63
+ self.band.get_related("abc123")
35
64
 
36
- # Make sure it also works using a string
37
- _manager_2 = t.cast(Manager, band.get_related("manager").run_sync())
38
- self.assertTrue(_manager_2.name == "Guido")
65
+ async def test_multiple_levels(self):
66
+ """
67
+ Make sure ``get_related`` works multiple levels deep.
68
+ """
69
+ concert = Concert(band_1=self.band, band_2=self.band_2)
70
+ await concert.save()
71
+
72
+ manager = await concert.get_related(Concert.band_1._.manager)
73
+ assert manager is not None
74
+ self.assertTrue(manager.id == self.manager.id)
75
+
76
+ manager_2 = await concert.get_related(Concert.band_2._.manager)
77
+ assert manager_2 is not None
78
+ self.assertTrue(manager_2.id == self.manager_2.id)
79
+
80
+ async def test_no_match(self):
81
+ """
82
+ If not related object exists, make sure ``None`` is returned.
83
+ """
84
+ concert = Concert(band_1=self.band, band_2=None)
85
+ await concert.save()
86
+
87
+ manager_2 = await concert.get_related(Concert.band_2._.manager)
88
+ assert manager_2 is None
89
+
90
+ async def test_not_in_db(self):
91
+ """
92
+ If the object we're calling ``get_related`` on doesn't exist in the
93
+ database, then make sure an error is raised.
94
+ """
95
+ concert = Concert(band_1=self.band, band_2=self.band_2)
39
96
 
40
- # Test an invalid string
41
97
  with self.assertRaises(ValueError):
42
- band.get_related("abc123")
98
+ await concert.get_related(Concert.band_1._.manager)