Skip to content

Commit 7a93765

Browse files
authored
all_columns fix (piccolo-orm#210)
* fixing bug with all_columns if called from too deep * ``ColumnsDelegate`` auto unpacks lists of columns This is useful if someone uses ``all_columns`` and forgets to unpack them. await Band.select(Band.name, Band.manager.all_columns()).run() The above now works - before this was required: await Band.select(Band.name, *Band.manager.all_columns()).run() * update docs, to mention that unpacking `all_columns` is now optional
1 parent 0132ae1 commit 7a93765

File tree

10 files changed

+147
-59
lines changed

10 files changed

+147
-59
lines changed

docs/src/piccolo/query_types/select.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ The joins can go several layers deep.
6161
>>> Concert.select(Concert.id, Concert.band_1.manager.name).run_sync()
6262
[{'id': 1, 'band_1.manager.name': 'Guido'}]
6363
64-
If you want all of the columns from a related table, there's a useful shortcut
65-
which saves you from typing them all out:
64+
If you want all of the columns from a related table you can use
65+
``all_columns``, which is a useful shortcut which saves you from typing them
66+
all out:
6667

6768
.. code-block:: python
6869
@@ -72,6 +73,10 @@ which saves you from typing them all out:
7273
{'name': 'Rustaceans', 'manager.id': 2, 'manager.name': 'Graydon'}
7374
]
7475
76+
# In Piccolo > 0.41.0 you no longer need to explicitly unpack ``all_columns``.
77+
# This is equivalent:
78+
>>> Band.select(Band.name, Band.manager.all_columns()).run_sync()
79+
7580
You can also get the response as nested dictionaries, which can be very useful:
7681

7782
.. code-block:: python

piccolo/columns/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class ColumnMeta:
137137
_table: t.Optional[t.Type[Table]] = None
138138

139139
# Used by Foreign Keys:
140-
call_chain: t.List["ForeignKey"] = field(default_factory=lambda: [])
140+
call_chain: t.List["ForeignKey"] = field(default_factory=list)
141141
table_alias: t.Optional[str] = None
142142

143143
@property

piccolo/columns/column_types.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,22 @@ def copy(self) -> ForeignKey:
12201220
column._foreign_key_meta = self._foreign_key_meta.copy()
12211221
return column
12221222

1223+
def all_columns(self):
1224+
"""
1225+
Allow a user to access all of the columns on the related table.
1226+
1227+
For example:
1228+
1229+
Band.select(Band.name, *Band.manager.all_columns()).run_sync()
1230+
1231+
"""
1232+
_fk_meta = object.__getattribute__(self, "_foreign_key_meta")
1233+
1234+
return [
1235+
getattr(self, column._meta.name)
1236+
for column in _fk_meta.resolved_references._meta.columns
1237+
]
1238+
12231239
def set_proxy_columns(self):
12241240
"""
12251241
In order to allow a fluent interface, where tables can be traversed
@@ -1233,22 +1249,6 @@ def set_proxy_columns(self):
12331249
setattr(self, _column._meta.name, _column)
12341250
_fk_meta.proxy_columns.append(_column)
12351251

1236-
def all_columns():
1237-
"""
1238-
Allow a user to access all of the columns on the related table.
1239-
1240-
For example:
1241-
1242-
Band.select(Band.name, *Band.manager.all_columns()).run_sync()
1243-
1244-
"""
1245-
return [
1246-
getattr(self, column._meta.name)
1247-
for column in _fk_meta.resolved_references._meta.columns
1248-
]
1249-
1250-
setattr(self, "all_columns", all_columns)
1251-
12521252
def __getattribute__(self, name: str):
12531253
"""
12541254
Returns attributes unmodified unless they're Column instances, in which

piccolo/query/mixins.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,22 @@ class ColumnsDelegate:
237237

238238
selected_columns: t.Sequence[Selectable] = field(default_factory=list)
239239

240-
def columns(self, *columns: Selectable):
241-
combined = list(self.selected_columns) + list(columns)
240+
def columns(self, *columns: t.Union[Selectable, t.List[Selectable]]):
241+
"""
242+
:param columns:
243+
We accept ``Selectable`` and ``List[Selectable]`` here, in case
244+
someone passes in a list by accident when using ``all_columns()``,
245+
in which case we flatten the list.
246+
247+
"""
248+
_columns = []
249+
for column in columns:
250+
if isinstance(column, list):
251+
_columns.extend(column)
252+
else:
253+
_columns.append(column)
254+
255+
combined = list(self.selected_columns) + _columns
242256
self.selected_columns = combined
243257

244258
def remove_secret_columns(self):

tests/columns/test_foreignkey.py

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
from piccolo.columns import Column, ForeignKey, LazyTableReference, Varchar
55
from piccolo.table import Table
6-
from tests.base import DBTestCase
7-
from tests.example_app.tables import Band, Manager
6+
from tests.example_app.tables import Band, Concert, Manager
87

98

109
class Manager1(Table, tablename="manager"):
@@ -172,32 +171,36 @@ def test_recursion_time(self):
172171
self.assertTrue(end - start < 1.0)
173172

174173

175-
class TestAllColumns(DBTestCase):
176-
def setUp(self):
177-
Manager.create_table().run_sync()
178-
manager = Manager(name="Guido")
179-
manager.save().run_sync()
180-
181-
Band.create_table().run_sync()
182-
Band(manager=manager, name="Pythonistas").save().run_sync()
183-
184-
def tearDown(self):
185-
Band.alter().drop_table().run_sync()
186-
Manager.alter().drop_table().run_sync()
187-
174+
class TestAllColumns(TestCase):
188175
def test_all_columns(self):
189176
"""
190177
Make sure you can retrieve all columns from a related table, without
191178
explicitly specifying them.
192179
"""
193-
result = Band.select(Band.name, *Band.manager.all_columns()).run_sync()
180+
all_columns = Band.manager.all_columns()
181+
self.assertEqual(all_columns, [Band.manager.id, Band.manager.name])
182+
183+
# Make sure the call chains are also correct.
194184
self.assertEqual(
195-
result,
196-
[
197-
{
198-
"name": "Pythonistas",
199-
"manager.id": 1,
200-
"manager.name": "Guido",
201-
}
202-
],
185+
all_columns[0]._meta.call_chain, Band.manager.id._meta.call_chain
186+
)
187+
self.assertEqual(
188+
all_columns[1]._meta.call_chain, Band.manager.name._meta.call_chain
189+
)
190+
191+
def test_all_columns_deep(self):
192+
"""
193+
Make sure ``all_columns`` works when the joins are several layers deep.
194+
"""
195+
all_columns = Concert.band_1.manager.all_columns()
196+
self.assertEqual(all_columns, [Band.manager.id, Band.manager.name])
197+
198+
# Make sure the call chains are also correct.
199+
self.assertEqual(
200+
all_columns[0]._meta.call_chain,
201+
Concert.band_1.manager.id._meta.call_chain,
202+
)
203+
self.assertEqual(
204+
all_columns[1]._meta.call_chain,
205+
Concert.band_1.manager.name._meta.call_chain,
203206
)

tests/query/test_mixins.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from unittest import TestCase
2+
3+
from piccolo.query.mixins import ColumnsDelegate
4+
from tests.example_app.tables import Band
5+
6+
7+
class TestColumnsDelegate(TestCase):
8+
def test_list_unpacking(self):
9+
"""
10+
The ``ColumnsDelegate`` should unpack a list of columns if passed in by
11+
mistake, without the user unpacking them explicitly.
12+
13+
.. code-block:: python
14+
15+
# These two should both work the same:
16+
await Band.select([Band.id, Band.name]).run()
17+
await Band.select(Band.id, Band.name).run()
18+
19+
"""
20+
columns_delegate = ColumnsDelegate()
21+
22+
columns_delegate.columns([Band.name])
23+
self.assertEqual(columns_delegate.selected_columns, [Band.name])
24+
25+
columns_delegate.columns([Band.id])
26+
self.assertEqual(
27+
columns_delegate.selected_columns, [Band.name, Band.id]
28+
)

tests/table/test_delete.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ def test_delete(self):
1111
Band.delete().where(Band.name == "CSharps").run_sync()
1212

1313
response = Band.count().where(Band.name == "CSharps").run_sync()
14-
print(f"response = {response}")
1514

1615
self.assertEqual(response, 0)
1716

tests/table/test_join.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@ def setUp(self):
2525
for table in self.tables:
2626
table.create_table().run_sync()
2727

28-
def tearDown(self):
29-
for table in reversed(self.tables):
30-
table.alter().drop_table().run_sync()
31-
32-
def test_join(self):
3328
manager_1 = Manager(name="Guido")
3429
manager_1.save().run_sync()
3530

@@ -42,14 +37,19 @@ def test_join(self):
4237
band_2 = Band(name="Rustaceans", manager=manager_2.id)
4338
band_2.save().run_sync()
4439

45-
venue = Venue(name="Grand Central")
40+
venue = Venue(name="Grand Central", capacity=1000)
4641
venue.save().run_sync()
4742

4843
save_query = Concert(
4944
band_1=band_1.id, band_2=band_2.id, venue=venue.id
5045
).save()
5146
save_query.run_sync()
5247

48+
def tearDown(self):
49+
for table in reversed(self.tables):
50+
table.alter().drop_table().run_sync()
51+
52+
def test_join(self):
5353
select_query = Concert.select(
5454
Concert.band_1.name,
5555
Concert.band_2.name,
@@ -73,3 +73,49 @@ def test_join(self):
7373
select_query = Concert.select(Concert.band_1.manager.name)
7474
response = select_query.run_sync()
7575
self.assertEqual(response, [{"band_1.manager.name": "Guido"}])
76+
77+
def test_all_columns(self):
78+
"""
79+
Make sure you can retrieve all columns from a related table, without
80+
explicitly specifying them.
81+
"""
82+
result = (
83+
Band.select(Band.name, *Band.manager.all_columns())
84+
.first()
85+
.run_sync()
86+
)
87+
self.assertEqual(
88+
result,
89+
{
90+
"name": "Pythonistas",
91+
"manager.id": 1,
92+
"manager.name": "Guido",
93+
},
94+
)
95+
96+
def test_all_columns_deep(self):
97+
"""
98+
Make sure that ``all_columns`` can be used several layers deep.
99+
"""
100+
result = (
101+
Concert.select(
102+
*Concert.venue.all_columns(),
103+
*Concert.band_1.manager.all_columns(),
104+
*Concert.band_2.manager.all_columns(),
105+
)
106+
.first()
107+
.run_sync()
108+
)
109+
110+
self.assertEqual(
111+
result,
112+
{
113+
"venue.id": 1,
114+
"venue.name": "Grand Central",
115+
"venue.capacity": 1000,
116+
"band_1.manager.id": 1,
117+
"band_1.manager.name": "Guido",
118+
"band_2.manager.id": 2,
119+
"band_2.manager.name": "Graydon",
120+
},
121+
)

tests/table/test_objects.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ def test_offset_postgres(self):
3232
"""
3333
self.insert_rows()
3434
response = Band.objects().order_by(Band.name).offset(1).run_sync()
35-
36-
print(f"response = {response}")
37-
3835
self.assertEqual(
3936
[i.name for i in response], ["Pythonistas", "Rustaceans"]
4037
)
@@ -54,8 +51,6 @@ def test_offset_sqlite(self):
5451

5552
response = query.run_sync()
5653

57-
print(f"response = {response}")
58-
5954
self.assertEqual(
6055
[i.name for i in response], ["Pythonistas", "Rustaceans"]
6156
)

tests/table/test_select.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ def test_query_all_columns(self):
1313
self.insert_row()
1414

1515
response = Band.select().run_sync()
16-
print(f"response = {response}")
1716

1817
self.assertDictEqual(
1918
response[0],
@@ -24,7 +23,6 @@ def test_query_some_columns(self):
2423
self.insert_row()
2524

2625
response = Band.select(Band.name).run_sync()
27-
print(f"response = {response}")
2826

2927
self.assertDictEqual(response[0], {"name": "Pythonistas"})
3028

0 commit comments

Comments
 (0)