From e0e35fffad9f8620375a50effac9aaec2cc818e1 Mon Sep 17 00:00:00 2001 From: Isaiah <99689836+Gardner-Programs@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:56:53 -0400 Subject: [PATCH 1/2] feat: add custom JSON serializer support to HTTPClient Allow configuring a custom JSON serializer via Client.set_serializer / HTTPClient.set_serializer, applied to all request bodies. Useful for encoding types the stdlib json module can't handle by default (e.g. datetime, Decimal). Defaults to None, preserving existing behavior. Closes #1556 --- docs/user-guide.rst | 17 ++++++++++++++ gspread/client.py | 14 +++++++++++- gspread/http_client.py | 34 ++++++++++++++++++++++++++- tests/http_client_test.py | 48 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 tests/http_client_test.py diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 34aeeb5d..a7c08321 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -361,6 +361,23 @@ Update a range worksheet.update([[1, 2], [3, 4]], 'A1:B2') +Using a Custom JSON Serializer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default gspread serializes request bodies with the standard library +``json`` module, which cannot encode some types such as ``datetime`` or +``Decimal``. You can register a custom serializer to handle them. + +.. code:: python + + import json + + gc.set_serializer(lambda body: json.dumps(body, default=str)) + +The serializer is any callable with the same signature as ``json.dumps``. +Pass ``None`` to restore the default behavior. + + Adding Data Validation ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/gspread/client.py b/gspread/client.py index 957088bb..03852366 100644 --- a/gspread/client.py +++ b/gspread/client.py @@ -14,7 +14,7 @@ from requests import Response, Session from .exceptions import APIError, SpreadsheetNotFound -from .http_client import HTTPClient, HTTPClientType, ParamsType +from .http_client import HTTPClient, HTTPClientType, ParamsType, SerializerType from .spreadsheet import Spreadsheet from .urls import DRIVE_FILES_API_V3_COMMENTS_URL, DRIVE_FILES_API_V3_URL from .utils import ExportFormat, MimeType, extract_id_from_url, finditem @@ -65,6 +65,18 @@ def set_timeout( """ self.http_client.set_timeout(timeout) + def set_serializer(self, serializer: SerializerType = None) -> None: + """Set a custom JSON serializer used to encode request bodies. + + See :meth:`gspread.http_client.HTTPClient.set_serializer` for details. + + Use value ``None`` to restore the default serialization behavior. + + :param serializer: A callable with the same signature as ``json.dumps``, + or ``None`` to use the default. + """ + self.http_client.set_serializer(serializer) + def get_file_drive_metadata(self, id: str) -> Any: """Get the metadata from the Drive API for a specific file This method is mainly here to retrieve the create/update time diff --git a/gspread/http_client.py b/gspread/http_client.py index 9aaf448a..a4cca912 100644 --- a/gspread/http_client.py +++ b/gspread/http_client.py @@ -12,6 +12,7 @@ from typing import ( IO, Any, + Callable, Dict, List, Mapping, @@ -44,6 +45,7 @@ from .utils import ExportFormat, convert_credentials, quote ParamsType = MutableMapping[str, Optional[Union[str, int, bool, float, List[str]]]] +SerializerType = Optional[Callable[[Mapping[str, Any]], Union[str, bytes]]] FileType = Optional[ Union[ @@ -83,6 +85,7 @@ def __init__(self, auth: Credentials, session: Optional[Session] = None) -> None self.session = AuthorizedSession(self.auth) self.timeout: Optional[Union[float, Tuple[float, float]]] = None + self.serializer: SerializerType = None def login(self) -> None: from google.auth.transport.requests import Request @@ -102,16 +105,45 @@ def set_timeout(self, timeout: Optional[Union[float, Tuple[float, float]]]) -> N """ self.timeout = timeout + def set_serializer(self, serializer: SerializerType) -> None: + """Set a custom JSON serializer used to encode request bodies. + + The serializer is a callable that takes the request body (a mapping) + and returns its JSON-encoded form as ``str`` or ``bytes``. It is + applied to every request that sends a JSON body, which is useful for + handling types the standard library ``json`` module cannot serialize + by default (e.g. ``datetime`` or ``Decimal``). + + Use value ``None`` to restore the default serialization behavior. + + :param serializer: A callable with the same signature as + ``json.dumps`` (e.g. ``functools.partial(json.dumps, default=...)``), + or ``None`` to use the default. + + Example:: + + import json + + client.set_serializer( + lambda body: json.dumps(body, default=str) + ) + """ + self.serializer = serializer + def request( self, method: str, endpoint: str, params: Optional[ParamsType] = None, - data: Optional[bytes] = None, + data: Optional[Union[str, bytes]] = None, json: Optional[Mapping[str, Any]] = None, files: FileType = None, headers: Optional[MutableMapping[str, str]] = None, ) -> Response: + if self.serializer is not None and json is not None: + data = self.serializer(dict(json)) + json = None + headers = {**(headers or {}), "Content-Type": "application/json"} response = self.session.request( method=method, url=endpoint, diff --git a/tests/http_client_test.py b/tests/http_client_test.py new file mode 100644 index 00000000..a7caa204 --- /dev/null +++ b/tests/http_client_test.py @@ -0,0 +1,48 @@ +import datetime +import json +from unittest import TestCase +from unittest.mock import Mock + +from gspread.http_client import HTTPClient + + +class HTTPClientSerializerTest(TestCase): + def _make_client(self): + session = Mock() + session.request.return_value.ok = True # so request() returns, doesn't raise + return HTTPClient(auth=None, session=session), session + + def test_default_uses_json_kwarg(self): + """Without a serializer, the body goes out via json= (unchanged behavior).""" + client, session = self._make_client() + body = {"values": [[1, 2, 3]]} + + client.request("post", "http://example.com", json=body) + + _, kwargs = session.request.call_args + self.assertEqual(kwargs["json"], body) + self.assertIsNone(kwargs["data"]) + + def test_custom_serializer_uses_data_and_header(self): + """With a serializer, the body is serialized into data= with a JSON header.""" + client, session = self._make_client() + client.set_serializer(lambda b: json.dumps(b, default=str)) + body = {"values": [[1, 2, 3]]} + + client.request("post", "http://example.com", json=body) + + _, kwargs = session.request.call_args + self.assertIsNone(kwargs["json"]) + self.assertEqual(kwargs["data"], json.dumps(body, default=str)) + self.assertEqual(kwargs["headers"]["Content-Type"], "application/json") + + def test_custom_serializer_handles_non_native_types(self): + """The actual use case from the issue: serialize a date the stdlib can't.""" + client, session = self._make_client() + client.set_serializer(lambda b: json.dumps(b, default=lambda o: o.isoformat())) + body = {"values": [[datetime.date(2026, 6, 19)]]} + + client.request("post", "http://example.com", json=body) + + _, kwargs = session.request.call_args + self.assertIn("2026-06-19", kwargs["data"]) From dbda68d1b69f0e7f3ce0045f680f9321b9b2dd51 Mon Sep 17 00:00:00 2001 From: Isaiah <99689836+Gardner-Programs@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:28:29 -0400 Subject: [PATCH 2/2] feat: add per-call default_serializer to worksheet write methods Replace the client-wide set_serializer() with a per-call default_serializer argument on update, batch_update, append_row and append_rows. It mirrors the default= hook of json.dumps, so gspread keeps ownership of encoding while users handle types the stdlib cannot serialize on its own (e.g. datetime, Decimal). Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 ++ docs/user-guide.rst | 42 +++++++++++++++++----- gspread/client.py | 14 +------- gspread/http_client.py | 57 ++++++++++++------------------ gspread/worksheet.py | 42 ++++++++++++++++++++-- tests/http_client_test.py | 73 ++++++++++++++++++++++++++++++++++----- 6 files changed, 163 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index 4e19af3c..c4d7911e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ env/ gspread.egg-info/ dist/ docs/build/ + +# local virtualenv +venv/ diff --git a/docs/user-guide.rst b/docs/user-guide.rst index a7c08321..be3fcb61 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -361,21 +361,45 @@ Update a range worksheet.update([[1, 2], [3, 4]], 'A1:B2') -Using a Custom JSON Serializer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Serializing Values the Standard ``json`` Module Cannot Encode +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -By default gspread serializes request bodies with the standard library -``json`` module, which cannot encode some types such as ``datetime`` or -``Decimal``. You can register a custom serializer to handle them. +gspread encodes request bodies with the standard library ``json`` module, +which cannot encode some types such as ``datetime`` or ``Decimal``. The +data-writing methods (``update``, ``batch_update``, ``append_row`` and +``append_rows``) accept a ``default_serializer`` argument for these cases. It +works exactly like the ``default`` argument of :func:`json.dumps`: it receives +a value the encoder cannot handle and returns a JSON-serializable substitute. .. code:: python - import json + import datetime - gc.set_serializer(lambda body: json.dumps(body, default=str)) + worksheet.update( + [[datetime.date(2026, 6, 23)]], + "A1", + default_serializer=str, + ) + +``str`` covers common types like ``datetime``, ``date``, ``Decimal`` and +``UUID``. For finer control, pass your own callable: + +.. code:: python + + def encode(value): + if isinstance(value, datetime.datetime): + return value.isoformat() + raise TypeError(f"cannot serialize {type(value)}") + + worksheet.update([[some_value]], "A1", default_serializer=encode) + +.. note:: -The serializer is any callable with the same signature as ``json.dumps``. -Pass ``None`` to restore the default behavior. + The substitute you return determines how the value lands in the cell. For + example ``default_serializer=str`` sends a ``Decimal`` as the JSON string + ``"19.99"`` (stored as text unless ``value_input_option`` parses it), + whereas returning ``float(value)`` sends a number. Choose the form that + matches how you want the cell interpreted. Adding Data Validation diff --git a/gspread/client.py b/gspread/client.py index 03852366..957088bb 100644 --- a/gspread/client.py +++ b/gspread/client.py @@ -14,7 +14,7 @@ from requests import Response, Session from .exceptions import APIError, SpreadsheetNotFound -from .http_client import HTTPClient, HTTPClientType, ParamsType, SerializerType +from .http_client import HTTPClient, HTTPClientType, ParamsType from .spreadsheet import Spreadsheet from .urls import DRIVE_FILES_API_V3_COMMENTS_URL, DRIVE_FILES_API_V3_URL from .utils import ExportFormat, MimeType, extract_id_from_url, finditem @@ -65,18 +65,6 @@ def set_timeout( """ self.http_client.set_timeout(timeout) - def set_serializer(self, serializer: SerializerType = None) -> None: - """Set a custom JSON serializer used to encode request bodies. - - See :meth:`gspread.http_client.HTTPClient.set_serializer` for details. - - Use value ``None`` to restore the default serialization behavior. - - :param serializer: A callable with the same signature as ``json.dumps``, - or ``None`` to use the default. - """ - self.http_client.set_serializer(serializer) - def get_file_drive_metadata(self, id: str) -> Any: """Get the metadata from the Drive API for a specific file This method is mainly here to retrieve the create/update time diff --git a/gspread/http_client.py b/gspread/http_client.py index a4cca912..b0581f3a 100644 --- a/gspread/http_client.py +++ b/gspread/http_client.py @@ -9,6 +9,7 @@ import time from http import HTTPStatus +from json import dumps as json_dumps from typing import ( IO, Any, @@ -45,7 +46,7 @@ from .utils import ExportFormat, convert_credentials, quote ParamsType = MutableMapping[str, Optional[Union[str, int, bool, float, List[str]]]] -SerializerType = Optional[Callable[[Mapping[str, Any]], Union[str, bytes]]] +DefaultSerializerType = Optional[Callable[[Any], Any]] FileType = Optional[ Union[ @@ -85,7 +86,6 @@ def __init__(self, auth: Credentials, session: Optional[Session] = None) -> None self.session = AuthorizedSession(self.auth) self.timeout: Optional[Union[float, Tuple[float, float]]] = None - self.serializer: SerializerType = None def login(self) -> None: from google.auth.transport.requests import Request @@ -105,31 +105,6 @@ def set_timeout(self, timeout: Optional[Union[float, Tuple[float, float]]]) -> N """ self.timeout = timeout - def set_serializer(self, serializer: SerializerType) -> None: - """Set a custom JSON serializer used to encode request bodies. - - The serializer is a callable that takes the request body (a mapping) - and returns its JSON-encoded form as ``str`` or ``bytes``. It is - applied to every request that sends a JSON body, which is useful for - handling types the standard library ``json`` module cannot serialize - by default (e.g. ``datetime`` or ``Decimal``). - - Use value ``None`` to restore the default serialization behavior. - - :param serializer: A callable with the same signature as - ``json.dumps`` (e.g. ``functools.partial(json.dumps, default=...)``), - or ``None`` to use the default. - - Example:: - - import json - - client.set_serializer( - lambda body: json.dumps(body, default=str) - ) - """ - self.serializer = serializer - def request( self, method: str, @@ -139,9 +114,10 @@ def request( json: Optional[Mapping[str, Any]] = None, files: FileType = None, headers: Optional[MutableMapping[str, str]] = None, + default_serializer: DefaultSerializerType = None, ) -> Response: - if self.serializer is not None and json is not None: - data = self.serializer(dict(json)) + if default_serializer is not None and json is not None: + data = json_dumps(dict(json), default=default_serializer) json = None headers = {**(headers or {}), "Content-Type": "application/json"} response = self.session.request( @@ -179,6 +155,7 @@ def values_update( range: str, params: Optional[ParamsType] = None, body: Optional[Mapping[str, Any]] = None, + default_serializer: DefaultSerializerType = None, ) -> Any: """Lower-level method that directly calls `PUT spreadsheets//values/ `_. @@ -203,11 +180,18 @@ def values_update( .. versionadded:: 3.0 """ url = SPREADSHEET_VALUES_URL % (id, quote(range)) - r = self.request("put", url, params=params, json=body) + r = self.request( + "put", url, params=params, json=body, default_serializer=default_serializer + ) return r.json() def values_append( - self, id: str, range: str, params: ParamsType, body: Optional[Mapping[str, Any]] + self, + id: str, + range: str, + params: ParamsType, + body: Optional[Mapping[str, Any]], + default_serializer: DefaultSerializerType = None, ) -> Any: """Lower-level method that directly calls `spreadsheets//values:append `_. @@ -221,7 +205,9 @@ def values_append( .. versionadded:: 3.0 """ url = SPREADSHEET_VALUES_APPEND_URL % (id, quote(range)) - r = self.request("post", url, params=params, json=body) + r = self.request( + "post", url, params=params, json=body, default_serializer=default_serializer + ) return r.json() def values_clear(self, id: str, range: str) -> Any: @@ -289,7 +275,10 @@ def values_batch_get( return r.json() def values_batch_update( - self, id: str, body: Optional[Mapping[str, Any]] = None + self, + id: str, + body: Optional[Mapping[str, Any]] = None, + default_serializer: DefaultSerializerType = None, ) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchUpdate `_. @@ -298,7 +287,7 @@ def values_batch_update( :rtype: dict """ url = SPREADSHEET_VALUES_BATCH_UPDATE_URL % id - r = self.request("post", url, json=body) + r = self.request("post", url, json=body, default_serializer=default_serializer) return r.json() def spreadsheets_get(self, id: str, params: Optional[ParamsType] = None) -> Any: diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 5b131051..b374a2a9 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 .http_client import DefaultSerializerType, HTTPClient, ParamsType from .urls import WORKSHEET_DRIVE_URL from .utils import ( DateTimeOption, @@ -1115,6 +1115,7 @@ def update( include_values_in_response: Optional[bool] = None, response_value_render_option: Optional[ValueRenderOption] = None, response_date_time_render_option: Optional[DateTimeOption] = None, + default_serializer: DefaultSerializerType = None, ) -> JSONResponse: """Sets values in a cell range of the sheet. @@ -1195,6 +1196,13 @@ def update( The default ``date_time_render_option`` is ``DateTimeOption.serial_number``. :type date_time_render_option: :class:`~gspread.utils.DateTimeOption` + :param default_serializer: (optional) A callable applied to values that + the standard ``json`` encoder cannot serialize on its own (e.g. + ``datetime`` or ``Decimal``). It receives the offending value and + must return a JSON-serializable substitute, mirroring the ``default`` + argument of :func:`json.dumps`. For example, ``default_serializer=str``. + :type default_serializer: Callable[[Any], Any] + Examples:: # Sets 'Hello world' in 'A2' cell @@ -1248,6 +1256,7 @@ def update( full_range_name, params=params, body={"values": values, "majorDimension": major_dimension}, + default_serializer=default_serializer, ) return response @@ -1260,6 +1269,7 @@ def batch_update( include_values_in_response: Optional[bool] = None, response_value_render_option: Optional[ValueRenderOption] = None, response_date_time_render_option: Optional[DateTimeOption] = None, + default_serializer: DefaultSerializerType = None, ) -> JSONResponse: """Sets values in one or more cell ranges of the sheet at once. @@ -1333,6 +1343,13 @@ def batch_update( The default ``date_time_render_option`` is ``DateTimeOption.serial_number``. :type date_time_render_option: :class:`~gspread.utils.DateTimeOption` + :param default_serializer: (optional) A callable applied to values that + the standard ``json`` encoder cannot serialize on its own (e.g. + ``datetime`` or ``Decimal``). It receives the offending value and + must return a JSON-serializable substitute, mirroring the ``default`` + argument of :func:`json.dumps`. For example, ``default_serializer=str``. + :type default_serializer: Callable[[Any], Any] + Examples:: worksheet.batch_update([{ @@ -1365,7 +1382,9 @@ def batch_update( "data": data, } - response = self.client.values_batch_update(self.spreadsheet_id, body=body) + response = self.client.values_batch_update( + self.spreadsheet_id, body=body, default_serializer=default_serializer + ) return response @@ -1785,6 +1804,7 @@ def append_row( insert_data_option: Optional[InsertDataOption] = None, table_range: Optional[str] = None, include_values_in_response: bool = False, + default_serializer: DefaultSerializerType = None, ) -> JSONResponse: """Adds a row to the worksheet and populates it with values. @@ -1804,6 +1824,12 @@ def append_row( :param bool include_values_in_response: (optional) Determines if the update response should include the values of the cells that were appended. By default, responses do not include the updated values. + :param default_serializer: (optional) A callable applied to values that + the standard ``json`` encoder cannot serialize on its own (e.g. + ``datetime`` or ``Decimal``). It receives the offending value and + must return a JSON-serializable substitute, mirroring the ``default`` + argument of :func:`json.dumps`. For example, ``default_serializer=str``. + :type default_serializer: Callable[[Any], Any] .. _ValueInputOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption .. _InsertDataOption: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#InsertDataOption @@ -1815,6 +1841,7 @@ def append_row( insert_data_option=insert_data_option, table_range=table_range, include_values_in_response=include_values_in_response, + default_serializer=default_serializer, ) def append_rows( @@ -1824,6 +1851,7 @@ def append_rows( insert_data_option: Optional[InsertDataOption] = None, table_range: Optional[str] = None, include_values_in_response: Optional[bool] = None, + default_serializer: DefaultSerializerType = None, ) -> JSONResponse: """Adds multiple rows to the worksheet and populates them with values. @@ -1845,6 +1873,12 @@ def append_rows( :param bool include_values_in_response: (optional) Determines if the update response should include the values of the cells that were appended. By default, responses do not include the updated values. + :param default_serializer: (optional) A callable applied to values that + the standard ``json`` encoder cannot serialize on its own (e.g. + ``datetime`` or ``Decimal``). It receives the offending value and + must return a JSON-serializable substitute, mirroring the ``default`` + argument of :func:`json.dumps`. For example, ``default_serializer=str``. + :type default_serializer: Callable[[Any], Any] .. _ValueInputOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption .. _InsertDataOption: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#InsertDataOption @@ -1859,7 +1893,9 @@ def append_rows( body = {"values": values} - res = self.client.values_append(self.spreadsheet_id, range_label, params, body) + res = self.client.values_append( + self.spreadsheet_id, range_label, params, body, default_serializer + ) num_new_rows = len(values) self._properties["gridProperties"]["rowCount"] += num_new_rows return res diff --git a/tests/http_client_test.py b/tests/http_client_test.py index a7caa204..b64407b6 100644 --- a/tests/http_client_test.py +++ b/tests/http_client_test.py @@ -4,6 +4,7 @@ from unittest.mock import Mock from gspread.http_client import HTTPClient +from gspread.worksheet import Worksheet class HTTPClientSerializerTest(TestCase): @@ -13,7 +14,7 @@ def _make_client(self): return HTTPClient(auth=None, session=session), session def test_default_uses_json_kwarg(self): - """Without a serializer, the body goes out via json= (unchanged behavior).""" + """Without default_serializer, the body goes out via json= (unchanged behavior).""" client, session = self._make_client() body = {"values": [[1, 2, 3]]} @@ -23,26 +24,80 @@ def test_default_uses_json_kwarg(self): self.assertEqual(kwargs["json"], body) self.assertIsNone(kwargs["data"]) - def test_custom_serializer_uses_data_and_header(self): - """With a serializer, the body is serialized into data= with a JSON header.""" + def test_default_serializer_uses_data_and_header(self): + """With default_serializer, the body is encoded into data= with a JSON header.""" client, session = self._make_client() - client.set_serializer(lambda b: json.dumps(b, default=str)) body = {"values": [[1, 2, 3]]} - client.request("post", "http://example.com", json=body) + client.request("post", "http://example.com", json=body, default_serializer=str) _, kwargs = session.request.call_args self.assertIsNone(kwargs["json"]) self.assertEqual(kwargs["data"], json.dumps(body, default=str)) self.assertEqual(kwargs["headers"]["Content-Type"], "application/json") - def test_custom_serializer_handles_non_native_types(self): - """The actual use case from the issue: serialize a date the stdlib can't.""" + def test_default_serializer_handles_non_native_types(self): + """The actual use case from the issue: encode a date the stdlib can't.""" client, session = self._make_client() - client.set_serializer(lambda b: json.dumps(b, default=lambda o: o.isoformat())) body = {"values": [[datetime.date(2026, 6, 19)]]} - client.request("post", "http://example.com", json=body) + client.request( + "post", + "http://example.com", + json=body, + default_serializer=lambda o: o.isoformat(), + ) _, kwargs = session.request.call_args self.assertIn("2026-06-19", kwargs["data"]) + + +class WorksheetForwardsSerializerTest(TestCase): + """default_serializer must reach the client from each write method.""" + + MARKER = staticmethod(lambda o: str(o)) + + def _make_worksheet(self): + client = HTTPClient(auth=None, session=Mock()) + client.values_update = Mock() + client.values_append = Mock() + client.values_batch_update = Mock() + properties = { + "title": "Sheet1", + "gridProperties": {"rowCount": 5, "columnCount": 5}, + } + worksheet = Worksheet( + spreadsheet=None, + properties=properties, + spreadsheet_id="abc123", + client=client, + ) + return worksheet, client + + def test_update_forwards_serializer(self): + worksheet, client = self._make_worksheet() + worksheet.update([[1, 2, 3]], "A1", default_serializer=self.MARKER) + self.assertIs( + client.values_update.call_args.kwargs["default_serializer"], self.MARKER + ) + + def test_batch_update_forwards_serializer(self): + worksheet, client = self._make_worksheet() + worksheet.batch_update( + [{"range": "A1", "values": [[1]]}], default_serializer=self.MARKER + ) + self.assertIs( + client.values_batch_update.call_args.kwargs["default_serializer"], + self.MARKER, + ) + + def test_append_rows_forwards_serializer(self): + worksheet, client = self._make_worksheet() + worksheet.append_rows([[1, 2, 3]], default_serializer=self.MARKER) + # values_append receives default_serializer as the last positional arg + self.assertIs(client.values_append.call_args.args[4], self.MARKER) + + def test_append_row_forwards_serializer(self): + worksheet, client = self._make_worksheet() + worksheet.append_row([1, 2, 3], default_serializer=self.MARKER) + self.assertIs(client.values_append.call_args.args[4], self.MARKER)