gapic-generator 1.30.4__py3-none-any.whl → 1.30.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. gapic/cli/generate.py +17 -8
  2. gapic/schema/imp.py +12 -2
  3. gapic/schema/metadata.py +3 -0
  4. gapic/schema/wrappers.py +8 -0
  5. gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +4 -3
  6. gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +1 -1
  7. gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +1 -1
  8. gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +1 -1
  9. gapic/templates/docs/common_setup.py.j2 +35 -0
  10. gapic/templates/docs/conf.py.j2 +7 -2
  11. gapic/utils/__init__.py +2 -0
  12. gapic/utils/cache.py +92 -0
  13. {gapic_generator-1.30.4.dist-info → gapic_generator-1.30.6.dist-info}/METADATA +1 -1
  14. {gapic_generator-1.30.4.dist-info → gapic_generator-1.30.6.dist-info}/RECORD +109 -108
  15. {gapic_generator-1.30.4.dist-info → gapic_generator-1.30.6.dist-info}/WHEEL +1 -1
  16. tests/integration/goldens/asset/docs/conf.py +35 -3
  17. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/async_client.py +6 -6
  18. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py +6 -6
  19. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py +1 -1
  20. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py +1 -1
  21. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py +1 -1
  22. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py +5 -5
  23. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest_base.py +1 -1
  24. tests/integration/goldens/asset/google/cloud/asset_v1/types/asset_service.py +7 -7
  25. tests/integration/goldens/asset/google/cloud/asset_v1/types/assets.py +9 -9
  26. tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +6 -6
  27. tests/integration/goldens/credentials/docs/conf.py +35 -3
  28. tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/async_client.py +2 -2
  29. tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py +2 -2
  30. tests/integration/goldens/credentials/google/iam/credentials_v1/types/common.py +2 -2
  31. tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +2 -2
  32. tests/integration/goldens/eventarc/docs/conf.py +35 -3
  33. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/async_client.py +4 -4
  34. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py +4 -4
  35. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py +8 -8
  36. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/channel.py +1 -1
  37. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/channel_connection.py +1 -1
  38. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/eventarc.py +2 -2
  39. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/google_channel_config.py +1 -1
  40. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/trigger.py +2 -2
  41. tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +4 -4
  42. tests/integration/goldens/logging/docs/conf.py +35 -3
  43. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/async_client.py +5 -5
  44. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py +5 -5
  45. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py +1 -1
  46. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +1 -1
  47. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +1 -1
  48. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/async_client.py +1 -1
  49. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py +1 -1
  50. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/pagers.py +1 -1
  51. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +1 -1
  52. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +1 -1
  53. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +1 -1
  54. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/async_client.py +3 -3
  55. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py +3 -3
  56. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +1 -1
  57. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +1 -1
  58. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +1 -1
  59. tests/integration/goldens/logging/google/cloud/logging_v2/types/log_entry.py +6 -6
  60. tests/integration/goldens/logging/google/cloud/logging_v2/types/logging.py +3 -3
  61. tests/integration/goldens/logging/google/cloud/logging_v2/types/logging_config.py +2 -2
  62. tests/integration/goldens/logging/google/cloud/logging_v2/types/logging_metrics.py +3 -3
  63. tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +4 -4
  64. tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +7 -7
  65. tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +6 -6
  66. tests/integration/goldens/logging_internal/docs/conf.py +35 -3
  67. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/async_client.py +5 -5
  68. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py +5 -5
  69. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py +1 -1
  70. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +1 -1
  71. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +1 -1
  72. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/async_client.py +1 -1
  73. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py +1 -1
  74. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/pagers.py +1 -1
  75. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +1 -1
  76. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +1 -1
  77. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +1 -1
  78. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/async_client.py +3 -3
  79. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py +3 -3
  80. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +1 -1
  81. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +1 -1
  82. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +1 -1
  83. tests/integration/goldens/logging_internal/google/cloud/logging_v2/types/log_entry.py +6 -6
  84. tests/integration/goldens/logging_internal/google/cloud/logging_v2/types/logging.py +3 -3
  85. tests/integration/goldens/logging_internal/google/cloud/logging_v2/types/logging_config.py +2 -2
  86. tests/integration/goldens/logging_internal/google/cloud/logging_v2/types/logging_metrics.py +3 -3
  87. tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +4 -4
  88. tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +7 -7
  89. tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +6 -6
  90. tests/integration/goldens/redis/docs/conf.py +35 -3
  91. tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/async_client.py +5 -5
  92. tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py +5 -5
  93. tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py +8 -8
  94. tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +8 -8
  95. tests/integration/goldens/redis/google/cloud/redis_v1/types/cloud_redis.py +5 -5
  96. tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +7 -7
  97. tests/integration/goldens/redis_selective/docs/conf.py +35 -3
  98. tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/async_client.py +5 -5
  99. tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py +5 -5
  100. tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py +3 -3
  101. tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +3 -3
  102. tests/integration/goldens/redis_selective/google/cloud/redis_v1/types/cloud_redis.py +5 -5
  103. tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +7 -7
  104. tests/unit/samplegen/test_samplegen.py +1 -1
  105. tests/unit/schema/test_imp.py +2 -2
  106. tests/unit/utils/test_cache.py +43 -0
  107. {gapic_generator-1.30.4.dist-info → gapic_generator-1.30.6.dist-info}/entry_points.txt +0 -0
  108. {gapic_generator-1.30.4.dist-info → gapic_generator-1.30.6.dist-info}/licenses/LICENSE +0 -0
  109. {gapic_generator-1.30.4.dist-info → gapic_generator-1.30.6.dist-info}/top_level.txt +0 -0
gapic/cli/generate.py CHANGED
@@ -23,6 +23,7 @@ from google.protobuf.compiler import plugin_pb2
23
23
  from gapic import generator
24
24
  from gapic.schema import api
25
25
  from gapic.utils import Options
26
+ from gapic.utils.cache import generation_cache_context
26
27
 
27
28
 
28
29
  @click.command()
@@ -56,15 +57,23 @@ def generate(request: typing.BinaryIO, output: typing.BinaryIO) -> None:
56
57
  [p.package for p in req.proto_file if p.name in req.file_to_generate]
57
58
  ).rstrip(".")
58
59
 
59
- # Build the API model object.
60
- # This object is a frozen representation of the whole API, and is sent
61
- # to each template in the rendering step.
62
- api_schema = api.API.build(req.proto_file, opts=opts, package=package)
60
+ # Create the generation cache context.
61
+ # This provides the shared storage for the @cached_proto_context decorator.
62
+ # 1. Performance: Memoizes `with_context` calls, speeding up generation significantly.
63
+ # 2. Safety: The decorator uses this storage to "pin" Proto objects in memory.
64
+ # This prevents Python's Garbage Collector from deleting objects created during
65
+ # `API.build` while `Generator.get_response` is still using their IDs.
66
+ # (See `gapic.utils.cache.cached_proto_context` for the specific pinning logic).
67
+ with generation_cache_context():
68
+ # Build the API model object.
69
+ # This object is a frozen representation of the whole API, and is sent
70
+ # to each template in the rendering step.
71
+ api_schema = api.API.build(req.proto_file, opts=opts, package=package)
63
72
 
64
- # Translate into a protobuf CodeGeneratorResponse; this reads the
65
- # individual templates and renders them.
66
- # If there are issues, error out appropriately.
67
- res = generator.Generator(opts).get_response(api_schema, opts)
73
+ # Translate into a protobuf CodeGeneratorResponse; this reads the
74
+ # individual templates and renders them.
75
+ # If there are issues, error out appropriately.
76
+ res = generator.Generator(opts).get_response(api_schema, opts)
68
77
 
69
78
  # Output the serialized response.
70
79
  output.write(res.SerializeToString())
gapic/schema/imp.py CHANGED
@@ -26,11 +26,21 @@ class Import:
26
26
  return self.package == other.package and self.module == other.module
27
27
 
28
28
  def __str__(self) -> str:
29
+ # Determine if we need to suppress type checking for this import.
30
+ # We do this for protobuf generated files (_pb2) and api_core
31
+ # internals where type information might be missing or incomplete.
32
+ needs_type_ignore = self.module.endswith("_pb2") or "api_core" in self.package
33
+ if needs_type_ignore:
34
+ # Use 'import absolute.path as module' syntax to prevent Ruff/isort
35
+ # from combining this with other imports. This ensures the
36
+ # '# type: ignore' comment remains effective for this specific import.
37
+ full_module = ".".join(self.package + (self.module,))
38
+ alias = self.alias or self.module
39
+ return f"import {full_module} as {alias} # type: ignore"
40
+
29
41
  answer = f"import {self.module}"
30
42
  if self.package:
31
43
  answer = f"from {'.'.join(self.package)} {answer}"
32
44
  if self.alias:
33
45
  answer += f" as {self.alias}"
34
- if self.module.endswith("_pb2") or "api_core" in self.package:
35
- answer += " # type: ignore"
36
46
  return answer
gapic/schema/metadata.py CHANGED
@@ -35,6 +35,7 @@ from google.protobuf import descriptor_pb2
35
35
  from gapic.schema import imp
36
36
  from gapic.schema import naming
37
37
  from gapic.utils import cached_property
38
+ from gapic.utils import cached_proto_context
38
39
  from gapic.utils import RESERVED_NAMES
39
40
 
40
41
  # This class is a minor hack to optimize Address's __eq__ method.
@@ -359,6 +360,7 @@ class Address(BaseAddress):
359
360
  return f'{".".join(self.package)}.{selector}'
360
361
  return selector
361
362
 
363
+ @cached_proto_context
362
364
  def with_context(self, *, collisions: Set[str]) -> "Address":
363
365
  """Return a derivative of this address with the provided context.
364
366
 
@@ -398,6 +400,7 @@ class Metadata:
398
400
  return "\n\n".join(self.documentation.leading_detached_comments)
399
401
  return ""
400
402
 
403
+ @cached_proto_context
401
404
  def with_context(self, *, collisions: Set[str]) -> "Metadata":
402
405
  """Return a derivative of this metadata with the provided context.
403
406
 
gapic/schema/wrappers.py CHANGED
@@ -67,6 +67,7 @@ from google.protobuf.json_format import MessageToDict # type: ignore
67
67
 
68
68
  from gapic import utils
69
69
  from gapic.schema import metadata
70
+ from gapic.utils import cached_proto_context
70
71
  from gapic.utils import uri_sample
71
72
  from gapic.utils import make_private
72
73
 
@@ -410,6 +411,7 @@ class Field:
410
411
  "This code should not be reachable; please file a bug."
411
412
  )
412
413
 
414
+ @cached_proto_context
413
415
  def with_context(
414
416
  self,
415
417
  *,
@@ -805,6 +807,7 @@ class MessageType:
805
807
  # message.
806
808
  return cursor.message.get_field(*field_path[1:], collisions=collisions)
807
809
 
810
+ @cached_proto_context
808
811
  def with_context(
809
812
  self,
810
813
  *,
@@ -937,6 +940,7 @@ class EnumType:
937
940
  """Return the identifier data to be used in templates."""
938
941
  return self.meta.address
939
942
 
943
+ @cached_proto_context
940
944
  def with_context(self, *, collisions: Set[str]) -> "EnumType":
941
945
  """Return a derivative of this enum with the provided context.
942
946
 
@@ -1058,6 +1062,7 @@ class ExtendedOperationInfo:
1058
1062
  request_type: MessageType
1059
1063
  operation_type: MessageType
1060
1064
 
1065
+ @cached_proto_context
1061
1066
  def with_context(
1062
1067
  self,
1063
1068
  *,
@@ -1127,6 +1132,7 @@ class OperationInfo:
1127
1132
  response_type: MessageType
1128
1133
  metadata_type: MessageType
1129
1134
 
1135
+ @cached_proto_context
1130
1136
  def with_context(
1131
1137
  self,
1132
1138
  *,
@@ -1937,6 +1943,7 @@ class Method:
1937
1943
  """Return True if this method has no return value, False otherwise."""
1938
1944
  return self.output.ident.proto == "google.protobuf.Empty"
1939
1945
 
1946
+ @cached_proto_context
1940
1947
  def with_context(
1941
1948
  self,
1942
1949
  *,
@@ -2357,6 +2364,7 @@ class Service:
2357
2364
  def is_internal(self) -> bool:
2358
2365
  return any(m.is_internal for m in self.methods.values())
2359
2366
 
2367
+ @cached_proto_context
2360
2368
  def with_context(
2361
2369
  self,
2362
2370
  *,
@@ -196,8 +196,9 @@ def _get_http_options():
196
196
  body_spec (str): The http options body i.e. method.http_options[0].body
197
197
  method_name (str): The method name.
198
198
  service: The service.
199
- is_async (bool): Used to determine the code path i.e. whether for sync or async call. #}
200
- {% macro rest_call_method_common(body_spec, method_name, service, is_async=False, is_proto_plus_type=False) %}
199
+ is_async (bool): Used to determine the code path i.e. whether for sync or async call.
200
+ is_request_message_proto_plus_type (bool): Used to determine whether the request message is a proto-plus type. #}
201
+ {% macro rest_call_method_common(body_spec, method_name, service, is_async=False, is_request_message_proto_plus_type=False) %}
201
202
  {% set service_name = service.name %}
202
203
  {% set await_prefix = "await " if is_async else "" %}
203
204
  {% set async_class_prefix = "Async" if is_async else "" %}
@@ -218,7 +219,7 @@ def _get_http_options():
218
219
  request_url = "{host}{uri}".format(host=self._host, uri=transcoded_request['uri'])
219
220
  method = transcoded_request['method']
220
221
  try:
221
- request_payload = {% if is_proto_plus_type %}type(request).to_json(request){% else %}json_format.MessageToJson(request){% endif %}
222
+ request_payload = {% if is_request_message_proto_plus_type %}type(request).to_json(request){% else %}json_format.MessageToJson(request){% endif %}
222
223
 
223
224
  except:
224
225
  {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2282): Remove try/except and correctly parse request payload. #}
@@ -3,7 +3,7 @@
3
3
  {% block content %}
4
4
 
5
5
  import abc
6
- from typing import Awaitable, Callable, Dict, Optional, Sequence, Union
6
+ from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union
7
7
 
8
8
  {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %}
9
9
  from {{package_path}} import gapic_version as package_version
@@ -240,7 +240,7 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport):
240
240
  {% endif %}
241
241
  """
242
242
 
243
- {{ shared_macros.rest_call_method_common(body_spec, method.name, service, False, method.output.ident.is_proto_plus_type)|indent(8) }}
243
+ {{ shared_macros.rest_call_method_common(body_spec, method.name, service, False, method.input.ident.is_proto_plus_type)|indent(8) }}
244
244
 
245
245
  {% if not method.void %}
246
246
  # Return the response
@@ -202,7 +202,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport):
202
202
  {% endif %}
203
203
  """
204
204
 
205
- {{ shared_macros.rest_call_method_common(body_spec, method.name, service, True, method.output.ident.is_proto_plus_type)|indent(8) }}
205
+ {{ shared_macros.rest_call_method_common(body_spec, method.name, service, True, method.input.ident.is_proto_plus_type)|indent(8) }}
206
206
 
207
207
  {% if not method.void %}
208
208
  # Return the response
@@ -0,0 +1,35 @@
1
+ {% macro sphinx_imports() -%}
2
+ import logging
3
+ from typing import Any
4
+ {%- endmacro %}
5
+
6
+ {% macro sphinx_setup() -%}
7
+ class UnexpectedUnindentFilter(logging.Filter):
8
+ """Filter out warnings about unexpected unindentation following bullet lists."""
9
+
10
+ def filter(self, record: logging.LogRecord) -> bool:
11
+ """Filter the log record.
12
+
13
+ Args:
14
+ record (logging.LogRecord): The log record.
15
+
16
+ Returns:
17
+ bool: False to suppress the warning, True to allow it.
18
+ """
19
+ msg = record.getMessage()
20
+ if "Bullet list ends without a blank line" in msg:
21
+ return False
22
+ return True
23
+
24
+
25
+ def setup(app: Any) -> None:
26
+ """Setup the Sphinx application.
27
+
28
+ Args:
29
+ app (Any): The Sphinx application.
30
+ """
31
+ # Sphinx's logger is hierarchical. Adding a filter to the
32
+ # root 'sphinx' logger will catch warnings from all sub-loggers.
33
+ logger = logging.getLogger('sphinx')
34
+ logger.addFilter(UnexpectedUnindentFilter())
35
+ {%- endmacro %}
@@ -1,7 +1,7 @@
1
1
  {% extends '_base.py.j2' %}
2
2
 
3
3
  {% block content %}
4
-
4
+ {% from "docs/common_setup.py.j2" import sphinx_imports, sphinx_setup %}
5
5
  #
6
6
  # {{ api.naming.warehouse_package_name }} documentation build configuration file
7
7
  #
@@ -14,9 +14,11 @@
14
14
  # All configuration values have a default; values that are commented out
15
15
  # serve to show the default.
16
16
 
17
- import sys
17
+ import logging
18
18
  import os
19
19
  import shlex
20
+ import sys
21
+ {{ sphinx_imports() }}
20
22
 
21
23
  # If extensions (or modules to document with autodoc) are in another directory,
22
24
  # add these directories to sys.path here. If the directory is relative to the
@@ -372,4 +374,7 @@ napoleon_use_admonition_for_references = False
372
374
  napoleon_use_ivar = False
373
375
  napoleon_use_param = True
374
376
  napoleon_use_rtype = True
377
+
378
+ # Setup for sphinx behaviors such as warning filters.
379
+ {{ sphinx_setup() }}
375
380
  {% endblock %}
gapic/utils/__init__.py CHANGED
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from gapic.utils.cache import cached_property
16
+ from gapic.utils.cache import cached_proto_context
16
17
  from gapic.utils.case import to_snake_case
17
18
  from gapic.utils.case import to_camel_case
18
19
  from gapic.utils.checks import is_msg_field_pb
@@ -34,6 +35,7 @@ from gapic.utils.uri_conv import convert_uri_fieldnames
34
35
 
35
36
  __all__ = (
36
37
  "cached_property",
38
+ "cached_proto_context",
37
39
  "convert_uri_fieldnames",
38
40
  "doc",
39
41
  "empty",
gapic/utils/cache.py CHANGED
@@ -13,6 +13,8 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import functools
16
+ import contextlib
17
+ import threading
16
18
 
17
19
 
18
20
  def cached_property(fx):
@@ -43,3 +45,93 @@ def cached_property(fx):
43
45
  return self._cached_values[fx.__name__]
44
46
 
45
47
  return property(inner)
48
+
49
+
50
+ # Thread-local storage for the simple cache dictionary.
51
+ # This ensures that parallel generation tasks (if any) do not corrupt each other's cache.
52
+ _proto_collisions_cache_state = threading.local()
53
+
54
+
55
+ @contextlib.contextmanager
56
+ def generation_cache_context():
57
+ """Context manager to explicitly manage the lifecycle of the generation cache.
58
+
59
+ This manager initializes a fresh dictionary in thread-local storage when entering
60
+ the context and strictly deletes it when exiting.
61
+
62
+ **Memory Management:**
63
+ The cache stores strong references to Proto objects to "pin" them in memory
64
+ (see `cached_proto_context`). It is critical that this context manager deletes
65
+ the dictionary in the `finally` block. Deleting the dictionary breaks the
66
+ reference chain, allowing Python's Garbage Collector to finally free all the
67
+ large Proto objects that were pinned during generation.
68
+ """
69
+ # Initialize the cache as a standard dictionary.
70
+ _proto_collisions_cache_state.resolved_collisions = {}
71
+ try:
72
+ yield
73
+ finally:
74
+ # Delete the dictionary to free all memory and pinned objects.
75
+ # This is essential to prevent memory leaks in long-running processes.
76
+ del _proto_collisions_cache_state.resolved_collisions
77
+
78
+
79
+ def cached_proto_context(func):
80
+ """Decorator to memoize `with_context` calls based on object identity and collisions.
81
+
82
+ This mechanism provides a significant performance boost by preventing
83
+ redundant recalculations of naming collisions during template rendering.
84
+
85
+ Since the Proto wrapper objects are unhashable (mutable), we use `id(self)` as
86
+ the primary cache key. Normally, this is dangerous: if the object is garbage
87
+ collected, Python might reuse its memory address for a *new* object, leading to
88
+ a cache collision (the "Zombie ID" bug).
89
+
90
+ To prevent this, this decorator stores the value as a tuple: `(result, self)`.
91
+ By keeping a reference to `self` in the cache value, we "pin" the object in
92
+ memory. This forces the Garbage Collector to keep the object alive, guaranteeing
93
+ that `id(self)` remains unique for the entire lifespan of the `generation_cache_context`.
94
+
95
+ Args:
96
+ func (Callable): The function to decorate (usually `with_context`).
97
+
98
+ Returns:
99
+ Callable: The wrapped function with caching and pinning logic.
100
+ """
101
+
102
+ @functools.wraps(func)
103
+ def wrapper(self, *, collisions, **kwargs):
104
+
105
+ # 1. Check for active cache (returns None if context is not active)
106
+ context_cache = getattr(
107
+ _proto_collisions_cache_state, "resolved_collisions", None
108
+ )
109
+
110
+ # If we are not inside a generation_cache_context (e.g. unit tests),
111
+ # bypass the cache entirely.
112
+ if context_cache is None:
113
+ return func(self, collisions=collisions, **kwargs)
114
+
115
+ # 2. Create the cache key
116
+ # We use frozenset for collisions to make it hashable.
117
+ # We use id(self) because 'self' is not hashable.
118
+ collisions_key = frozenset(collisions) if collisions else None
119
+ key = (id(self), collisions_key)
120
+
121
+ # 3. Check Cache
122
+ if key in context_cache:
123
+ # The cache stores (result, pinned_object). We return just the result.
124
+ return context_cache[key][0]
125
+
126
+ # 4. Execute the actual function
127
+ # We ensure context_cache is passed down to the recursive calls
128
+ result = func(self, collisions=collisions, **kwargs)
129
+
130
+ # 5. Update Cache & Pin Object
131
+ # We store (result, self). The reference to 'self' prevents garbage collection,
132
+ # ensuring that 'id(self)' cannot be reused for a new object while this
133
+ # cache entry exists.
134
+ context_cache[key] = (result, self)
135
+ return result
136
+
137
+ return wrapper
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gapic-generator
3
- Version: 1.30.4
3
+ Version: 1.30.6
4
4
  Summary: Google API Client Generator for Python
5
5
  Home-page: https://github.com/googleapis/gapic-generator-python
6
6
  Author: Google LLC