Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions googleapiclient/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -737,6 +750,7 @@ def build_from_document(
rootDesc=service,
schema=schema,
universe_domain=universe_domain,
generate_docstrings=generate_docstrings,
)


Expand Down Expand Up @@ -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:
Expand All @@ -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)
(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.

Expand All @@ -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 = []

Expand All @@ -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()

Expand Down Expand Up @@ -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__)
Expand All @@ -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__)
Expand Down Expand Up @@ -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.")
Expand Down
29 changes: 29 additions & 0 deletions tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Comment on lines +618 to +625

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test test_build_without_docstrings_still_builds_requests is intended to verify that building a service without docstrings still allows requests to be built successfully. However, the test currently only asserts that the docstrings are None and does not actually attempt to build any requests. To ensure that request building is indeed functional and not broken by the lack of docstrings, we should explicitly build both a standard request and a media request and assert that they are created successfully.

Suggested change
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_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__)
req = zoo.animals().get(name="Lion")
self.assertIsNotNone(req)
media_req = zoo.animals().get_media(name="Lion")
self.assertIsNotNone(media_req)


def test_building_with_base_remembers_base(self):
discovery = read_datafile("plus.json")

Expand Down Expand Up @@ -2315,6 +2343,7 @@ def test_pickle(self):
"_credentials_validated",
"_developerKey",
"_dynamic_attrs",
"_generate_docstrings",
"_http",
"_model",
"_requestBuilder",
Expand Down
Loading