From 17dc57824f0a4dff2ba7086e2144c326b3114daf Mon Sep 17 00:00:00 2001 From: Alexandre Lavigne Date: Mon, 3 Jun 2024 00:36:59 +0200 Subject: [PATCH 1/6] [feature] Add 'expand_table' feature Add a new feature that allows a user to expand a cell range into a table. the expand will look for the right most cell with adjacent value. the expand will look for the bottom most cell with adjacent value. the expand will table down from top left celle range to bottom right value. closes #1414 Signed-off-by: Alexandre Lavigne --- gspread/utils.py | 130 +++++++++++++++++++++++++++++++++++++++++++ gspread/worksheet.py | 55 ++++++++++++++++++ tests/utils_test.py | 51 +++++++++++++++++ 3 files changed, 236 insertions(+) diff --git a/gspread/utils.py b/gspread/utils.py index 6f3fedef2..fc2ab7875 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -168,6 +168,12 @@ class ValidationConditionType(StrEnum): filter_expression = "FILTER_EXPRESSION" +class TableDirection(StrEnum): + table = "TABLE" + down = "DOWN" + right = "RIGHT" + + def convert_credentials(credentials: Credentials) -> Credentials: module = credentials.__module__ cls = credentials.__class__.__name__ @@ -979,6 +985,130 @@ def to_records( return [dict(zip(headers, row)) for row in values] +def _expand_right(values: List[List[str]], start: int, end: int, row: int) -> int: + """This is a private function, returning the column index of the first empty cell + on the given row. + + Search starts from ``start`` index column. + Search ends on ``end`` index column. + Searches only in the row pointed by ``row``. + + If no empty value is found, it will return the given ``end`` index. + """ + for column in range(start, end): + # in case the given row is smaller that what is being asked + if column >= len(values[row]): + return len(values[row]) - 1 + + if values[row][column] == "": + return column + + return end + + +def _expand_bottom(values: List[List[str]], start: int, end: int, col: int) -> int: + """This is a private function, returning the row index of the first empty cell + on the given column. + + Search starts from ``start`` index row. + Search ends on ``end`` index row. + Searches only in the column pointed by ``col``. + + If no empty value is found, it will return the given ``end`` index. + """ + for rows in range(start, end): + # in case we try to look further than last row + if rows >= len(values): + return len(values) - 1 + + # this row is smaller than the others, just keep looking + if col >= len(values[rows]): + continue + + if values[rows][col] == "": + return rows + + return end + + +def find_table( + values: List[List[str]], + start_range: str, + direction: TableDirection = TableDirection.table, +) -> List[List[str]]: + """Expands a list of values based on non-null adjacent cells. + + Expand can be done in 3 directions defined in :class:`~gspread.utils.TableDirection` + + * ``TableDirection.right``: expands right until the first empty cell + * ``TableDirection.down``: expands down until the first empty cell + * ``TableDirection.table``: expands right until the first empty cell, then down until the first empty cell + + Regardless of the direction this function always returns a matrix of data, even if it has + only one column. + + Example:: + + values = [ + ['', '', '', '', '' ], + ['', 'B2', 'C2', '', 'E2'], + ['', 'B3', 'C3', '', 'E3'], + ['', '' , '' , '', 'E4'], + ] + >>> utils.find_table(TableDirection.table, 'B2') + [ + ['B2', 'C2'], + ['B3', 'C3'], + ] + + + .. note:: + + the ``TableDirection.table`` will first look right, then look down. + It will not check cells located inside the table. This could lead to + potential empty values located in the middle of the table. + + .. warning:: + + Given values must be padded with `''` empty values. + + :param list[list] values: values where to find the table. + :param gspread.utils.TableDirection direction: the expand direction. + :param str start_range: the starting cell range. + :rtype list(list): the resulting matrix + """ + row, col = a1_to_rowcol(start_range) + + # a1_to_rowcol returns coordinates starting form 1 + row -= 1 + col -= 1 + + if direction == TableDirection.down: + rightMost = col + 1 + bottomMost = _expand_bottom(values, row, len(values), col) + + if direction == TableDirection.right: + rightMost = _expand_right(values, col, len(values[row]), row) + bottomMost = row + 1 + + if direction == TableDirection.table: + rightMost = _expand_right(values, col, len(values[row]), row) + + checkColumn = rightMost + if checkColumn != 0: + checkColumn -= 1 + + bottomMost = _expand_bottom(values, row, len(values), checkColumn) + + result = [] + + # build resulting array + for rows in values[row:bottomMost]: + result.append(rows[col:rightMost]) + + return result + + # SHOULD NOT BE NEEDED UNTIL NEXT MAJOR VERSION # DEPRECATION_WARNING_TEMPLATE = ( # "[Deprecated][in version {v_deprecated}]: {msg_deprecated}" diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 463a72786..7eadd454f 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -41,6 +41,7 @@ PasteOrientation, PasteType, T, + TableDirection, ValidationConditionType, ValueInputOption, ValueRenderOption, @@ -53,6 +54,7 @@ convert_colors_to_hex_value, convert_hex_to_colors_dict, fill_gaps, + find_table, finditem, get_a1_from_absolute_range, is_full_a1_notation, @@ -3336,3 +3338,56 @@ def add_validation( } return self.client.batch_update(self.spreadsheet_id, body) + + def expand( + self, + top_left_range_name: str = "A1", + direction: TableDirection = TableDirection.table, + ) -> List[List[str]]: + """Expands a cell range based on non-null adjacent cells. + + Expand can be done in 3 directions defined in :class:`~gspread.utils.TableDirection` + + * ``TableDirection.right``: expands right until the first empty cell + * ``TableDirection.down``: expands down until the first empty cell + * ``TableDirection.table``: expands right until the first empty cell, then down until the first empty cell + + Regardless of the direction this function always returns a matrix of data, even if it has + only one column. + + Example:: + + values = [ + ['', '', '', '' , '' , ''], + ['', 'B2', 'C2', 'D2', '' , 'F2'], + ['', 'B3', '' , 'D3', '' , 'F3'], + ['', 'B4', 'C4', 'D4', '' , 'F4'], + ['', '' , '' , '' , '' , 'F5'], + ] + >>> worksheet.expand_table(TableDirection.table, 'B2') + [ + ['B2', 'C2', 'D2], + ['B3', '' , 'D3'], + ['B4', 'C4', 'D4'], + ] + + + .. note:: + + the ``TableDirection.table`` will first look right, then look down. + It will not check cells located inside the table. This could lead to + potential empty values located in the middle of the table. + + .. note:: + + when it is necessary to use non-default options for :meth:`~gspread.worksheet.Worksheet.get`, + please get the data first using desired options then use the function + :func:`gspread.utils.find_table` to extract the desired table. + + :param str top_left_range_name: the top left corner of the table to expand. + :param gspread.utils.TableDirection direction: the expand direction + :rtype list(list): the resulting matrix + """ + + values = self.get(pad_values=True) + return find_table(values, top_left_range_name, direction) diff --git a/tests/utils_test.py b/tests/utils_test.py index 187e74eb0..47eb33fd6 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -503,3 +503,54 @@ def test_to_records(self): # given key are unordered # but they must match a value from the given input values self.assertIn(record[key], values[i]) + + def test_find_table(self): + """Test find table function""" + + values = [ + ["A1", "B1", "C1", "D1"], + ["", "B2", "C2", "", "D2"], + ["", "B3", "C3", "", "D3"], + ["", "", "", "", "D4"], + ] + + table = utils.find_table( + values, + "B2", + utils.TableDirection.table, + ) + right = utils.find_table( + values, + "B2", + utils.TableDirection.right, + ) + down = utils.find_table( + values, + "B2", + utils.TableDirection.down, + ) + single = utils.find_table(values, "C3", utils.TableDirection.table) + no_values = utils.find_table(values, "A2", utils.TableDirection.table) + + table_values = [ + ["B2", "C2"], + ["B3", "C3"], + ] + for row in range(len(table)): + self.assertListEqual(table[row], table_values[row]) + + right_values = [ + ["B2", "C2"], + ] + for row in range(len(right)): + self.assertListEqual(right[row], right_values[row]) + + bottom_values = [ + ["B2"], + ["B3"], + ] + for row in range(len(down)): + self.assertListEqual(down[row], bottom_values[row]) + + self.assertEqual(single[0][0], "C3") + self.assertEqual(no_values, []) From c4ed289d24f405ad0e2cf0a9b76a1ad65078e503 Mon Sep 17 00:00:00 2001 From: alifeee Date: Sat, 29 Jun 2024 16:54:31 +0100 Subject: [PATCH 2/6] add some more tests for "edge" cases --- tests/utils_test.py | 107 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/tests/utils_test.py b/tests/utils_test.py index 47eb33fd6..6bd10fa2d 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -504,9 +504,8 @@ def test_to_records(self): # but they must match a value from the given input values self.assertIn(record[key], values[i]) - def test_find_table(self): - """Test find table function""" - + def test_find_table_simple(self): + """Test find table with basic case""" values = [ ["A1", "B1", "C1", "D1"], ["", "B2", "C2", "", "D2"], @@ -536,21 +535,111 @@ def test_find_table(self): ["B2", "C2"], ["B3", "C3"], ] - for row in range(len(table)): - self.assertListEqual(table[row], table_values[row]) + for rowindex, row in enumerate(table): + self.assertListEqual(row, table_values[rowindex]) right_values = [ ["B2", "C2"], ] - for row in range(len(right)): - self.assertListEqual(right[row], right_values[row]) + for rowindex, row in enumerate(right): + self.assertListEqual(row, right_values[rowindex]) bottom_values = [ ["B2"], ["B3"], ] - for row in range(len(down)): - self.assertListEqual(down[row], bottom_values[row]) + for rowindex, row in enumerate(down): + self.assertListEqual(row, bottom_values[rowindex]) self.assertEqual(single[0][0], "C3") self.assertEqual(no_values, []) + + def test_find_table_header_gap(self): + """Test find table with gap in header""" + values = [ + ["A1", "", "C1", ""], + ["A2", "B2", "C2", ""], + ["A3", "B3", "C3", ""], + ["", "", "", ""], + ] + expected_table = [ + ["A1"], + ["A2"], + ["A3"], + ] + + table = utils.find_table( + values, + "A1", + utils.TableDirection.table, + ) + + for rowindex, row in enumerate(table): + self.assertListEqual(row, expected_table[rowindex]) + + def test_find_table_empty_first_cell(self): + """Test find table with first cell empty""" + values = [ + ["", "B1", "C1", ""], + ["A2", "B2", "C2", ""], + ["A3", "B3", "C3", ""], + ["", "", "", ""], + ] + expected_table = [ + ["", "B1", "C1"], + ["A2", "B2", "C2"], + ["A3", "B3", "C3"], + ] + + table = utils.find_table( + values, + "A1", + utils.TableDirection.table, + ) + + for rowindex, row in enumerate(table): + self.assertListEqual(row, expected_table[rowindex]) + + def test_find_table_first_column_gap(self): + """Test find table with a gap in first column""" + values = [ + ["A1", "B1", "C1", ""], + ["", "B2", "C2", ""], + ["A3", "B3", "C3", ""], + ["", "", "", ""], + ] + expected_table = [ + ["A1", "B1", "C1"], + ["", "B2", "C2"], + ["A3", "B3", "C3"], + ] + + table = utils.find_table( + values, + "A1", + utils.TableDirection.table, + ) + + for rowindex, row in enumerate(table): + self.assertListEqual(row, expected_table[rowindex]) + + def test_find_table_last_column_gap(self): + """Test find table with a gap in last column""" + values = [ + ["A1", "B1", "C1", ""], + ["A2", "B2", "", ""], + ["A3", "B3", "C3", ""], + ["", "", "", ""], + ] + expected_table = [ + ["A1", "B1", "C1"], + ] + + table = utils.find_table( + values, + "A1", + utils.TableDirection.table, + ) + + for rowindex, row in enumerate(table): + self.assertListEqual(row, expected_table[rowindex]) From 4d7c2e997adfca27763dd32c77363c43723d65d2 Mon Sep 17 00:00:00 2001 From: alifeee Date: Tue, 9 Jul 2024 01:26:48 +0100 Subject: [PATCH 3/6] update tests to reflect expected behaviour (failing) --- tests/utils_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/utils_test.py b/tests/utils_test.py index 6bd10fa2d..19ab245ae 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -508,9 +508,9 @@ def test_find_table_simple(self): """Test find table with basic case""" values = [ ["A1", "B1", "C1", "D1"], - ["", "B2", "C2", "", "D2"], - ["", "B3", "C3", "", "D3"], - ["", "", "", "", "D4"], + ["", "B2", "C2", "", "E2"], + ["", "B3", "C3", "D3", "E3"], + ["A4", "", "C4", "D4", "E4"], ] table = utils.find_table( @@ -610,8 +610,6 @@ def test_find_table_first_column_gap(self): ] expected_table = [ ["A1", "B1", "C1"], - ["", "B2", "C2"], - ["A3", "B3", "C3"], ] table = utils.find_table( @@ -633,6 +631,8 @@ def test_find_table_last_column_gap(self): ] expected_table = [ ["A1", "B1", "C1"], + ["A2", "B2", ""], + ["A3", "B3", "C3"], ] table = utils.find_table( From 3296c5910cb18b6d1b096e4726e0f6aaff7c53c5 Mon Sep 17 00:00:00 2001 From: alifeee Date: Tue, 9 Jul 2024 01:27:01 +0100 Subject: [PATCH 4/6] update `find_table` to return correct behaviour --- gspread/utils.py | 71 ++++++++++++++++++------------------- gspread/worksheet.py | 26 +++++++------- tests/utils_test.py | 84 ++++++++++++++++++++++++++++++-------------- 3 files changed, 105 insertions(+), 76 deletions(-) diff --git a/gspread/utils.py b/gspread/utils.py index fc2ab7875..4cc738ccf 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -986,49 +986,37 @@ def to_records( def _expand_right(values: List[List[str]], start: int, end: int, row: int) -> int: - """This is a private function, returning the column index of the first empty cell + """This is a private function, returning the column index of the last non empty cell on the given row. Search starts from ``start`` index column. Search ends on ``end`` index column. Searches only in the row pointed by ``row``. - - If no empty value is found, it will return the given ``end`` index. """ - for column in range(start, end): - # in case the given row is smaller that what is being asked - if column >= len(values[row]): - return len(values[row]) - 1 - - if values[row][column] == "": - return column - - return end + try: + return values[row].index("", start, end) - 1 + except ValueError: + return end def _expand_bottom(values: List[List[str]], start: int, end: int, col: int) -> int: - """This is a private function, returning the row index of the first empty cell + """This is a private function, returning the row index of the last non empty cell on the given column. Search starts from ``start`` index row. Search ends on ``end`` index row. Searches only in the column pointed by ``col``. - - If no empty value is found, it will return the given ``end`` index. """ for rows in range(start, end): # in case we try to look further than last row if rows >= len(values): return len(values) - 1 - # this row is smaller than the others, just keep looking - if col >= len(values[rows]): - continue - - if values[rows][col] == "": - return rows + # check if cell is empty (or the row => empty cell) + if col >= len(values[rows]) or values[rows][col] == "": + return rows - 1 - return end + return end - 1 def find_table( @@ -1042,10 +1030,12 @@ def find_table( * ``TableDirection.right``: expands right until the first empty cell * ``TableDirection.down``: expands down until the first empty cell - * ``TableDirection.table``: expands right until the first empty cell, then down until the first empty cell + * ``TableDirection.table``: expands right until the first empty cell and down until first empty cell - Regardless of the direction this function always returns a matrix of data, even if it has - only one column. + In case of empty result an empty list is restuned. + + When the given ``start_range`` is outside the given matrix of values the exception + `~gspread.exceptions.InvalidInputValue` is raised. Example:: @@ -1064,7 +1054,7 @@ def find_table( .. note:: - the ``TableDirection.table`` will first look right, then look down. + the ``TableDirection.table`` will look right from starting cell then look down from starting cell. It will not check cells located inside the table. This could lead to potential empty values located in the middle of the table. @@ -1083,28 +1073,37 @@ def find_table( row -= 1 col -= 1 + if row >= len(values): + raise InvalidInputValue( + "given row for start_range is outside given values: start range row ({}) >= rows in values {}".format( + row, len(values) + ) + ) + + if col >= len(values[row]): + raise InvalidInputValue( + "given column for start_range is outside given values: start range column ({}) >= columns in values {}".format( + col, len(values[row]) + ) + ) + if direction == TableDirection.down: - rightMost = col + 1 + rightMost = col bottomMost = _expand_bottom(values, row, len(values), col) if direction == TableDirection.right: + bottomMost = row rightMost = _expand_right(values, col, len(values[row]), row) - bottomMost = row + 1 if direction == TableDirection.table: rightMost = _expand_right(values, col, len(values[row]), row) - - checkColumn = rightMost - if checkColumn != 0: - checkColumn -= 1 - - bottomMost = _expand_bottom(values, row, len(values), checkColumn) + bottomMost = _expand_bottom(values, row, len(values), col) result = [] # build resulting array - for rows in values[row:bottomMost]: - result.append(rows[col:rightMost]) + for rows in values[row : bottomMost + 1]: + result.append(rows[col : rightMost + 1]) return result diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 7eadd454f..bd9eefe50 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -3350,31 +3350,31 @@ def expand( * ``TableDirection.right``: expands right until the first empty cell * ``TableDirection.down``: expands down until the first empty cell - * ``TableDirection.table``: expands right until the first empty cell, then down until the first empty cell + * ``TableDirection.table``: expands right until the first empty cell and down until the first empty cell - Regardless of the direction this function always returns a matrix of data, even if it has - only one column. + In case of empty result an empty list is restuned. + + When the given ``start_range`` is outside the given matrix of values the exception + `~gspread.exceptions.InvalidInputValue` is raised. Example:: values = [ - ['', '', '', '' , '' , ''], - ['', 'B2', 'C2', 'D2', '' , 'F2'], - ['', 'B3', '' , 'D3', '' , 'F3'], - ['', 'B4', 'C4', 'D4', '' , 'F4'], - ['', '' , '' , '' , '' , 'F5'], + ['', '', '', '', '' ], + ['', 'B2', 'C2', '', 'E2'], + ['', 'B3', 'C3', '', 'E3'], + ['', '' , '' , '', 'E4'], ] - >>> worksheet.expand_table(TableDirection.table, 'B2') + >>> utils.find_table(TableDirection.table, 'B2') [ - ['B2', 'C2', 'D2], - ['B3', '' , 'D3'], - ['B4', 'C4', 'D4'], + ['B2', 'C2'], + ['B3', 'C3'], ] .. note:: - the ``TableDirection.table`` will first look right, then look down. + the ``TableDirection.table`` will look right from starting cell then look down from starting cell. It will not check cells located inside the table. This could lead to potential empty values located in the middle of the table. diff --git a/tests/utils_test.py b/tests/utils_test.py index 19ab245ae..f64b759ae 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -518,6 +518,12 @@ def test_find_table_simple(self): "B2", utils.TableDirection.table, ) + + table_max_row_max_column = utils.find_table( + values, + "D3", + utils.TableDirection.table, + ) right = utils.find_table( values, "B2", @@ -528,44 +534,55 @@ def test_find_table_simple(self): "B2", utils.TableDirection.down, ) - single = utils.find_table(values, "C3", utils.TableDirection.table) + single = utils.find_table(values, "D1", utils.TableDirection.table) no_values = utils.find_table(values, "A2", utils.TableDirection.table) table_values = [ ["B2", "C2"], ["B3", "C3"], ] - for rowindex, row in enumerate(table): - self.assertListEqual(row, table_values[rowindex]) + + for rowindex, row in enumerate(table_values): + self.assertListEqual(row, table[rowindex]) + + table_max_row_max_column_values = [ + ["D3", "E3"], + ["D4", "E4"], + ] + + for rowindex, row in enumerate(table_max_row_max_column): + self.assertListEqual(row, table_max_row_max_column_values[rowindex]) right_values = [ ["B2", "C2"], ] - for rowindex, row in enumerate(right): - self.assertListEqual(row, right_values[rowindex]) + for rowindex, row in enumerate(right_values): + self.assertListEqual(row, right[rowindex]) bottom_values = [ ["B2"], ["B3"], ] - for rowindex, row in enumerate(down): - self.assertListEqual(row, bottom_values[rowindex]) + for rowindex, row in enumerate(bottom_values): + self.assertListEqual(row, down[rowindex]) - self.assertEqual(single[0][0], "C3") + self.assertEqual(len(single), 1) + self.assertEqual(len(single[0]), 1) + self.assertEqual(single[0][0], "D1") self.assertEqual(no_values, []) - def test_find_table_header_gap(self): + def test_find_table_inner_gap(self): """Test find table with gap in header""" values = [ - ["A1", "", "C1", ""], - ["A2", "B2", "C2", ""], + ["A1", "B1", "C1", ""], + ["A2", "", "C2", ""], ["A3", "B3", "C3", ""], ["", "", "", ""], ] expected_table = [ - ["A1"], - ["A2"], - ["A3"], + ["A1", "B1", "C1"], + ["A2", "", "C2"], + ["A3", "B3", "C3"], ] table = utils.find_table( @@ -574,21 +591,21 @@ def test_find_table_header_gap(self): utils.TableDirection.table, ) - for rowindex, row in enumerate(table): - self.assertListEqual(row, expected_table[rowindex]) + for rowindex, row in enumerate(expected_table): + self.assertListEqual(row, table[rowindex]) - def test_find_table_empty_first_cell(self): + def test_find_table_first_row_gap(self): """Test find table with first cell empty""" values = [ - ["", "B1", "C1", ""], + ["A1", "", "C1", ""], ["A2", "B2", "C2", ""], ["A3", "B3", "C3", ""], ["", "", "", ""], ] expected_table = [ - ["", "B1", "C1"], - ["A2", "B2", "C2"], - ["A3", "B3", "C3"], + ["A1"], + ["A2"], + ["A3"], ] table = utils.find_table( @@ -597,8 +614,8 @@ def test_find_table_empty_first_cell(self): utils.TableDirection.table, ) - for rowindex, row in enumerate(table): - self.assertListEqual(row, expected_table[rowindex]) + for rowindex, row in enumerate(expected_table): + self.assertListEqual(row, table[rowindex]) def test_find_table_first_column_gap(self): """Test find table with a gap in first column""" @@ -618,8 +635,8 @@ def test_find_table_first_column_gap(self): utils.TableDirection.table, ) - for rowindex, row in enumerate(table): - self.assertListEqual(row, expected_table[rowindex]) + for rowindex, row in enumerate(expected_table): + self.assertListEqual(row, table[rowindex]) def test_find_table_last_column_gap(self): """Test find table with a gap in last column""" @@ -641,5 +658,18 @@ def test_find_table_last_column_gap(self): utils.TableDirection.table, ) - for rowindex, row in enumerate(table): - self.assertListEqual(row, expected_table[rowindex]) + for rowindex, row in enumerate(expected_table): + self.assertListEqual(row, table[rowindex]) + + def test_find_table_empty_top_left_corner(self): + """Test find table with an empty top left cell and empty adjacent cells""" + + values = [ + ["", "", "C1", ""], + ["", "B2", "C2", ""], + ["", "B3", "C3", ""], + ] + + table = utils.find_table(values, "A1", utils.TableDirection.table) + + self.assertListEqual(table, [], "resulting table should be empty") From 858d77d05f716c350f8ebabe73bd1085531a392d Mon Sep 17 00:00:00 2001 From: Alexandre Lavigne Date: Mon, 23 Sep 2024 00:24:24 +0200 Subject: [PATCH 5/6] doc: add documentation on how to use `worksheet.expand()`. Signed-off-by: Alexandre Lavigne --- docs/user-guide.rst | 68 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/user-guide.rst b/docs/user-guide.rst index cc4599656..50b90971f 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -350,7 +350,73 @@ Check out the api docs for `DataValidationRule`_ and `CondtionType`_ for more de .. _CondtionType: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType -.. _DataValidationRule: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule +.. _DataValidationRule: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule + +Extract table +~~~~~~~~~~~~~ + +Gspread provides a function to extract a data table. +A data table is defined as a rectangular table that stops either on the **first empty** cell or +the **enge of the sheet**. + +You can extract table from any address by providing the top left corner of the desired table. + +Gspread provides 3 directions for searching the end of the table: + + * :attr:`~gspread.utils.TableDirection.right`: extract a single row searching on the right of the starting cell + * :attr:`~gspread.utils.TableDirection.down`: extract a single column searching on the bottom of the starting cell + * :attr:`~gspread.utils.TableDirection.table`: extract a rectangular table by first searching right from starting cell, + then searching down from starting cell. + + .. note:: + + Gspread will not look for empty cell inside the table. it only look at the top row and first column. + +Example extracting a table from the below sample sheet: + +.. list-table:: Find table + :header-rows: 1 + + * - ID + - Name + - Universe + - Super power + * - 1 + - Batman + - DC + - Very rich + * - 2 + - DeadPool + - Marvel + - self healing + * - 3 + - Superman + - DC + - super human + * - + - \- + - \- + - \- + * - 5 + - Lavigne958 + - + - maintains Gspread + * - 6 + - Alifee + - + - maintains Gspread + +Using the below code will result in rows 2 to 4: + +.. code:: python + + worksheet.expand("A2") + + [ + ["Batman", "DC", "Very rich"], + ["DeadPool", "Marvel", "self healing"], + ["Superman", "DC", "super human"], + ] From 6bb5a1a80869e2fed3ace997a3110a79b5861483 Mon Sep 17 00:00:00 2001 From: Alexandre Lavigne Date: Wed, 25 Sep 2024 18:04:46 +0200 Subject: [PATCH 6/6] fix docstring Signed-off-by: Alexandre Lavigne --- gspread/utils.py | 2 +- gspread/worksheet.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gspread/utils.py b/gspread/utils.py index 4cc738ccf..6daac8616 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -1035,7 +1035,7 @@ def find_table( In case of empty result an empty list is restuned. When the given ``start_range`` is outside the given matrix of values the exception - `~gspread.exceptions.InvalidInputValue` is raised. + :class:`~gspread.exceptions.InvalidInputValue` is raised. Example:: diff --git a/gspread/worksheet.py b/gspread/worksheet.py index bd9eefe50..109c44db7 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -3355,7 +3355,7 @@ def expand( In case of empty result an empty list is restuned. When the given ``start_range`` is outside the given matrix of values the exception - `~gspread.exceptions.InvalidInputValue` is raised. + :class:`~gspread.exceptions.InvalidInputValue` is raised. Example::