Skip to content

Commit a612a49

Browse files
authored
Fix sheet names quoting (#748)
Extend `utils.absolute_range_name` to handle missing `range_name` parameter, wrap sheet name in single quotes and escape single quotes. Use `utils.absolute_range_name` throughout `gspread.models` to construct proper range names.
1 parent 8f13a9b commit a612a49

3 files changed

Lines changed: 737 additions & 86 deletions

File tree

gspread/models.py

Lines changed: 80 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,9 @@ def reorder_worksheets(self, worksheets_in_desired_order):
430430
Note: If you omit some of the Spreadsheet's existing Worksheet objects from
431431
the provided sequence, those Worksheets will be appended to the end of the sequence
432432
in the order that they appear in the list returned by ``Spreadsheet.worksheets()``.
433+
434+
.. versionadded:: 3.4
435+
433436
"""
434437
idx_map = {}
435438
for idx, w in enumerate(worksheets_in_desired_order):
@@ -646,14 +649,13 @@ def cell(self, row, col, value_render_option='FORMATTED_VALUE'):
646649
647650
"""
648651

649-
range_label = '%s!%s' % (self.title, rowcol_to_a1(row, col))
650-
data = self.spreadsheet.values_get(
651-
range_label,
652-
params={'valueRenderOption': value_render_option}
653-
)
654-
655652
try:
656-
value = data['values'][0][0]
653+
data = self.get(
654+
rowcol_to_a1(row, col),
655+
value_render_option=value_render_option
656+
)
657+
658+
value = data.first()
657659
except KeyError:
658660
value = ''
659661

@@ -690,7 +692,7 @@ def range(self, name):
690692
691693
"""
692694

693-
range_label = '%s!%s' % (self.title, name)
695+
range_label = absolute_range_name(self.title, name)
694696

695697
data = self.spreadsheet.values_get(range_label)
696698

@@ -726,10 +728,10 @@ def get_all_values(self, value_render_option='FORMATTED_VALUE'):
726728
727729
Empty trailing rows and columns will not be included.
728730
"""
731+
range_name = absolute_range_name(self.title)
729732

730-
title = self.title.replace("'", "''")
731733
data = self.spreadsheet.values_get(
732-
"'{}'".format(title),
734+
range_name,
733735
params={'valueRenderOption': value_render_option}
734736
)
735737

@@ -790,7 +792,12 @@ def get_all_records(
790792

791793
return [dict(zip(keys, row)) for row in values]
792794

793-
def row_values(self, row, value_render_option='FORMATTED_VALUE'):
795+
@accepted_kwargs(
796+
major_dimension=None,
797+
value_render_option=None,
798+
date_time_render_option=None
799+
)
800+
def row_values(self, row, **kwargs):
794801
"""Returns a list of all values in a `row`.
795802
796803
Empty cells in this list will be rendered as :const:`None`.
@@ -805,16 +812,9 @@ def row_values(self, row, value_render_option='FORMATTED_VALUE'):
805812
.. _ValueRenderOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption
806813
807814
"""
808-
809-
range_label = '%s!A%s:%s' % (self.title, row, row)
810-
811-
data = self.spreadsheet.values_get(
812-
range_label,
813-
params={'valueRenderOption': value_render_option}
814-
)
815-
816815
try:
817-
return data['values'][0]
816+
data = self.get('A{}:{}'.format(row, row), **kwargs)
817+
return data[0]
818818
except KeyError:
819819
return []
820820

@@ -835,10 +835,12 @@ def col_values(self, col, value_render_option='FORMATTED_VALUE'):
835835
"""
836836

837837
start_label = rowcol_to_a1(1, col)
838-
range_label = '%s!%s:%s' % (self.title, start_label, start_label[:-1])
838+
range_label = '{}:{}'.format(start_label, start_label[:-1])
839+
840+
range_name = absolute_range_name(self.title, range_label)
839841

840842
data = self.spreadsheet.values_get(
841-
range_label,
843+
range_name,
842844
params={
843845
'valueRenderOption': value_render_option,
844846
'majorDimension': 'COLUMNS'
@@ -879,10 +881,10 @@ def update_cell(self, row, col, value):
879881
worksheet.update_cell(1, 1, '42')
880882
881883
"""
882-
range_label = '%s!%s' % (self.title, rowcol_to_a1(row, col))
884+
range_name = absolute_range_name(self.title, rowcol_to_a1(row, col))
883885

884886
data = self.spreadsheet.values_update(
885-
range_label,
887+
range_name,
886888
params={
887889
'valueInputOption': 'USER_ENTERED'
888890
},
@@ -922,10 +924,13 @@ def update_cells(self, cell_list, value_input_option='RAW'):
922924
start = rowcol_to_a1(min(c.row for c in cell_list), min(c.col for c in cell_list))
923925
end = rowcol_to_a1(max(c.row for c in cell_list), max(c.col for c in cell_list))
924926

925-
range_label = '%s!%s:%s' % (self.title, start, end)
927+
range_name = absolute_range_name(
928+
self.title,
929+
'{}:{}'.format(start, end)
930+
)
926931

927932
data = self.spreadsheet.values_update(
928-
range_label,
933+
range_name,
929934
params={
930935
'valueInputOption': value_input_option
931936
},
@@ -976,10 +981,7 @@ def get(self, range_name=None, **kwargs):
976981
.. versionadded:: 3.3
977982
978983
"""
979-
if range_name:
980-
range_name = '%s!%s' % (self.title, range_name)
981-
else:
982-
range_name = self.title
984+
range_name = absolute_range_name(self.title, range_name)
983985

984986
params = filter_dict_values({
985987
'majorDimension': kwargs['major_dimension'],
@@ -1022,7 +1024,7 @@ def batch_get(self, ranges, **kwargs):
10221024
worksheet.batch_get(['A1:B2', 'F12'])
10231025
10241026
"""
1025-
ranges = ['%s!%s' % (self.title, r) for r in ranges if r]
1027+
ranges = [absolute_range_name(self.title, r) for r in ranges if r]
10261028

10271029
params = filter_dict_values({
10281030
'majorDimension': kwargs['major_dimension'],
@@ -1093,10 +1095,10 @@ def update(self, range_name, values=None, **kwargs):
10931095
10941096
"""
10951097
if is_scalar(range_name):
1096-
range_name = '%s!%s' % (self.title, range_name)
1098+
range_name = absolute_range_name(self.title, range_name)
10971099
else:
10981100
values = range_name
1099-
range_name = self.title
1101+
range_name = absolute_range_name(self.title)
11001102

11011103
if is_scalar(values):
11021104
values = [[values]]
@@ -1290,8 +1292,9 @@ def sort(self, *specs, range=None):
12901292
# Sort range A2:G8 basing on column 'G' A -> Z and column 'B' Z -> A
12911293
wks.sort((7, 'asc'), (2, 'des'), range='A2:G8')
12921294
1293-
"""
1295+
.. versionadded:: 3.4
12941296
1297+
"""
12951298
if range:
12961299
start_a1, end_a1 = range.split(':')
12971300
start_row, start_col = a1_to_rowcol(start_a1)
@@ -1301,6 +1304,7 @@ def sort(self, *specs, range=None):
13011304
start_col = 1
13021305
end_row = self.row_count
13031306
end_col = self.col_count
1307+
13041308
request_range = {
13051309
'sheetId': self.id,
13061310
'startRowIndex': start_row - 1,
@@ -1369,8 +1373,9 @@ def update_index(self, index):
13691373
13701374
To reorder all worksheets in a spreadsheet, see `Spreadsheet.reorder_worksheets`.
13711375
1372-
"""
1376+
.. versionadded:: 3.4
13731377
1378+
"""
13741379
body = {
13751380
'requests': [{
13761381
'updateSheetProperties': {
@@ -1434,25 +1439,20 @@ def append_row(
14341439
.. _InsertDataOption: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#InsertDataOption
14351440
14361441
"""
1437-
1438-
range_label = (
1439-
'%s!%s' % (self.title, table_range)
1440-
if table_range
1441-
else self.title
1442+
return self.append_rows(
1443+
[values],
1444+
value_input_option=value_input_option,
1445+
insert_data_option=insert_data_option,
1446+
table_range=table_range
14421447
)
14431448

1444-
params = {
1445-
'valueInputOption': value_input_option,
1446-
'insertDataOption': insert_data_option
1447-
}
1448-
1449-
body = {
1450-
'values': [values]
1451-
}
1452-
1453-
return self.spreadsheet.values_append(range_label, params, body)
1454-
1455-
def append_rows(self, values, value_input_option='RAW'):
1449+
def append_rows(
1450+
self,
1451+
values,
1452+
value_input_option='RAW',
1453+
insert_data_option=None,
1454+
table_range=None
1455+
):
14561456
"""Adds multiple rows to the worksheet and populates them with values.
14571457
Widens the worksheet if there are more values than columns.
14581458
@@ -1461,19 +1461,33 @@ def append_rows(self, values, value_input_option='RAW'):
14611461
be interpreted. See `ValueInputOption`_ in
14621462
the Sheets API.
14631463
:type value_input_option: str
1464+
:param insert_data_option: (optional) Determines how the input data
1465+
should be inserted. See
1466+
`InsertDataOption`_ in the Sheets API
1467+
reference.
1468+
:type insert_data_option: str
1469+
:param table_range: (optional) The A1 notation of a range to search for
1470+
a logical table of data. Values are appended after
1471+
the last row of the table.
1472+
Examples: `A1` or `B2:D4`
1473+
:type table_range: str
14641474
14651475
.. _ValueInputOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption
1476+
.. _InsertDataOption: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#InsertDataOption
14661477
14671478
"""
1479+
range_label = absolute_range_name(self.title, table_range)
1480+
14681481
params = {
1469-
'valueInputOption': value_input_option
1482+
'valueInputOption': value_input_option,
1483+
'insertDataOption': insert_data_option
14701484
}
14711485

14721486
body = {
14731487
'values': values
14741488
}
14751489

1476-
return self.spreadsheet.values_append(self.title, params, body)
1490+
return self.spreadsheet.values_append(range_label, params, body)
14771491

14781492
def insert_row(
14791493
self,
@@ -1513,7 +1527,7 @@ def insert_row(
15131527

15141528
self.spreadsheet.batch_update(body)
15151529

1516-
range_label = '%s!%s' % (self.title, 'A%s' % index)
1530+
range_label = absolute_range_name(self.title, 'A%s' % index)
15171531

15181532
data = self.spreadsheet.values_update(
15191533
range_label,
@@ -1573,10 +1587,13 @@ def delete_rows(self, start_index, end_index):
15731587
def clear(self):
15741588
"""Clears all cells in the worksheet.
15751589
"""
1576-
return self.spreadsheet.values_clear(self.title)
1590+
return self.spreadsheet.values_clear(
1591+
absolute_range_name(self.title)
1592+
)
15771593

15781594
def _finder(self, func, query):
1579-
data = self.spreadsheet.values_get(self.title)
1595+
1596+
data = self.spreadsheet.values_get(absolute_range_name(self.title))
15801597

15811598
try:
15821599
values = fill_gaps(data['values'])
@@ -1666,6 +1683,9 @@ def add_basic_filter(self, name=None):
16661683
:param first_col: Integer row number
16671684
:param last_row: Integer row number
16681685
:param last_col: Integer row number
1686+
1687+
.. versionadded:: 3.4
1688+
16691689
"""
16701690
rng = {
16711691
'sheetId': self.id,
@@ -1696,6 +1716,9 @@ def add_basic_filter(self, name=None):
16961716

16971717
def remove_basic_filter(self):
16981718
"""Remove the basic filter from a worksheet.
1719+
1720+
.. versionadded:: 3.4
1721+
16991722
"""
17001723
body = {
17011724
'requests': [{

gspread/utils.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -425,17 +425,37 @@ def quote(value, safe='', encoding='utf-8'):
425425
return urllib.quote(value.encode(encoding), safe)
426426

427427

428-
def absolute_range_name(sheet_name, range_name):
428+
def absolute_range_name(sheet_name, range_name=None):
429429
"""Return an absolutized path of a range.
430430
431-
>>> absolute_range_name('Sheet1', 'A1:B1')
432-
'Sheet1!A1:B1'
431+
>>> absolute_range_name("Sheet1", "A1:B1")
432+
"'Sheet1'!A1:B1"
433433
434-
>>> absolute_range_name('Sheet1', 'A1')
435-
'Sheet1!A1'
434+
>>> absolute_range_name("Sheet1", "A1")
435+
"'Sheet1'!A1"
436+
437+
>>> absolute_range_name("Sheet1")
438+
"'Sheet1'"
439+
440+
>>> absolute_range_name("Sheet'1")
441+
"'Sheet''1'"
442+
443+
>>> absolute_range_name("Sheet''1")
444+
"'Sheet''''1'"
445+
446+
>>> absolute_range_name("''sheet12''", "A1:B2")
447+
"'''''sheet12'''''!A1:B2"
436448
437449
"""
438-
return '%s!%s' % (sheet_name, range_name)
450+
451+
sheet_name = "'{}'".format(
452+
sheet_name.replace("'", "''")
453+
)
454+
455+
if range_name:
456+
return '{}!{}'.format(sheet_name, range_name)
457+
else:
458+
return sheet_name
439459

440460

441461
def is_scalar(x):

0 commit comments

Comments
 (0)