gapic-generator 1.30.5__py3-none-any.whl → 1.30.7__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 (132) hide show
  1. gapic/cli/generate.py +18 -9
  2. gapic/generator/generator.py +1 -3
  3. gapic/samplegen_utils/snippet_index.py +3 -1
  4. gapic/samplegen_utils/yaml.py +3 -1
  5. gapic/schema/api.py +2 -7
  6. gapic/schema/imp.py +12 -2
  7. gapic/schema/metadata.py +4 -1
  8. gapic/schema/naming.py +1 -1
  9. gapic/schema/wrappers.py +9 -1
  10. gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +4 -3
  11. gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 +4 -2
  12. gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +1 -1
  13. gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +1 -1
  14. gapic/templates/docs/common_setup.py.j2 +35 -0
  15. gapic/templates/docs/conf.py.j2 +7 -2
  16. gapic/templates/noxfile.py.j2 +33 -15
  17. gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +1 -0
  18. gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 +30 -0
  19. gapic/utils/__init__.py +2 -0
  20. gapic/utils/cache.py +91 -0
  21. {gapic_generator-1.30.5.dist-info → gapic_generator-1.30.7.dist-info}/METADATA +1 -1
  22. {gapic_generator-1.30.5.dist-info → gapic_generator-1.30.7.dist-info}/RECORD +132 -131
  23. tests/integration/goldens/asset/docs/conf.py +35 -3
  24. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/async_client.py +10 -8
  25. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py +6 -6
  26. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py +1 -1
  27. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py +1 -1
  28. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py +1 -1
  29. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py +5 -5
  30. tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest_base.py +1 -1
  31. tests/integration/goldens/asset/google/cloud/asset_v1/types/asset_service.py +7 -7
  32. tests/integration/goldens/asset/google/cloud/asset_v1/types/assets.py +9 -9
  33. tests/integration/goldens/asset/noxfile.py +32 -15
  34. tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +7 -6
  35. tests/integration/goldens/credentials/docs/conf.py +35 -3
  36. tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/async_client.py +6 -4
  37. tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py +2 -2
  38. tests/integration/goldens/credentials/google/iam/credentials_v1/types/common.py +2 -2
  39. tests/integration/goldens/credentials/noxfile.py +32 -15
  40. tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +3 -2
  41. tests/integration/goldens/eventarc/docs/conf.py +35 -3
  42. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/async_client.py +8 -6
  43. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py +4 -4
  44. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py +8 -8
  45. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/channel.py +1 -1
  46. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/channel_connection.py +1 -1
  47. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/eventarc.py +2 -2
  48. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/google_channel_config.py +1 -1
  49. tests/integration/goldens/eventarc/google/cloud/eventarc_v1/types/trigger.py +2 -2
  50. tests/integration/goldens/eventarc/noxfile.py +32 -15
  51. tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +5 -4
  52. tests/integration/goldens/logging/docs/conf.py +35 -3
  53. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/async_client.py +9 -7
  54. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py +5 -5
  55. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py +1 -1
  56. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +1 -1
  57. tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +1 -1
  58. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/async_client.py +5 -3
  59. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py +1 -1
  60. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/pagers.py +1 -1
  61. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +1 -1
  62. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +1 -1
  63. tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +1 -1
  64. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/async_client.py +7 -5
  65. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py +3 -3
  66. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +1 -1
  67. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +1 -1
  68. tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +1 -1
  69. tests/integration/goldens/logging/google/cloud/logging_v2/types/log_entry.py +6 -6
  70. tests/integration/goldens/logging/google/cloud/logging_v2/types/logging.py +3 -3
  71. tests/integration/goldens/logging/google/cloud/logging_v2/types/logging_config.py +2 -2
  72. tests/integration/goldens/logging/google/cloud/logging_v2/types/logging_metrics.py +3 -3
  73. tests/integration/goldens/logging/noxfile.py +32 -15
  74. tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +5 -4
  75. tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +8 -7
  76. tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +7 -6
  77. tests/integration/goldens/logging_internal/docs/conf.py +35 -3
  78. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/async_client.py +9 -7
  79. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py +5 -5
  80. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py +1 -1
  81. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +1 -1
  82. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +1 -1
  83. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/async_client.py +5 -3
  84. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py +1 -1
  85. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/pagers.py +1 -1
  86. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +1 -1
  87. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +1 -1
  88. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +1 -1
  89. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/async_client.py +7 -5
  90. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py +3 -3
  91. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +1 -1
  92. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +1 -1
  93. tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +1 -1
  94. tests/integration/goldens/logging_internal/google/cloud/logging_v2/types/log_entry.py +6 -6
  95. tests/integration/goldens/logging_internal/google/cloud/logging_v2/types/logging.py +3 -3
  96. tests/integration/goldens/logging_internal/google/cloud/logging_v2/types/logging_config.py +2 -2
  97. tests/integration/goldens/logging_internal/google/cloud/logging_v2/types/logging_metrics.py +3 -3
  98. tests/integration/goldens/logging_internal/noxfile.py +32 -15
  99. tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +5 -4
  100. tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +8 -7
  101. tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +7 -6
  102. tests/integration/goldens/redis/docs/conf.py +35 -3
  103. tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/async_client.py +9 -7
  104. tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py +5 -5
  105. tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py +8 -8
  106. tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +8 -8
  107. tests/integration/goldens/redis/google/cloud/redis_v1/types/cloud_redis.py +5 -5
  108. tests/integration/goldens/redis/noxfile.py +32 -15
  109. tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +8 -7
  110. tests/integration/goldens/redis_selective/docs/conf.py +35 -3
  111. tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/async_client.py +9 -7
  112. tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py +5 -5
  113. tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py +3 -3
  114. tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +3 -3
  115. tests/integration/goldens/redis_selective/google/cloud/redis_v1/types/cloud_redis.py +5 -5
  116. tests/integration/goldens/redis_selective/noxfile.py +32 -15
  117. tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +8 -7
  118. tests/system/test_lro.py +0 -1
  119. tests/unit/common_types.py +1 -1
  120. tests/unit/generator/test_formatter.py +25 -40
  121. tests/unit/generator/test_generator.py +2 -2
  122. tests/unit/generator/test_options.py +1 -1
  123. tests/unit/samplegen/test_samplegen.py +1 -1
  124. tests/unit/schema/test_api.py +18 -18
  125. tests/unit/schema/test_imp.py +2 -2
  126. tests/unit/schema/wrappers/test_field.py +6 -6
  127. tests/unit/schema/wrappers/test_method.py +2 -1
  128. tests/unit/utils/test_cache.py +43 -0
  129. {gapic_generator-1.30.5.dist-info → gapic_generator-1.30.7.dist-info}/WHEEL +0 -0
  130. {gapic_generator-1.30.5.dist-info → gapic_generator-1.30.7.dist-info}/entry_points.txt +0 -0
  131. {gapic_generator-1.30.5.dist-info → gapic_generator-1.30.7.dist-info}/licenses/LICENSE +0 -0
  132. {gapic_generator-1.30.5.dist-info → gapic_generator-1.30.7.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()
@@ -38,7 +39,7 @@ from gapic.utils import Options
38
39
  "--output",
39
40
  type=click.File("wb"),
40
41
  default=sys.stdout.buffer,
41
- help="Where to output the `CodeGeneratorResponse`. " "Defaults to stdout.",
42
+ help="Where to output the `CodeGeneratorResponse`. Defaults to stdout.",
42
43
  )
43
44
  def generate(request: typing.BinaryIO, output: typing.BinaryIO) -> None:
44
45
  """Generate a full API client description."""
@@ -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())
@@ -131,9 +131,7 @@ class Generator:
131
131
  )
132
132
 
133
133
  # Return the CodeGeneratorResponse output.
134
- res = CodeGeneratorResponse(
135
- file=[i for i in output_files.values()]
136
- ) # type: ignore
134
+ res = CodeGeneratorResponse(file=[i for i in output_files.values()]) # type: ignore
137
135
  res.supported_features |= CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL # type: ignore
138
136
  return res
139
137
 
@@ -117,7 +117,9 @@ class SnippetIndex:
117
117
  self.metadata_index.client_library.name = (
118
118
  api_schema.naming.warehouse_package_name
119
119
  )
120
- self.metadata_index.client_library.language = snippet_metadata_pb2.Language.PYTHON # type: ignore
120
+ self.metadata_index.client_library.language = (
121
+ snippet_metadata_pb2.Language.PYTHON # type: ignore
122
+ )
121
123
 
122
124
  self.metadata_index.client_library.version = api_schema.gapic_version
123
125
 
@@ -116,7 +116,9 @@ class Map(Element):
116
116
  return next(
117
117
  iter(
118
118
  [
119
- e.val for e in self.elements if e.key == key # type: ignore
119
+ e.val
120
+ for e in self.elements
121
+ if e.key == key # type: ignore
120
122
  ] # type: ignore
121
123
  ),
122
124
  default,
gapic/schema/api.py CHANGED
@@ -941,11 +941,7 @@ class API:
941
941
  selective_gapic_errors = {}
942
942
  # TODO(https://github.com/googleapis/gapic-generator-python/issues/2446):
943
943
  # Workaround issue in Python 3.14 related to code coverage by adding `# pragma: no branch`
944
- for (
945
- method_name
946
- ) in (
947
- library_settings.python_settings.common.selective_gapic_generation.methods
948
- ): # pragma: no branch
944
+ for method_name in library_settings.python_settings.common.selective_gapic_generation.methods: # pragma: no branch
949
945
  if method_name not in self.all_methods:
950
946
  selective_gapic_errors[method_name] = "Method does not exist."
951
947
  elif not method_name.startswith(library_settings.version):
@@ -1180,8 +1176,7 @@ class _ProtoBuilder:
1180
1176
  object.__setattr__(field, "enum", maybe_enum_type)
1181
1177
  else:
1182
1178
  raise TypeError(
1183
- f"Unknown type referenced in "
1184
- f"{self.file_descriptor.name}: '{key}'"
1179
+ f"Unknown type referenced in {self.file_descriptor.name}: '{key}'"
1185
1180
  )
1186
1181
 
1187
1182
  # Only generate the service if this is a target file to be generated.
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.
@@ -356,9 +357,10 @@ class Address(BaseAddress):
356
357
  str: An absolute selector.
357
358
  """
358
359
  if "." not in selector:
359
- return f'{".".join(self.package)}.{selector}'
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/naming.py CHANGED
@@ -86,7 +86,7 @@ class Naming(abc.ABC):
86
86
  "The protos provided do not share a common root package. "
87
87
  "Ensure that all explicitly-specified protos are for a "
88
88
  "single API. "
89
- f'The packages we got are: {", ".join(proto_packages)}'
89
+ f"The packages we got are: {', '.join(proto_packages)}"
90
90
  )
91
91
 
92
92
  # Define the valid regex to split the package.
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
  *,
@@ -797,7 +799,7 @@ class MessageType:
797
799
  # Quick check: If this cursor has no message, there is a problem.
798
800
  if not cursor.message:
799
801
  raise KeyError(
800
- f'Field {".".join(field_path)} could not be resolved from '
802
+ f"Field {'.'.join(field_path)} could not be resolved from "
801
803
  f"{cursor.name}.",
802
804
  )
803
805
 
@@ -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. #}
@@ -98,7 +98,8 @@ class {{ service.async_client_name }}:
98
98
  Returns:
99
99
  {{ service.async_client_name }}: The constructed client.
100
100
  """
101
- return {{ service.client_name }}.from_service_account_info.__func__({{ service.async_client_name }}, info, *args, **kwargs) # type: ignore
101
+ sa_info_func = {{ service.client_name }}.from_service_account_info.__func__ # type: ignore
102
+ return sa_info_func({{ service.async_client_name }}, info, *args, **kwargs)
102
103
 
103
104
  @classmethod
104
105
  def from_service_account_file(cls, filename: str, *args, **kwargs):
@@ -114,7 +115,8 @@ class {{ service.async_client_name }}:
114
115
  Returns:
115
116
  {{ service.async_client_name }}: The constructed client.
116
117
  """
117
- return {{ service.client_name }}.from_service_account_file.__func__({{ service.async_client_name }}, filename, *args, **kwargs) # type: ignore
118
+ sa_file_func = {{ service.client_name }}.from_service_account_file.__func__ # type: ignore
119
+ return sa_file_func({{ service.async_client_name }}, filename, *args, **kwargs)
118
120
 
119
121
  from_service_account_json = from_service_account_file
120
122
 
@@ -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 %}
@@ -12,8 +12,7 @@ import warnings
12
12
 
13
13
  import nox
14
14
 
15
- BLACK_VERSION = "black[jupyter]==23.7.0"
16
- ISORT_VERSION = "isort==5.11.0"
15
+ RUFF_VERSION = "ruff==0.14.14"
17
16
 
18
17
  {% if api.naming.module_namespace %}
19
18
  LINT_PATHS = ["docs", "{{ api.naming.module_namespace[0] }}", "tests", "noxfile.py", "setup.py"]
@@ -147,13 +146,18 @@ def lint(session):
147
146
  Returns a failure if the linters find linting errors or sufficiently
148
147
  serious code quality issues.
149
148
  """
150
- session.install("flake8", BLACK_VERSION)
149
+ session.install("flake8", RUFF_VERSION)
150
+
151
+ # 2. Check formatting
151
152
  session.run(
152
- "black",
153
+ "ruff", "format",
153
154
  "--check",
155
+ f"--target-version=py{ALL_PYTHON[0].replace('.', '')}",
156
+ "--line-length=88",
154
157
  *LINT_PATHS,
155
158
  )
156
159
 
160
+
157
161
  {% if api.naming.module_namespace %}
158
162
  session.run("flake8", "{{ api.naming.module_namespace[0] }}", "tests")
159
163
  {% else %}
@@ -163,10 +167,15 @@ def lint(session):
163
167
 
164
168
  @nox.session(python=DEFAULT_PYTHON_VERSION)
165
169
  def blacken(session):
166
- """Run black. Format code to uniform standard."""
167
- session.install(BLACK_VERSION)
170
+ """(Deprecated) Legacy session. Please use 'nox -s format'."""
171
+ session.log("WARNING: The 'blacken' session is deprecated and will be removed in a future release. Please use 'nox -s format' in the future.")
172
+
173
+ # Just run the ruff formatter (keeping legacy behavior of only formatting, not sorting imports)
174
+ session.install(RUFF_VERSION)
168
175
  session.run(
169
- "black",
176
+ "ruff", "format",
177
+ f"--target-version=py{ALL_PYTHON[0].replace('.', '')}",
178
+ "--line-length=88",
170
179
  *LINT_PATHS,
171
180
  )
172
181
 
@@ -174,19 +183,28 @@ def blacken(session):
174
183
  @nox.session(python=DEFAULT_PYTHON_VERSION)
175
184
  def format(session):
176
185
  """
177
- Run isort to sort imports. Then run black
178
- to format code to uniform standard.
186
+ Run ruff to sort imports and format code.
179
187
  """
180
- session.install(BLACK_VERSION, ISORT_VERSION)
181
- # Use the --fss option to sort imports using strict alphabetical order.
182
- # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections
188
+ # 1. Install ruff (skipped automatically if you run with --no-venv)
189
+ session.install(RUFF_VERSION)
190
+
191
+ # 2. Run Ruff to fix imports
192
+ # check --select I: Enables strict import sorting
193
+ # --fix: Applies the changes automatically
183
194
  session.run(
184
- "isort",
185
- "--fss",
195
+ "ruff", "check",
196
+ "--select", "I",
197
+ "--fix",
198
+ f"--target-version=py{ALL_PYTHON[0].replace('.', '')}",
199
+ "--line-length=88", # Standard Black line length
186
200
  *LINT_PATHS,
187
201
  )
202
+
203
+ # 3. Run Ruff to format code
188
204
  session.run(
189
- "black",
205
+ "ruff", "format",
206
+ f"--target-version=py{ALL_PYTHON[0].replace('.', '')}",
207
+ "--line-length=88", # Standard Black line length
190
208
  *LINT_PATHS,
191
209
  )
192
210
 
@@ -26,6 +26,7 @@ from google.protobuf import json_format
26
26
  import json
27
27
  import math
28
28
  import pytest
29
+ from collections.abc import Sequence, Mapping
29
30
  from google.api_core import api_core_version
30
31
  from proto.marshal.rules.dates import DurationRule, TimestampRule
31
32
  from proto.marshal.rules import wrappers
@@ -752,8 +752,18 @@ def test_{{ method_name }}_pager(transport_name: str = "grpc"):
752
752
 
753
753
  results = list(pager)
754
754
  assert len(results) == 6
755
+ {% if method.paged_result_field.type.ident|string == 'struct_pb2.ListValue' %}
756
+ assert all(isinstance(i, Sequence)
757
+ for i in results)
758
+ {% elif method.paged_result_field.type.ident|string == 'struct_pb2.Struct' %}
759
+ assert all(isinstance(i, Mapping)
760
+ for i in results)
761
+ {% elif method.paged_result_field.type.ident|string == 'struct_pb2.Value' %}
762
+ assert all(True for i in results)
763
+ {% else %}
755
764
  assert all(isinstance(i, {{ method.paged_result_field.type.ident }})
756
765
  for i in results)
766
+ {% endif %}
757
767
  {% endif %}
758
768
  def test_{{ method_name }}_pages(transport_name: str = "grpc"):
759
769
  client = {{ service.client_name }}(
@@ -913,9 +923,19 @@ async def test_{{ method_name }}_async_pager():
913
923
  assert async_pager.get('a') is None
914
924
  assert isinstance(async_pager.get('h'), {{ method.paged_result_field.type.fields.get('value').ident }})
915
925
  {% else %}
926
+ {% if method.paged_result_field.type.ident|string == 'struct_pb2.ListValue' %}
927
+ assert all(isinstance(i, Sequence)
928
+ for i in responses)
929
+ {% elif method.paged_result_field.type.ident|string == 'struct_pb2.Struct' %}
930
+ assert all(isinstance(i, Mapping)
931
+ for i in responses)
932
+ {% elif method.paged_result_field.type.ident|string == 'struct_pb2.Value' %}
933
+ assert all(True for i in responses)
934
+ {% else %}
916
935
  assert all(isinstance(i, {{ method.paged_result_field.type.ident }})
917
936
  for i in responses)
918
937
  {% endif %}
938
+ {% endif %}
919
939
 
920
940
 
921
941
  @pytest.mark.asyncio
@@ -1412,9 +1432,19 @@ def test_{{ method_name }}_rest_pager(transport: str = 'rest'):
1412
1432
  assert pager.get('a') is None
1413
1433
  assert isinstance(pager.get('h'), {{ method.paged_result_field.type.fields.get('value').ident }})
1414
1434
  {% else %}
1435
+ {% if method.paged_result_field.type.ident|string == 'struct_pb2.ListValue' %}
1436
+ assert all(isinstance(i, Sequence)
1437
+ for i in results)
1438
+ {% elif method.paged_result_field.type.ident|string == 'struct_pb2.Struct' %}
1439
+ assert all(isinstance(i, Mapping)
1440
+ for i in results)
1441
+ {% elif method.paged_result_field.type.ident|string == 'struct_pb2.Value' %}
1442
+ assert all(True for i in results)
1443
+ {% else %}
1415
1444
  assert all(isinstance(i, {{ method.paged_result_field.type.ident }})
1416
1445
  for i in results)
1417
1446
  {% endif %}
1447
+ {% endif %}
1418
1448
 
1419
1449
  pages = list(client.{{ method_name }}(request=sample_request).pages)
1420
1450
  for page_, token in zip(pages, ['abc','def','ghi', '']):
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,92 @@ 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
+ # 1. Check for active cache (returns None if context is not active)
105
+ context_cache = getattr(
106
+ _proto_collisions_cache_state, "resolved_collisions", None
107
+ )
108
+
109
+ # If we are not inside a generation_cache_context (e.g. unit tests),
110
+ # bypass the cache entirely.
111
+ if context_cache is None:
112
+ return func(self, collisions=collisions, **kwargs)
113
+
114
+ # 2. Create the cache key
115
+ # We use frozenset for collisions to make it hashable.
116
+ # We use id(self) because 'self' is not hashable.
117
+ collisions_key = frozenset(collisions) if collisions else None
118
+ key = (id(self), collisions_key)
119
+
120
+ # 3. Check Cache
121
+ if key in context_cache:
122
+ # The cache stores (result, pinned_object). We return just the result.
123
+ return context_cache[key][0]
124
+
125
+ # 4. Execute the actual function
126
+ # We ensure context_cache is passed down to the recursive calls
127
+ result = func(self, collisions=collisions, **kwargs)
128
+
129
+ # 5. Update Cache & Pin Object
130
+ # We store (result, self). The reference to 'self' prevents garbage collection,
131
+ # ensuring that 'id(self)' cannot be reused for a new object while this
132
+ # cache entry exists.
133
+ context_cache[key] = (result, self)
134
+ return result
135
+
136
+ return wrapper
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gapic-generator
3
- Version: 1.30.5
3
+ Version: 1.30.7
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