From dc9a04cd945cc01b1d7fe471418e6a5caf027aee Mon Sep 17 00:00:00 2001 From: Alexandre Lavigne Date: Thu, 1 Feb 2024 20:58:04 +0100 Subject: [PATCH] Fixup get empty cell value is `None` Fixup regression where getting a cell on an empty cell should return a valid `Cell` object with a value `None`. closes #1403 Signed-off-by: Alexandre Lavigne --- gspread/cell.py | 4 +- gspread/utils.py | 2 +- gspread/worksheet.py | 14 +- .../WorksheetTest.test_cell_return_first.json | 454 ++++++++++++++++++ tests/worksheet_test.py | 7 + 5 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 tests/cassettes/WorksheetTest.test_cell_return_first.json diff --git a/gspread/cell.py b/gspread/cell.py index 2ab1d4a68..61bb260d2 100644 --- a/gspread/cell.py +++ b/gspread/cell.py @@ -16,12 +16,12 @@ class Cell: in a :class:`~gspread.worksheet.Worksheet`. """ - def __init__(self, row: int, col: int, value: str = "") -> None: + def __init__(self, row: int, col: int, value: Optional[str] = "") -> None: self._row: int = row self._col: int = col #: Value of the cell. - self.value: str = value + self.value: Optional[str] = value @classmethod def from_address(cls, label: str, value: str = "") -> "Cell": diff --git a/gspread/utils.py b/gspread/utils.py index e97dced74..0dcb55341 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -622,7 +622,7 @@ def cell_list_to_rect(cell_list: List["Cell"]) -> List[List[Optional[str]]]: if not cell_list: return [] - rows: Dict[int, Dict[int, str]] = defaultdict(dict) + rows: Dict[int, Dict[int, Optional[str]]] = defaultdict(dict) row_offset = min(c.row for c in cell_list) col_offset = min(c.col for c in cell_list) diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 8154b9a4c..551458f1a 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -316,10 +316,14 @@ def cell( value_render_option=value_render_option, return_type=GridRangeType.ValueRange, ) - try: - value = str(data[0][0]) - except IndexError: - value = str(None) + + # we force a return type to GridRangeType.ValueRange + # help typing tool to see it too :-) + if isinstance(data, ValueRange): + value = data.first() + else: + raise RuntimeError("returned data must be of type ValueRange") + except KeyError: value = "" @@ -2209,7 +2213,7 @@ def _finder( str_query = query def match(x: Cell) -> bool: - if case_sensitive: + if case_sensitive or x.value is None: return x.value == str_query else: return x.value.casefold() == str_query.casefold() diff --git a/tests/cassettes/WorksheetTest.test_cell_return_first.json b/tests/cassettes/WorksheetTest.test_cell_return_first.json new file mode 100644 index 000000000..f7d57cd70 --- /dev/null +++ b/tests/cassettes/WorksheetTest.test_cell_return_first.json @@ -0,0 +1,454 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "POST", + "uri": "https://www.googleapis.com/drive/v3/files?supportsAllDrives=True", + "body": "{\"name\": \"Test WorksheetTest test_cell_return_first\", \"mimeType\": \"application/vnd.google-apps.spreadsheet\"}", + "headers": { + "User-Agent": [ + "python-requests/2.31.0" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "108" + ], + "Content-Type": [ + "application/json" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Cache-Control": [ + "no-cache, no-store, max-age=0, must-revalidate" + ], + "Date": [ + "Thu, 01 Feb 2024 19:55:41 GMT" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Pragma": [ + "no-cache" + ], + "Transfer-Encoding": [ + "chunked" + ], + "X-XSS-Protection": [ + "0" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "Expires": [ + "Mon, 01 Jan 1990 00:00:00 GMT" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Vary": [ + "Origin, X-Origin" + ], + "content-length": [ + "195" + ] + }, + "body": { + "string": "{\n \"kind\": \"drive#file\",\n \"id\": \"1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E\",\n \"name\": \"Test WorksheetTest test_cell_return_first\",\n \"mimeType\": \"application/vnd.google-apps.spreadsheet\"\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E?includeGridData=false", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.31.0" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Cache-Control": [ + "private" + ], + "Date": [ + "Thu, 01 Feb 2024 19:55:42 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Transfer-Encoding": [ + "chunked" + ], + "X-XSS-Protection": [ + "0" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "3339" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E\",\n \"properties\": {\n \"title\": \"Test WorksheetTest test_cell_return_first\",\n \"locale\": \"en_US\",\n \"autoRecalc\": \"ON_CHANGE\",\n \"timeZone\": \"Etc/GMT\",\n \"defaultFormat\": {\n \"backgroundColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n },\n \"padding\": {\n \"top\": 2,\n \"right\": 3,\n \"bottom\": 2,\n \"left\": 3\n },\n \"verticalAlignment\": \"BOTTOM\",\n \"wrapStrategy\": \"OVERFLOW_CELL\",\n \"textFormat\": {\n \"foregroundColor\": {},\n \"fontFamily\": \"arial,sans,sans-serif\",\n \"fontSize\": 10,\n \"bold\": false,\n \"italic\": false,\n \"strikethrough\": false,\n \"underline\": false,\n \"foregroundColorStyle\": {\n \"rgbColor\": {}\n }\n },\n \"backgroundColorStyle\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n \"spreadsheetTheme\": {\n \"primaryFontFamily\": \"Arial\",\n \"themeColors\": [\n {\n \"colorType\": \"TEXT\",\n \"color\": {\n \"rgbColor\": {}\n }\n },\n {\n \"colorType\": \"BACKGROUND\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n {\n \"colorType\": \"ACCENT1\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.25882354,\n \"green\": 0.52156866,\n \"blue\": 0.95686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT2\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.91764706,\n \"green\": 0.2627451,\n \"blue\": 0.20784314\n }\n }\n },\n {\n \"colorType\": \"ACCENT3\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.9843137,\n \"green\": 0.7372549,\n \"blue\": 0.015686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT4\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.20392157,\n \"green\": 0.65882355,\n \"blue\": 0.3254902\n }\n }\n },\n {\n \"colorType\": \"ACCENT5\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 0.42745098,\n \"blue\": 0.003921569\n }\n }\n },\n {\n \"colorType\": \"ACCENT6\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.27450982,\n \"green\": 0.7411765,\n \"blue\": 0.7764706\n }\n }\n },\n {\n \"colorType\": \"LINK\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.06666667,\n \"green\": 0.33333334,\n \"blue\": 0.8\n }\n }\n }\n ]\n }\n },\n \"sheets\": [\n {\n \"properties\": {\n \"sheetId\": 0,\n \"title\": \"Sheet1\",\n \"index\": 0,\n \"sheetType\": \"GRID\",\n \"gridProperties\": {\n \"rowCount\": 1000,\n \"columnCount\": 26\n }\n }\n }\n ],\n \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E/edit\"\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E?includeGridData=false", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.31.0" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Cache-Control": [ + "private" + ], + "Date": [ + "Thu, 01 Feb 2024 19:55:42 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Transfer-Encoding": [ + "chunked" + ], + "X-XSS-Protection": [ + "0" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "3339" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E\",\n \"properties\": {\n \"title\": \"Test WorksheetTest test_cell_return_first\",\n \"locale\": \"en_US\",\n \"autoRecalc\": \"ON_CHANGE\",\n \"timeZone\": \"Etc/GMT\",\n \"defaultFormat\": {\n \"backgroundColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n },\n \"padding\": {\n \"top\": 2,\n \"right\": 3,\n \"bottom\": 2,\n \"left\": 3\n },\n \"verticalAlignment\": \"BOTTOM\",\n \"wrapStrategy\": \"OVERFLOW_CELL\",\n \"textFormat\": {\n \"foregroundColor\": {},\n \"fontFamily\": \"arial,sans,sans-serif\",\n \"fontSize\": 10,\n \"bold\": false,\n \"italic\": false,\n \"strikethrough\": false,\n \"underline\": false,\n \"foregroundColorStyle\": {\n \"rgbColor\": {}\n }\n },\n \"backgroundColorStyle\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n \"spreadsheetTheme\": {\n \"primaryFontFamily\": \"Arial\",\n \"themeColors\": [\n {\n \"colorType\": \"TEXT\",\n \"color\": {\n \"rgbColor\": {}\n }\n },\n {\n \"colorType\": \"BACKGROUND\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 1,\n \"blue\": 1\n }\n }\n },\n {\n \"colorType\": \"ACCENT1\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.25882354,\n \"green\": 0.52156866,\n \"blue\": 0.95686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT2\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.91764706,\n \"green\": 0.2627451,\n \"blue\": 0.20784314\n }\n }\n },\n {\n \"colorType\": \"ACCENT3\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.9843137,\n \"green\": 0.7372549,\n \"blue\": 0.015686275\n }\n }\n },\n {\n \"colorType\": \"ACCENT4\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.20392157,\n \"green\": 0.65882355,\n \"blue\": 0.3254902\n }\n }\n },\n {\n \"colorType\": \"ACCENT5\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 1,\n \"green\": 0.42745098,\n \"blue\": 0.003921569\n }\n }\n },\n {\n \"colorType\": \"ACCENT6\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.27450982,\n \"green\": 0.7411765,\n \"blue\": 0.7764706\n }\n }\n },\n {\n \"colorType\": \"LINK\",\n \"color\": {\n \"rgbColor\": {\n \"red\": 0.06666667,\n \"green\": 0.33333334,\n \"blue\": 0.8\n }\n }\n }\n ]\n }\n },\n \"sheets\": [\n {\n \"properties\": {\n \"sheetId\": 0,\n \"title\": \"Sheet1\",\n \"index\": 0,\n \"sheetType\": \"GRID\",\n \"gridProperties\": {\n \"rowCount\": 1000,\n \"columnCount\": 26\n }\n }\n }\n ],\n \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E/edit\"\n}\n" + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E/values/%27Sheet1%27:clear", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.31.0" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "0" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Cache-Control": [ + "private" + ], + "Date": [ + "Thu, 01 Feb 2024 19:55:43 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Transfer-Encoding": [ + "chunked" + ], + "X-XSS-Protection": [ + "0" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "107" + ] + }, + "body": { + "string": "{\n \"spreadsheetId\": \"1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E\",\n \"clearedRange\": \"Sheet1!A1:Z1000\"\n}\n" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://sheets.googleapis.com/v4/spreadsheets/1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E/values/%27Sheet1%27%21A1?valueRenderOption=FORMATTED_VALUE", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.31.0" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Cache-Control": [ + "private" + ], + "Date": [ + "Thu, 01 Feb 2024 19:55:44 GMT" + ], + "x-l2-request-path": [ + "l2-managed-6" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Transfer-Encoding": [ + "chunked" + ], + "X-XSS-Protection": [ + "0" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "application/json; charset=UTF-8" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Vary": [ + "Origin", + "X-Origin", + "Referer" + ], + "content-length": [ + "55" + ] + }, + "body": { + "string": "{\n \"range\": \"Sheet1!A1\",\n \"majorDimension\": \"ROWS\"\n}\n" + } + } + }, + { + "request": { + "method": "DELETE", + "uri": "https://www.googleapis.com/drive/v3/files/1oR6zRLu7eQp4Po6Dtrx2VpitERPciAlIQxy9QDsKW3E?supportsAllDrives=True", + "body": null, + "headers": { + "User-Agent": [ + "python-requests/2.31.0" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "0" + ], + "authorization": [ + "" + ] + } + }, + "response": { + "status": { + "code": 204, + "message": "No Content" + }, + "headers": { + "Content-Length": [ + "0" + ], + "Cache-Control": [ + "no-cache, no-store, max-age=0, must-revalidate" + ], + "Date": [ + "Thu, 01 Feb 2024 19:55:44 GMT" + ], + "Alt-Svc": [ + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + "Server": [ + "ESF" + ], + "Pragma": [ + "no-cache" + ], + "X-XSS-Protection": [ + "0" + ], + "X-Frame-Options": [ + "SAMEORIGIN" + ], + "Content-Type": [ + "text/html" + ], + "Expires": [ + "Mon, 01 Jan 1990 00:00:00 GMT" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "Vary": [ + "Origin, X-Origin" + ] + }, + "body": { + "string": "" + } + } + } + ] +} diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py index f8a4bcf1b..667500f6e 100644 --- a/tests/worksheet_test.py +++ b/tests/worksheet_test.py @@ -1440,6 +1440,13 @@ def test_batch_get(self): self.assertEqual(value_ranges[1].range, "Sheet1!B4:D4") self.assertEqual(value_ranges[0].first(), "A1") + @pytest.mark.vcr() + def test_cell_return_first(self): + cell = self.sheet.cell(1, 1) + + self.assertIsInstance(cell, gspread.cell.Cell) + self.assertIsNone(cell.value) + @pytest.mark.vcr() def test_batch_update(self): self.sheet.batch_update(