Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 44 additions & 12 deletions gspread/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,31 +679,63 @@ def fill_gaps(
return [[]]


def cell_list_to_rect(cell_list: List["Cell"]) -> List[List[Optional[str]]]:
def cell_list_to_rect(
cell_list: List["Cell"],
) -> Tuple[List[List[Optional[str]]], List[List[Optional[int]]]]:
"""Convert a list of Cells into a rectangular 2D grid of values.

:param cell_list: List of Cells.
:type cell_list: list[Cell]

:return: Tuple of (values_rect, reverse_map) where:
- values_rect is a 2D list of strings or None for missing cells.
- reverse_map is a 2D list of indices into the original cell_list,
or None for missing cells.
:rtype: tuple[list[list[Optional[str]]], list[list[Optional[int]]]]

Example::
>>> cells = [
... Cell(row=1, col=1, value="A"),
... Cell(row=2, col=1, value="C"),
... Cell(row=1, col=2, value="B"),
... ]
>>> values_rect, reverse_map = cell_list_to_rect(cells)
>>> values_rect
[['A', 'B'],
['C', None]]
>>> reverse_map
[[0, 2],
[1, None]]
"""
if not cell_list:
return []
return [[]], [[]]

rows: Dict[int, Dict[int, Optional[str]]] = defaultdict(dict)
rows: Dict[int, Dict[int, int]] = defaultdict(dict)

row_offset = min(c.row for c in cell_list)
col_offset = min(c.col for c in cell_list)

for cell in cell_list:
row = rows.setdefault(int(cell.row) - row_offset, {})
row[cell.col - col_offset] = cell.value
for idx, cell in enumerate(cell_list):
rows[int(cell.row) - row_offset][cell.col - col_offset] = idx

if not rows:
return []
return [[]], [[]]

all_row_keys = chain.from_iterable(row.keys() for row in rows.values())
rect_cols = range(max(all_row_keys) + 1)
rect_rows = range(max(rows.keys()) + 1)

# Return the values of the cells as a list of lists where each sublist
# contains all of the values for one row. The Google API requires a rectangle
# of updates, so if a cell isn't present in the input cell_list, then the
# value will be None and will not be updated.
return [[rows[i].get(j) for j in rect_cols] for i in rect_rows]
values_rect, reverse_map = [], []
for i in rect_rows:
val_row, map_row = [], []
for j in rect_cols:
cur_idx = rows[i].get(j)
val_row.append(cell_list[cur_idx].value if cur_idx is not None else None)
map_row.append(cur_idx)
values_rect.append(val_row)
reverse_map.append(map_row)

return values_rect, reverse_map


def quote(value: str, safe: str = "", encoding: str = "utf-8") -> str:
Expand Down
24 changes: 21 additions & 3 deletions gspread/worksheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,8 @@ def update_cells(
self,
cell_list: List[Cell],
value_input_option: ValueInputOption = ValueInputOption.raw,
) -> Mapping[str, Any]:
include_values_in_response: Optional[bool] = None,
) -> JSONResponse:
"""Updates many cells at once.

:param list cell_list: List of :class:`gspread.cell.Cell` objects to update.
Expand All @@ -792,6 +793,9 @@ def update_cells(

:type value_input_option: :namedtuple:`~gspread.utils.ValueInputOption`

:param bool include_values_in_response: (optional) Determines if the update response
should include the values of the cells that were updated.

.. _ValueInputOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption

Example::
Expand All @@ -805,7 +809,7 @@ def update_cells(
# Update in batch
worksheet.update_cells(cell_list)
"""
values_rect = cell_list_to_rect(cell_list)
values_rect, reverse_map = cell_list_to_rect(cell_list)

start = rowcol_to_a1(
min(c.row for c in cell_list), min(c.col for c in cell_list)
Expand All @@ -814,13 +818,27 @@ def update_cells(

range_name = absolute_range_name(self.title, "{}:{}".format(start, end))

params: ParamsType = {
"valueInputOption": value_input_option,
"includeValuesInResponse": include_values_in_response,
}

data = self.client.values_update(
self.spreadsheet_id,
range_name,
params={"valueInputOption": value_input_option},
params=params,
body={"values": values_rect},
)

if include_values_in_response:
# reformat values to original cell_list order
result = [None] * len(cell_list)
for r, row in enumerate(reverse_map):
for c, idx in enumerate(row):
if idx is not None:
result[idx] = data["updatedData"]["values"][r][c]
data["updatedData"]["values"] = result

return data

def get(
Expand Down
Loading
Loading