From 1c55e1a6419144000dc88368a8a1ce1ab00b66e5 Mon Sep 17 00:00:00 2001 From: rootkiller6788 <221446036+rootkiller6788@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:59:11 +0800 Subject: [PATCH 1/2] let build() skip docstring generation Expanding every method's request/response schema into a docstring is the expensive part of building a service. For sheets.v4, calling service.spreadsheets() peaks at ~57 MB just for that, and it's paid again for every service built. Add a generate_docstrings flag to build() and build_from_document() so callers that never read the docstrings can opt out. It defaults to True, so nothing changes for existing code. --- googleapiclient/discovery.py | 43 +++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py index 4e92b2cc1b..7f0bc2ed3c 100644 --- a/googleapiclient/discovery.py +++ b/googleapiclient/discovery.py @@ -208,6 +208,7 @@ def build( num_retries=1, static_discovery=None, always_use_jwt_access=False, + generate_docstrings=True, ): """Construct a Resource for interacting with an API. @@ -269,6 +270,11 @@ def build( always_use_jwt_access: Boolean, whether always use self signed JWT for service account credentials. This only applies to google.oauth2.service_account.Credentials. + generate_docstrings: Boolean, whether to generate docstrings for the + methods on the returned Resource. Generating them expands every method's + request and response schema, which is slow and memory hungry for large + APIs. Set to False if you never read the docstrings, e.g. in a server + that builds a service per request. Returns: A Resource object with methods for interacting with the service. @@ -325,6 +331,7 @@ def build( adc_cert_path=adc_cert_path, adc_key_path=adc_key_path, always_use_jwt_access=always_use_jwt_access, + generate_docstrings=generate_docstrings, ) break # exit if a service was created except HttpError as e: @@ -475,6 +482,7 @@ def build_from_document( adc_cert_path=None, adc_key_path=None, always_use_jwt_access=False, + generate_docstrings=True, ): """Create a Resource for interacting with an API. @@ -527,6 +535,11 @@ def build_from_document( always_use_jwt_access: Boolean, whether always use self signed JWT for service account credentials. This only applies to google.oauth2.service_account.Credentials. + generate_docstrings: Boolean, whether to generate docstrings for the + methods on the returned Resource. Generating them expands every method's + request and response schema, which is slow and memory hungry for large + APIs. Set to False if you never read the docstrings, e.g. in a server + that builds a service per request. Returns: A Resource object with methods for interacting with the service. @@ -737,6 +750,7 @@ def build_from_document( rootDesc=service, schema=schema, universe_domain=universe_domain, + generate_docstrings=generate_docstrings, ) @@ -1074,7 +1088,7 @@ def set_parameters(self, method_desc): self.query_params.remove(name) -def createMethod(methodName, methodDesc, rootDesc, schema): +def createMethod(methodName, methodDesc, rootDesc, schema, generate_docstrings=True): """Creates a method for attaching to a Resource. Args: @@ -1083,6 +1097,10 @@ def createMethod(methodName, methodDesc, rootDesc, schema): describes the method. rootDesc: object, the entire deserialized discovery document. schema: object, mapping of schema names to schema descriptions. + generate_docstrings: Boolean, whether to build the method's docstring from + the discovery document. Setting this to False skips expanding the + request/response schemas, which is the expensive part of building a + service and is only useful when reading help(). """ methodName = fix_method_name(methodName) ( @@ -1275,6 +1293,10 @@ def method(self, **kwargs): resumable=resumable, ) + if not generate_docstrings: + # Leave method without a docstring, nothing was generated for it. + return (methodName, method) + docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"] if len(parameters.argmap) > 0: docs.append("Args:\n") @@ -1414,6 +1436,7 @@ def __init__( rootDesc, schema, universe_domain=universe.DEFAULT_UNIVERSE if HAS_UNIVERSE else "", + generate_docstrings=True, ): """Build a Resource from the API description. @@ -1433,6 +1456,10 @@ def __init__( schema: object, mapping of schema names to schema descriptions. universe_domain: string, the universe for the API. The default universe is "googleapis.com". + generate_docstrings: Boolean, whether to generate method docstrings + from the discovery document. Set to False to skip expanding the + request/response schemas for every method, which saves a lot of + memory and time when the docstrings are never read. """ self._dynamic_attrs = [] @@ -1446,6 +1473,7 @@ def __init__( self._schema = schema self._universe_domain = universe_domain self._credentials_validated = False + self._generate_docstrings = generate_docstrings self._set_service_methods() @@ -1529,7 +1557,11 @@ def new_batch_http_request(callback=None): if "methods" in resourceDesc: for methodName, methodDesc in resourceDesc["methods"].items(): fixedMethodName, method = createMethod( - methodName, methodDesc, rootDesc, schema + methodName, + methodDesc, + rootDesc, + schema, + generate_docstrings=self._generate_docstrings, ) self._set_dynamic_attr( fixedMethodName, method.__get__(self, self.__class__) @@ -1538,7 +1570,11 @@ def new_batch_http_request(callback=None): # change when it sees that the method name ends in _media. if methodDesc.get("supportsMediaDownload", False): fixedMethodName, method = createMethod( - methodName + "_media", methodDesc, rootDesc, schema + methodName + "_media", + methodDesc, + rootDesc, + schema, + generate_docstrings=self._generate_docstrings, ) self._set_dynamic_attr( fixedMethodName, method.__get__(self, self.__class__) @@ -1569,6 +1605,7 @@ def methodResource(self): rootDesc=rootDesc, schema=schema, universe_domain=self._universe_domain, + generate_docstrings=self._generate_docstrings, ) setattr(methodResource, "__doc__", "A collection resource.") From f621ce47b82cd46a5401ab0c41a3b0bfd1ff533e Mon Sep 17 00:00:00 2001 From: rootkiller6788 <221446036+rootkiller6788@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:00:58 +0800 Subject: [PATCH 2/2] tests for the docstring opt-out Covers both build() and build_from_document(), and checks the flag reaches lazily built nested resources (plus.activities().list). Also updates test_pickle, which pins the exact set of Resource attributes. --- tests/test_discovery.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 6912783451..361cdc06f1 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -596,6 +596,34 @@ def test_can_build_from_local_deserialized_document(self): self.assertIsNotNone(plus) self.assertTrue(hasattr(plus, "activities")) + def test_docstrings_are_generated_by_default(self): + plus = build_from_document( + read_datafile("plus.json"), + base="https://www.googleapis.com/", + credentials=self.MOCK_CREDENTIALS, + ) + self.assertIn("Returns:", plus.activities().list.__doc__) + + def test_can_build_from_local_document_without_docstrings(self): + plus = build_from_document( + read_datafile("plus.json"), + base="https://www.googleapis.com/", + credentials=self.MOCK_CREDENTIALS, + generate_docstrings=False, + ) + # The flag has to reach the nested resources too, they are built lazily. + self.assertIsNone(plus.activities().list.__doc__) + self.assertIsNone(plus.people().get.__doc__) + + def test_build_without_docstrings_still_builds_requests(self): + http = HttpMock(datafile("zoo.json"), {"status": "200"}) + zoo = build( + "zoo", "v1", http=http, static_discovery=False, generate_docstrings=False + ) + self.assertIsNone(zoo.animals().get.__doc__) + # The generated _media variants go through the same path. + self.assertIsNone(zoo.animals().get_media.__doc__) + def test_building_with_base_remembers_base(self): discovery = read_datafile("plus.json") @@ -2315,6 +2343,7 @@ def test_pickle(self): "_credentials_validated", "_developerKey", "_dynamic_attrs", + "_generate_docstrings", "_http", "_model", "_requestBuilder",