refactor(gapic-generator): deduplicate sync/async prep_wrapped_messages and transport method wrapping
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Refactor
- Clarity
- Needs clarification
- Activity status
- Active
- Tech stack
- python
- Domain
- build-system, tooling
Research direction
Start by comparing the synchronous implementation in packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 with prep_wrapped_messages_async_method in _shared_macros.j2, then review the related PR #18433. Decide among the proposed consolidation approaches and verify that generated sync, AsyncIO gRPC, REST, and AsyncIO REST transports retain their wrapping and retry behavior without the duplicated templates.
Written by the indexing model from the issue text.
Description
Problem Summary
In packages/gapic-generator/gapic/templates/, there is code duplication and structural asymmetry between synchronous and asynchronous RPC preparation and method wrapping across the generated transport classes:
-
_prep_wrapped_messagesDuplication:packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2(lines 239–280) defines_prep_wrapped_messages(self, client_info)for synchronous transports.packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2(lines 322–371) definesprep_wrapped_messages_async_method(api, service)for asynchronous transports.- These two blocks are ~50 lines of Jinja template code that are nearly identical. Both iterate over
service.methods.values()andapi.mixin_api_methods.keys(), mapping every RPC to its retry predicate, default timeout, client info, fully-qualified method name, and streaming flags. The only differences are:self._wrap_methodvsself._wrap_async_methodretries.Retry(...)vsretries.AsyncRetry(...)
-
_wrap_methodvs_wrap_async_methodDuplication:- Both methods are defined side-by-side in
base.py.j2(lines 174–237). - Both methods implement identical compatibility checks and argument filtering across three generations of
google-api-core(client_options,kind,method_name,is_streaming), delegating respectively togapic_v1.method.wrap_methodandgapic_v1.method_async.wrap_method. - Consider moving these to
_compat.pyto reduce the footprint in generated code.
- Both methods are defined side-by-side in
-
Architectural Asymmetry (Base Class vs Shared Macros):
_wrap_async_methodis defined directly on the synchronous base class{{ service.name }}Transportinbase.py.j2, even though synchronous transports never call it.- Conversely, asynchronous
_prep_wrapped_messagesis not inbase.py.j2—it lives as an external Jinja macro in_shared_macros.j2that is injected intogrpc_asyncio.py.j2andrest_asyncio.py.j2to override the base class implementation. - This mixing makes the transport hierarchy harder to reason about and causes maintenance drift when retry parameters, metadata, or tracing attributes are updated.
Deduplication Options to Consider
Option A: Polymorphic Dispatch in Base Transport (Recommended)
Consolidate _prep_wrapped_messages into a single, unified implementation in base.py.j2 by making transport classes declare their retry class and RPC wrapper callable:
# In base.py.j2 ({{ service.name }}Transport):
_is_async: bool = False
_retry_type = retries.Retry
def _wrap_rpc(self, func, *args, **kwargs):
# Unified wrap helper parameterized by sync vs async
wrap_func = gapic_v1.method_async.wrap_method if self._is_async else gapic_v1.method.wrap_method
supports_tracing = _ASYNC_WRAP_METHOD_SUPPORTS_TRACING if self._is_async else _WRAP_METHOD_SUPPORTS_TRACING
if supports_tracing:
kwargs["client_options"] = self._client_options
if self.kind:
kwargs["kind"] = self.kind
return wrap_func(func, *args, **kwargs)
...
- Pros:
_prep_wrapped_messagesonly exists once inbase.py.j2. All transports (gRPC, AsyncIO gRPC, REST, AsyncIO REST) inherit it naturally.- Deletes
prep_wrapped_messages_async_methodfrom_shared_macros.j2entirely (~50 lines removed). - Eliminates duplicate template loops for mixin and service methods.
- Cons: Requires setting transport class-level attributes (
_is_async,_retry_type) on async transports.
Option B: Parameterized Jinja Macro
Unify synchronous and asynchronous message preparation into a single reusable Jinja macro in _shared_macros.j2:
{% macro prep_wrapped_messages(api, service, is_async=False) %}
def _prep_wrapped_messages(self, client_info):
self._wrapped_methods = {
{% for method in service.methods.values() %}
self.{{ method.transport_safe_name|snake_case }}: self.{{ "_wrap_async_method" if is_async else "_wrap_method" }}(
...
{% if method.retry %}
default_retry=retries.{{ "AsyncRetry" if is_async else "Retry" }}(...),
{% endif %}
...
),
{% endfor %}
}
{% endmacro %}
- Pros:
- Keeps generation purely in templates without modifying Python transport class contracts or inheritance semantics.
- Single source of truth for method iteration and configuration.
- Cons: Still renders two copies of the method across generated files; Jinja syntax becomes slightly more branchy.
Option C: Dedicated AsyncTransport Base Class
Introduce an explicit _Base{{ service.name }}AsyncTransport class (similar to _Base{{ service.name }}RestTransport):
-
{{ service.name }}Transport: Sync base class containing sync_prep_wrapped_messagesand_wrap_method. -
_Base{{ service.name }}AsyncTransport: Async base class containing async_prep_wrapped_messagesand_wrap_async_method. -
Pros:
- Clean separation of concerns; async wrappers are no longer baked into sync base classes.
- Idiomatic object-oriented hierarchy.
-
Cons:
- Larger structural change across generated codebases and public type contracts.
- Requires updating multiple transport inheritance graphs.
Related Discussion
- PR #18433:
feat(observability): [WIP] implement universal 4-path OpenTelemetry tracing - Follow-up from reviewer feedback regarding code duplication between sync and async transports.
- Dominant language
- Python
- Stars
- 5.4k
- Forks
- 1.8k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 173
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from googleapis/google-cloud-python
-
firestore: field path with a quoted name ending in a backslash is misparsedPossibly taken @sebxt claimed this 25 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
googleapis/google-cloud-python#18771 ·
Maintainers usually reply within 1 day
-
api: spanner type: feature request
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
googleapis/google-cloud-python#18584 ·
Maintainers usually reply within 1 day
-
auth: `impersonated_credentials` loses `subject` on copy, so `with_scopes()` and `with_quota_project()` turn off domain wide delegationPossibly taken @Om-singhaI claimed this 55 days ago. Openauth priority: p2 type: bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
googleapis/google-cloud-python#18428 ·
Maintainers usually reply within 1 day
-
A client called `run_in_transaction` be stuck if Spanner is downPossibly taken @founderinaction claimed this 7 days ago. Openpriority: p2 type: bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
googleapis/google-cloud-python#18375 · 1 comment ·
Maintainers usually reply within 1 day
-
ci: enable --strict flag in presubmit twine_checkMay be free again @ohmayr claimed this 44 days ago, and no pull request is open. Open
Difficulty 1/5 Under an hour Newbie friendliness 76/100
googleapis/google-cloud-python#18339 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
All issues in googleapis/google-cloud-python
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
cpp-best-practices/project_validator#267 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Project-N-E-K-O/N.E.K.O#3725 ·
Maintainers usually reply within 1 day
-
[Bug] Sending a Telegram message chain twice duplicates mentionsPossibly taken @123XieC claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
AstrBotDevs/AstrBot#11125 ·
Maintainers usually reply within 1 day
-
backend bug priority: high
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
nooriegarg/ContribIQ#73 ·
Maintainers usually reply within 1 day
-
bug p3 security
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day