From cef9fb375732e8bffcff944db5e4cde04610d13f Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:04:41 +0000 Subject: [PATCH 01/26] Added a column headers property and getters [Issue #677] --- gspread/worksheet.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 31ba6a67..ad344a82 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -199,6 +199,17 @@ def __init__( # kept for backward compatibility - publicly available # do not use if possible. self._spreadsheet = spreadsheet + self._column_headers = [] + + @property + def column_headers(self) -> List[str]: + if not self._column_headers: + self._column_headers = self.row_values(1) + return self._column_headers + + @column_headers.setter + def column_headers(self, value: List[str]) -> None: + self._column_headers = value def __repr__(self) -> str: return "<{} {} id:{}>".format( From 43a8ac2717eb7d3e197fe055137b3cdb487062cd Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:15:24 +0000 Subject: [PATCH 02/26] Created basic set_row & set_rows methods. --- gspread/worksheet.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index ad344a82..152a84ad 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -579,6 +579,7 @@ def get_all_records( return [] keys = entire_sheet[head - 1] + self.column_headers = keys values = entire_sheet[head:] if expected_headers is None: @@ -619,6 +620,20 @@ def get_all_records( return to_records(keys, values) + def set_records(self, rows: List[Dict[str, Any]]) -> None: + cols = self.column_headers + insert_rows = [] + for row in rows: + insert_row = [] + for col in cols: + insert_row.append(row[col]) + insert_rows.append(insert_row) + + self.update(insert_rows) + + def set_record(self, row: Dict[str, Any]) -> None: + self.set_records([row]) + def get_all_cells(self) -> List[Cell]: """Returns a list of all `Cell` of the current sheet.""" From d0162b192518d48bb913960ad2ea9ff96eea3618 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:57:32 +0000 Subject: [PATCH 03/26] using insert_rows instead of update --- gspread/worksheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 152a84ad..f767b113 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -629,7 +629,7 @@ def set_records(self, rows: List[Dict[str, Any]]) -> None: insert_row.append(row[col]) insert_rows.append(insert_row) - self.update(insert_rows) + self.insert_rows(insert_rows, row=self.row_count + 1) def set_record(self, row: Dict[str, Any]) -> None: self.set_records([row]) From 708b1c198af270437de682e4ec3148dddd25ab56 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Sat, 20 Jul 2024 14:58:52 +0500 Subject: [PATCH 04/26] Using `append_rows` to handle input. --- gspread/worksheet.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index f767b113..e09a48f5 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -31,7 +31,7 @@ from .cell import Cell from .exceptions import GSpreadException from .http_client import HTTPClient, ParamsType -from .urls import WORKSHEET_DRIVE_URL +from .urls import WORKSHEET_DRIVE_URL, SPREADSHEET_VALUES_APPEND_URL from .utils import ( DateTimeOption, Dimension, @@ -629,7 +629,10 @@ def set_records(self, rows: List[Dict[str, Any]]) -> None: insert_row.append(row[col]) insert_rows.append(insert_row) - self.insert_rows(insert_rows, row=self.row_count + 1) + self.append_rows( + insert_rows, + value_input_option=ValueInputOption.user_entered, + ) def set_record(self, row: Dict[str, Any]) -> None: self.set_records([row]) From 5e13729806affa5e3015603c19c7c83aed1e005d Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Sun, 21 Jul 2024 16:52:16 +0500 Subject: [PATCH 05/26] Added options to handle missinge headers and extra headers --- gspread/worksheet.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index e09a48f5..70bdd997 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -620,13 +620,21 @@ def get_all_records( return to_records(keys, values) - def set_records(self, rows: List[Dict[str, Any]]) -> None: + def set_records( + self, + rows: List[Dict[str, Any]], + ignore_extra_headers: bool = False, + default_blank: Any = "", + ) -> None: cols = self.column_headers insert_rows = [] for row in rows: + if set(row.keys()).issubset(set(cols)) and not ignore_extra_headers: + raise GSpreadException("Extra headers found in the data set") + insert_row = [] for col in cols: - insert_row.append(row[col]) + insert_row.append(row.get(col, default_blank)) insert_rows.append(insert_row) self.append_rows( @@ -634,8 +642,17 @@ def set_records(self, rows: List[Dict[str, Any]]) -> None: value_input_option=ValueInputOption.user_entered, ) - def set_record(self, row: Dict[str, Any]) -> None: - self.set_records([row]) + def set_record( + self, + row: Dict[str, Any], + ignore_extra_headers: bool = False, + default_blank: Any = "", + ) -> None: + self.set_records( + [row], + ignore_extra_headers=ignore_extra_headers, + default_blank=default_blank, + ) def get_all_cells(self) -> List[Cell]: """Returns a list of all `Cell` of the current sheet.""" From 1829c07c8dbe6826ddb69e96732d206ac56bc9b3 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Sun, 21 Jul 2024 17:20:50 +0500 Subject: [PATCH 06/26] added a test for set_records. --- tests/worksheet_test.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index f8523b5b..5f533321 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1751,6 +1751,27 @@ def test_batch_clear(self): self.assertListEqual(w.get_values("A1:B1"), [[]]) self.assertListEqual(w.get_values("C2:E2"), [[]]) + @pytest.mark.vcr() + def test_set_records(self): + + w = self.spreadsheet.sheet1 + values = [["name", "age"], ["jean", 44]] + w.update(values, "A1:B2") + self.assertEqual(w.get_all_values(), values) + update_values = [{"name": "john", "age": 11}, {"name": "abdullah"}] + w.set_records(update_values) + values = [*values, ["john", 11], ["abdullah", ""]] + self.assertEqual(w.get_all_values(), values) + with self.assertRaises(GSpreadException) as ar: + w.set_record({"name": "lin", "location": "helsinki"}) + + self.assertEqual(ar.exception, GSpreadException) + w.set_record( + {"name": "juanita", "status": "active", "age": 33}, + ignore_extra_headers=True, + ) + self.assertEqual(w.get_all_values(), [*values, ["juanita", 33]]) + @pytest.mark.vcr() def test_group_columns(self): w = self.sheet From e2c1151027b76bde206c2203e42f36186f0df176 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Sun, 21 Jul 2024 12:25:51 +0000 Subject: [PATCH 07/26] removed uncessary code. --- gspread/worksheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 70bdd997..baf51067 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -629,7 +629,7 @@ def set_records( cols = self.column_headers insert_rows = [] for row in rows: - if set(row.keys()).issubset(set(cols)) and not ignore_extra_headers: + if set(row).issubset(set(cols)) and not ignore_extra_headers: raise GSpreadException("Extra headers found in the data set") insert_row = [] From 7437dca5956064d26dcdf19047a07758df5750fc Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Sun, 21 Jul 2024 13:05:39 +0000 Subject: [PATCH 08/26] fixed the unique headers test. --- gspread/worksheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index baf51067..21a4298e 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -629,7 +629,7 @@ def set_records( cols = self.column_headers insert_rows = [] for row in rows: - if set(row).issubset(set(cols)) and not ignore_extra_headers: + if not set(row).issubset(set(cols)) and not ignore_extra_headers: raise GSpreadException("Extra headers found in the data set") insert_row = [] From 6ae9b12db714053b716e42ac32a753751ffeaa2c Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Sun, 21 Jul 2024 13:07:34 +0000 Subject: [PATCH 09/26] added `ValueRenderOption` to mitigate type errors. --- tests/worksheet_test.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index 5f533321..38553bfb 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1757,20 +1757,28 @@ def test_set_records(self): w = self.spreadsheet.sheet1 values = [["name", "age"], ["jean", 44]] w.update(values, "A1:B2") - self.assertEqual(w.get_all_values(), values) + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + values, + ) update_values = [{"name": "john", "age": 11}, {"name": "abdullah"}] w.set_records(update_values) values = [*values, ["john", 11], ["abdullah", ""]] - self.assertEqual(w.get_all_values(), values) - with self.assertRaises(GSpreadException) as ar: + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + values, + ) + with pytest.raises(GSpreadException): w.set_record({"name": "lin", "location": "helsinki"}) - self.assertEqual(ar.exception, GSpreadException) w.set_record( {"name": "juanita", "status": "active", "age": 33}, ignore_extra_headers=True, ) - self.assertEqual(w.get_all_values(), [*values, ["juanita", 33]]) + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + [*values, ["juanita", 33]], + ) @pytest.mark.vcr() def test_group_columns(self): From f1d8d5a2353fc9d8a9984e41d89cd94c893f79ad Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Sun, 21 Jul 2024 13:16:35 +0000 Subject: [PATCH 10/26] started the docstring for `set_records`. --- gspread/worksheet.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 21a4298e..159c9174 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -626,6 +626,15 @@ def set_records( ignore_extra_headers: bool = False, default_blank: Any = "", ) -> None: + """Update the sheet via records(list of dicts). The keys for the dicts must be the headers for the column. + + Missing columns are filled with `default blank` in the row. + + Keyword arguments: + argument -- description + Return: return_description + """ + cols = self.column_headers insert_rows = [] for row in rows: From eb06148192707cc8e483f634ec1a9ad40abf7241 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:07:06 +0000 Subject: [PATCH 11/26] made test values more generic. --- tests/worksheet_test.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index 38553bfb..1dde27cf 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1755,29 +1755,46 @@ def test_batch_clear(self): def test_set_records(self): w = self.spreadsheet.sheet1 - values = [["name", "age"], ["jean", 44]] + values = [ + ["header1", "header2"], + ["value1", "value2"], + ] w.update(values, "A1:B2") self.assertEqual( w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), values, ) - update_values = [{"name": "john", "age": 11}, {"name": "abdullah"}] + update_values = [ + {"header1": "new value1", "header2": "new value2"}, + {"header1": "new value3"}, + ] w.set_records(update_values) - values = [*values, ["john", 11], ["abdullah", ""]] + new_values = [ + *values, + ["new value1", "new value2"], + ["new value3", ""], + ] self.assertEqual( w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), - values, + new_values, ) with pytest.raises(GSpreadException): - w.set_record({"name": "lin", "location": "helsinki"}) + w.set_record({"header1": "error value1", "location": "error value2"}) w.set_record( - {"name": "juanita", "status": "active", "age": 33}, + { + "header1": "single entry1", + "status": "active", + "header2": "single entry2", + }, ignore_extra_headers=True, ) self.assertEqual( w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), - [*values, ["juanita", 33]], + [ + *new_values, + ["single entry1", "single entry2"], + ], ) @pytest.mark.vcr() From aac2a0f41475e4db2a5de9060acef6e6a7238a89 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:36:20 +0000 Subject: [PATCH 12/26] Renamed the functions to `append_` to be more accurate. --- gspread/worksheet.py | 6 +++--- tests/worksheet_test.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 159c9174..ea6ff02c 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -620,7 +620,7 @@ def get_all_records( return to_records(keys, values) - def set_records( + def append_records( self, rows: List[Dict[str, Any]], ignore_extra_headers: bool = False, @@ -651,13 +651,13 @@ def set_records( value_input_option=ValueInputOption.user_entered, ) - def set_record( + def append_record( self, row: Dict[str, Any], ignore_extra_headers: bool = False, default_blank: Any = "", ) -> None: - self.set_records( + self.append_records( [row], ignore_extra_headers=ignore_extra_headers, default_blank=default_blank, diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index 1dde27cf..fad77dcf 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1752,7 +1752,7 @@ def test_batch_clear(self): self.assertListEqual(w.get_values("C2:E2"), [[]]) @pytest.mark.vcr() - def test_set_records(self): + def test_append_records(self): w = self.spreadsheet.sheet1 values = [ @@ -1768,7 +1768,7 @@ def test_set_records(self): {"header1": "new value1", "header2": "new value2"}, {"header1": "new value3"}, ] - w.set_records(update_values) + w.append_records(update_values) new_values = [ *values, ["new value1", "new value2"], @@ -1779,9 +1779,9 @@ def test_set_records(self): new_values, ) with pytest.raises(GSpreadException): - w.set_record({"header1": "error value1", "location": "error value2"}) + w.append_record({"header1": "error value1", "location": "error value2"}) - w.set_record( + w.append_record( { "header1": "single entry1", "status": "active", From 97b11c8d6ed431179725c7852587504d2b7ff29c Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:49:31 +0000 Subject: [PATCH 13/26] Added `insert_records`/`insert_record` and related tests. --- gspread/worksheet.py | 40 ++++++++++++++++++++++++++++++++++++++- tests/worksheet_test.py | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index ea6ff02c..c8848618 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -626,7 +626,7 @@ def append_records( ignore_extra_headers: bool = False, default_blank: Any = "", ) -> None: - """Update the sheet via records(list of dicts). The keys for the dicts must be the headers for the column. + """Adds rows to the end of data range via records(list of dicts). The keys for the dicts must be the headers for the column. Missing columns are filled with `default blank` in the row. @@ -663,6 +663,44 @@ def append_record( default_blank=default_blank, ) + def insert_records( + self, + rows: List[Dict[str, Any]], + ignore_extra_headers: bool = False, + default_blank: Any = "", + insert_row: int = 2, + ) -> None: + cols = self.column_headers + insert_rows = [] + for row in rows: + if not set(row).issubset(set(cols)) and not ignore_extra_headers: + raise GSpreadException("Extra headers found in the data set") + + ins_row = [] + for col in cols: + ins_row.append(row.get(col, default_blank)) + insert_rows.append(ins_row) + + self.insert_rows( + insert_rows, + row=insert_row, + value_input_option=ValueInputOption.user_entered, + ) + + def insert_record( + self, + row: Dict[str, Any], + ignore_extra_headers: bool = False, + default_blank: Any = "", + insert_row: int = 2, + ) -> None: + self.insert_records( + [row], + ignore_extra_headers=ignore_extra_headers, + insert_row=insert_row, + default_blank=default_blank, + ) + def get_all_cells(self) -> List[Cell]: """Returns a list of all `Cell` of the current sheet.""" diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index fad77dcf..14875f23 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1797,6 +1797,48 @@ def test_append_records(self): ], ) + @pytest.mark.vcr() + def test_insert_rows(self): + w = self.spreadsheet.sheet1 + values = [ + ["header1", "header2"], + ["value1", "value2"], + ] + w.update(values, "A1:B2") + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + values, + ) + + w.insert_records( + [ + {"header1": "value3", "header2": "value4"}, + {"header2": "value5"}, + ] + ) + + new_values = [ + values[0], + ["value3", "value4"], + ["", "value5"], + ] + + self.assertEqual(w.get_all_values(), new_values) + + with pytest.raises(GSpreadException): + w.insert_record({"header1": "error value1", "location": "error value2"}) + + w.insert_record( + {"header4": "ignore value", "header1": "value6", "header2": "value7"}, + insert_row=4, + ignore_extra_headers=True, + ) + + self.assertEqual( + w.get_all_values(), + [*new_values, ["value6", "value7"]], + ) + @pytest.mark.vcr() def test_group_columns(self): w = self.sheet From 907980bade1e5483742f073616d8946b635f07bc Mon Sep 17 00:00:00 2001 From: Mudassir Chapra <37051110+muddi900@users.noreply.github.com> Date: Thu, 25 Jul 2024 08:55:53 +0000 Subject: [PATCH 14/26] fix tests to better incorporate `insert_row` behavior. --- tests/worksheet_test.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index 14875f23..9307fcbc 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -14,9 +14,12 @@ from gspread.exceptions import APIError, GSpreadException from gspread.spreadsheet import Spreadsheet from gspread.worksheet import Worksheet +from dotenv import load_dotenv from .conftest import I18N_STR, GspreadTest +load_dotenv() + class WorksheetTest(GspreadTest): """Test for gspread.Worksheet.""" @@ -1805,6 +1808,7 @@ def test_insert_rows(self): ["value1", "value2"], ] w.update(values, "A1:B2") + new_values = [values[0], ["value3", "value4"], ["", "value5"], *values[1:]] self.assertEqual( w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), values, @@ -1817,20 +1821,17 @@ def test_insert_rows(self): ] ) - new_values = [ - values[0], - ["value3", "value4"], - ["", "value5"], - ] - - self.assertEqual(w.get_all_values(), new_values) + self.assertEqual( + w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), + new_values, + ) with pytest.raises(GSpreadException): w.insert_record({"header1": "error value1", "location": "error value2"}) w.insert_record( {"header4": "ignore value", "header1": "value6", "header2": "value7"}, - insert_row=4, + insert_row=5, ignore_extra_headers=True, ) From 25351ed462ecfb15675e575d6f972b4d926ddb4e Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Sun, 28 Jul 2024 15:14:15 +0500 Subject: [PATCH 15/26] Completed docstring for `append_records` --- gspread/worksheet.py | 19 ++++++++++++------- tests/worksheet_test.py | 1 - 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index c8848618..a489c5e4 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -626,13 +626,18 @@ def append_records( ignore_extra_headers: bool = False, default_blank: Any = "", ) -> None: - """Adds rows to the end of data range via records(list of dicts). The keys for the dicts must be the headers for the column. - - Missing columns are filled with `default blank` in the row. - - Keyword arguments: - argument -- description - Return: return_description + """Appends rows to you data range. + + :param rows: A list of dictionaries, where each dictionary represents a row to be appended. + The keys of the dictionaries should correspond to the column headers of the spreadsheet. + :type rows: List[Dict[str, Any]] + :param ignore_extra_headers: If True, extra headers found in the data set will be ignored. Otherwise, + a `GSpreadException` will be raised. Defaults to False. + :type ignore_extra_headers: bool + :param default_blank: The value to use for missing columns in the data. Defaults to an empty string. + :type default_blank: Any + + :raises GSpreadException: If extra headers are found in the data set and `ignore_extra_headers` is False. """ cols = self.column_headers diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index 9307fcbc..262cd6ae 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -14,7 +14,6 @@ from gspread.exceptions import APIError, GSpreadException from gspread.spreadsheet import Spreadsheet from gspread.worksheet import Worksheet -from dotenv import load_dotenv from .conftest import I18N_STR, GspreadTest From 5b3f90dfa4e32afac9aea21bda9acc1038912231 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Sun, 28 Jul 2024 15:32:57 +0500 Subject: [PATCH 16/26] Added all the doc strings. --- gspread/worksheet.py | 60 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index a489c5e4..544b7b54 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -626,7 +626,7 @@ def append_records( ignore_extra_headers: bool = False, default_blank: Any = "", ) -> None: - """Appends rows to you data range. + """Appends records as rows to your data range. :param rows: A list of dictionaries, where each dictionary represents a row to be appended. The keys of the dictionaries should correspond to the column headers of the spreadsheet. @@ -662,6 +662,20 @@ def append_record( ignore_extra_headers: bool = False, default_blank: Any = "", ) -> None: + """Appends a dict as a row to your data range. + + :param row: A dict. The keys of the dict should + correspond to the column headers of the spreadsheet. + :type row: Dict[str, Any] + :param ignore_extra_headers: If True, extra headers found in the data set will be ignored. Otherwise, + a `GSpreadException` will be raised. Defaults to False. + :type ignore_extra_headers: bool + :param default_blank: The value to use for missing columns in the data. Defaults to an empty string. + :type default_blank: Any + + :raises GSpreadException: If extra headers are found in the data set and `ignore_extra_headers` is False. + """ + self.append_records( [row], ignore_extra_headers=ignore_extra_headers, @@ -675,6 +689,28 @@ def insert_records( default_blank: Any = "", insert_row: int = 2, ) -> None: + """Insert records as rows to your data range at the stated row. + + :param rows: A list of dictionaries, where + each dictionary represents a row to be inserted. + The keys of the dictionaries should correspond + to the column headers of the spreadsheet. + :type rows: List[Dict[str, Any]] + :param ignore_extra_headers: If True, extra headers found + in the data set will be ignored. Otherwise, + a `GSpreadException` will be raised. Defaults to False. + :type ignore_extra_headers: bool + :param default_blank: The value to use for missing + columns in the data. Defaults to an empty string. + :type default_blank: Any + :param insert_row: Row number(1-indexed) where the + data would be inserted. Defaults to 2. + :type insert_row: int + + :raises GSpreadException: If extra headers are found + in the data set and `ignore_extra_headers` is False. + """ + cols = self.column_headers insert_rows = [] for row in rows: @@ -699,6 +735,28 @@ def insert_record( default_blank: Any = "", insert_row: int = 2, ) -> None: + """Insert a dict as rows to your data range at the stated row. + + :param row: A dict, where + each dictionary represents a row to be inserted. + The keys of the dictionaries should correspond + to the column headers of the spreadsheet. + :type rows: List[Dict[str, Any]] + :param ignore_extra_headers: If True, extra headers found + in the data set will be ignored. Otherwise, + a `GSpreadException` will be raised. Defaults to False. + :type ignore_extra_headers: bool + :param default_blank: The value to use for missing + columns in the data. Defaults to an empty string. + :type default_blank: Any + :param insert_row: Row number(1-indexed) where the + data would be inserted. Defaults to 2. + :type insert_row: int + + :raises GSpreadException: If extra headers are found + in the data set and `ignore_extra_headers` is False. + """ + self.insert_records( [row], ignore_extra_headers=ignore_extra_headers, From 24117901e6cdf86fc5abee0f503aec3dd5e57d3f Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Sat, 26 Oct 2024 11:46:36 +0500 Subject: [PATCH 17/26] removed extraneous statement --- tests/worksheet_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index 262cd6ae..039c4b0b 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -17,8 +17,6 @@ from .conftest import I18N_STR, GspreadTest -load_dotenv() - class WorksheetTest(GspreadTest): """Test for gspread.Worksheet.""" From d5f85d50fa15ca5ecdeeff761f630ec28bf2747a Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Sat, 26 Oct 2024 14:41:23 +0500 Subject: [PATCH 18/26] removed getters and cache. --- gspread/worksheet.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 544b7b54..04f04496 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -199,17 +199,9 @@ def __init__( # kept for backward compatibility - publicly available # do not use if possible. self._spreadsheet = spreadsheet - self._column_headers = [] - @property - def column_headers(self) -> List[str]: - if not self._column_headers: - self._column_headers = self.row_values(1) - return self._column_headers - - @column_headers.setter - def column_headers(self, value: List[str]) -> None: - self._column_headers = value + def get_column_headers(self, header_row: Optional[int] = None) -> List[str]: + return self.row_values(header_row or 1) def __repr__(self) -> str: return "<{} {} id:{}>".format( From d36a7d26a85a3e8938d59eb6b68672928f7dfff0 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Sat, 26 Oct 2024 14:47:02 +0500 Subject: [PATCH 19/26] better arg passthrough --- gspread/worksheet.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 04f04496..4e3f0fc4 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -571,7 +571,6 @@ def get_all_records( return [] keys = entire_sheet[head - 1] - self.column_headers = keys values = entire_sheet[head:] if expected_headers is None: @@ -617,6 +616,8 @@ def append_records( rows: List[Dict[str, Any]], ignore_extra_headers: bool = False, default_blank: Any = "", + header_row: Optional[int] = None, + value_input_option: Optional[ValueInputOption] = None, ) -> None: """Appends records as rows to your data range. @@ -629,10 +630,11 @@ def append_records( :param default_blank: The value to use for missing columns in the data. Defaults to an empty string. :type default_blank: Any + :raises GSpreadException: If extra headers are found in the data set and `ignore_extra_headers` is False. """ - cols = self.column_headers + cols = self.get_column_headers(header_row) insert_rows = [] for row in rows: if not set(row).issubset(set(cols)) and not ignore_extra_headers: @@ -643,16 +645,15 @@ def append_records( insert_row.append(row.get(col, default_blank)) insert_rows.append(insert_row) - self.append_rows( - insert_rows, - value_input_option=ValueInputOption.user_entered, - ) + self.append_rows(insert_rows, value_input_option=value_input_option) def append_record( self, row: Dict[str, Any], ignore_extra_headers: bool = False, default_blank: Any = "", + header_row: Optional[int] = None, + value_input_option: Optional[ValueInputOption] = None, ) -> None: """Appends a dict as a row to your data range. @@ -672,6 +673,8 @@ def append_record( [row], ignore_extra_headers=ignore_extra_headers, default_blank=default_blank, + header_row=header_row, + value_input_option=value_input_option, ) def insert_records( @@ -680,6 +683,8 @@ def insert_records( ignore_extra_headers: bool = False, default_blank: Any = "", insert_row: int = 2, + header_row: Optional[int] = None, + value_input_option: Optional[ValueInputOption] = None, ) -> None: """Insert records as rows to your data range at the stated row. @@ -717,7 +722,8 @@ def insert_records( self.insert_rows( insert_rows, row=insert_row, - value_input_option=ValueInputOption.user_entered, + value_input_option=value_input_option, + header_row=header_row, ) def insert_record( @@ -726,6 +732,8 @@ def insert_record( ignore_extra_headers: bool = False, default_blank: Any = "", insert_row: int = 2, + header_row: Optional[int] = None, + value_input_option: Optional[ValueInputOption] = None, ) -> None: """Insert a dict as rows to your data range at the stated row. @@ -754,6 +762,8 @@ def insert_record( ignore_extra_headers=ignore_extra_headers, insert_row=insert_row, default_blank=default_blank, + header_row=header_row, + value_input_option=value_input_option, ) def get_all_cells(self) -> List[Cell]: From dd76f2ba65d3d03fa9bddbfc7e50fc0ece3d4de8 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Sat, 26 Oct 2024 15:29:21 +0500 Subject: [PATCH 20/26] Updated the docs --- gspread/worksheet.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 4e3f0fc4..64266dba 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -629,7 +629,12 @@ def append_records( :type ignore_extra_headers: bool :param default_blank: The value to use for missing columns in the data. Defaults to an empty string. :type default_blank: Any - + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + :param value_input_option: (optional) Determines how the input data + should be interpreted. See `ValueInputOption`_ in the Sheets API + reference. + :type value_input_option: :class:`~gspread.utils.ValueInputOption` :raises GSpreadException: If extra headers are found in the data set and `ignore_extra_headers` is False. """ @@ -665,6 +670,12 @@ def append_record( :type ignore_extra_headers: bool :param default_blank: The value to use for missing columns in the data. Defaults to an empty string. :type default_blank: Any + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + :param value_input_option: (optional) Determines how the input data + should be interpreted. See `ValueInputOption`_ in the Sheets API + reference. + :type value_input_option: :class:`~gspread.utils.ValueInputOption` :raises GSpreadException: If extra headers are found in the data set and `ignore_extra_headers` is False. """ @@ -703,6 +714,12 @@ def insert_records( :param insert_row: Row number(1-indexed) where the data would be inserted. Defaults to 2. :type insert_row: int + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + :param value_input_option: (optional) Determines how the input data + should be interpreted. See `ValueInputOption`_ in the Sheets API + reference. + :type value_input_option: :class:`~gspread.utils.ValueInputOption` :raises GSpreadException: If extra headers are found in the data set and `ignore_extra_headers` is False. @@ -752,6 +769,12 @@ def insert_record( :param insert_row: Row number(1-indexed) where the data would be inserted. Defaults to 2. :type insert_row: int + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + :param value_input_option: (optional) Determines how the input data + should be interpreted. See `ValueInputOption`_ in the Sheets API + reference. + :type value_input_option: :class:`~gspread.utils.ValueInputOption` :raises GSpreadException: If extra headers are found in the data set and `ignore_extra_headers` is False. From 1bb6f4e3316151e15130889556a94361a17ad8f4 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Tue, 26 Nov 2024 19:12:35 +0500 Subject: [PATCH 21/26] Added doc string for `get_column_headers` --- gspread/worksheet.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 64266dba..6142b4bf 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -201,6 +201,13 @@ def __init__( self._spreadsheet = spreadsheet def get_column_headers(self, header_row: Optional[int] = None) -> List[str]: + """Get the column headers a list of strings. + + :param header_row: (optional) Row number(1-indexed) of column titles. Defualts to 1. + :type header_row: int + + """ + return self.row_values(header_row or 1) def __repr__(self) -> str: From 68a79fe3505dd6c13eda356de1e3365721d85e49 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Tue, 26 Nov 2024 20:45:18 +0500 Subject: [PATCH 22/26] Added a default `value_input_option` --- gspread/worksheet.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 6142b4bf..c6ca8409 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -657,7 +657,10 @@ def append_records( insert_row.append(row.get(col, default_blank)) insert_rows.append(insert_row) - self.append_rows(insert_rows, value_input_option=value_input_option) + self.append_rows( + insert_rows, + value_input_option=value_input_option or ValueInputOption.raw, + ) def append_record( self, From 439e89afb7790f741e866dc9d799ea87faa130c1 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Tue, 26 Nov 2024 20:45:44 +0500 Subject: [PATCH 23/26] More explicit test values --- tests/worksheet_test.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index 039c4b0b..5e069613 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1755,28 +1755,29 @@ def test_batch_clear(self): def test_append_records(self): w = self.spreadsheet.sheet1 - values = [ + values_before = [ ["header1", "header2"], ["value1", "value2"], ] - w.update(values, "A1:B2") + w.update(values_before, "A1:B2") self.assertEqual( w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), - values, + values_before, ) update_values = [ {"header1": "new value1", "header2": "new value2"}, {"header1": "new value3"}, ] w.append_records(update_values) - new_values = [ - *values, + values_after = [ + ["header1", "header2"], + ["value1", "value2"], ["new value1", "new value2"], ["new value3", ""], ] self.assertEqual( w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), - new_values, + values_after, ) with pytest.raises(GSpreadException): w.append_record({"header1": "error value1", "location": "error value2"}) @@ -1789,12 +1790,16 @@ def test_append_records(self): }, ignore_extra_headers=True, ) + values_after_single_entry = [ + ["header1", "header2"], + ["value1", "value2"], + ["new value1", "new value2"], + ["new value3", ""], + ["single entry1", "single entry2"], + ] self.assertEqual( w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), - [ - *new_values, - ["single entry1", "single entry2"], - ], + values_after_single_entry, ) @pytest.mark.vcr() From 5764eb04ef3b8f0ee586fa01fb9ed4d2f98fcace Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Tue, 26 Nov 2024 21:08:49 +0500 Subject: [PATCH 24/26] Default `ValueInputOption` for `insert_records` --- gspread/worksheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index c6ca8409..e2d2e68d 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -749,7 +749,7 @@ def insert_records( self.insert_rows( insert_rows, row=insert_row, - value_input_option=value_input_option, + value_input_option=value_input_option or ValueInputOption.raw, header_row=header_row, ) From b4f68a8a8bf74fa7758e9c0a68f10301f73d3163 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Tue, 26 Nov 2024 21:09:06 +0500 Subject: [PATCH 25/26] Fixed the column header method --- gspread/worksheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index e2d2e68d..c6fff0e1 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -735,7 +735,7 @@ def insert_records( in the data set and `ignore_extra_headers` is False. """ - cols = self.column_headers + cols = self.get_column_headers() insert_rows = [] for row in rows: if not set(row).issubset(set(cols)) and not ignore_extra_headers: From 48679a3d94df57ee95cc94842ac2ca6660a7ce55 Mon Sep 17 00:00:00 2001 From: Mudassir Chapra Date: Tue, 26 Nov 2024 21:13:38 +0500 Subject: [PATCH 26/26] Explicit test values for `insert_records` --- tests/worksheet_test.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index 5e069613..6e0ddc0c 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1803,17 +1803,22 @@ def test_append_records(self): ) @pytest.mark.vcr() - def test_insert_rows(self): + def test_insert_records(self): w = self.spreadsheet.sheet1 - values = [ + values_before = [ + ["header1", "header2"], + ["value1", "value2"], + ] + w.update(values_before, "A1:B2") + values_after = [ ["header1", "header2"], + ["value3", "value4"], + ["", "value5"], ["value1", "value2"], ] - w.update(values, "A1:B2") - new_values = [values[0], ["value3", "value4"], ["", "value5"], *values[1:]] self.assertEqual( w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), - values, + values_before, ) w.insert_records( @@ -1825,7 +1830,7 @@ def test_insert_rows(self): self.assertEqual( w.get_all_values(value_render_option=utils.ValueRenderOption.unformatted), - new_values, + values_after, ) with pytest.raises(GSpreadException): @@ -1833,13 +1838,21 @@ def test_insert_rows(self): w.insert_record( {"header4": "ignore value", "header1": "value6", "header2": "value7"}, - insert_row=5, + insert_row=4, ignore_extra_headers=True, ) + values_after_single_entry = [ + ["header1", "header2"], + ["value3", "value4"], + ["", "value5"], + ["value6", "value7"], + ["value1", "value2"], + ] + self.assertEqual( w.get_all_values(), - [*new_values, ["value6", "value7"]], + values_after_single_entry, ) @pytest.mark.vcr()