refactor(gapic-generator): deduplicate sync/async prep_wrapped_messages and transport method wrapping
Mantenedores costumam responder em até 1 dia
Ninguém assumiu esta issue ainda.
Avaliação
- Dificuldade
- 5/5
- Tempo estimado
- Mais de uma semana
- Facilidade para iniciantes
- 35/100
- Tipo de issue
- Refatoração
- Clareza
- Precisa de esclarecimento
- Status de atividade
- Ativa
- Stack de tecnologia
- python
- Domínio
- build-system, tooling
Direção de pesquisa
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.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
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.
- Linguagem predominante
- Python
- Estrelas
- 5.4k
- Forks
- 1.8k
- Merge médio
- 2d 7h
- PRs com merge (30d)
- 173
Preparar o ambiente
- Sem Dockerfile nem arquivo Docker Compose
- Tem um modelo de pull request
- Ler o guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de googleapis/google-cloud-python
-
firestore: field path with a quoted name ending in a backslash is misparsedTalvez já em andamento @sebxt assumiu há 25 dias. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
googleapis/google-cloud-python#18771 ·
Mantenedores costumam responder em até 1 dia
-
api: spanner type: feature request
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 68/100
googleapis/google-cloud-python#18584 ·
Mantenedores costumam responder em até 1 dia
-
auth: `impersonated_credentials` loses `subject` on copy, so `with_scopes()` and `with_quota_project()` turn off domain wide delegationTalvez já em andamento @Om-singhaI assumiu há 55 dias. Abertaauth priority: p2 type: bug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 88/100
googleapis/google-cloud-python#18428 ·
Mantenedores costumam responder em até 1 dia
-
A client called `run_in_transaction` be stuck if Spanner is downTalvez já em andamento @founderinaction assumiu há 7 dias. Abertapriority: p2 type: bug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
googleapis/google-cloud-python#18375 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
ci: enable --strict flag in presubmit twine_checkTalvez livre de novo @ohmayr assumiu há 44 dias e não há nenhum pull request aberto. Aberta
Dificuldade 1/5 Menos de uma hora Facilidade para iniciantes 76/100
googleapis/google-cloud-python#18339 · 1 comentário · 1 responsável ·
Mantenedores costumam responder em até 1 dia
Todas as issues de googleapis/google-cloud-python
Issues semelhantes
-
plot_graph crashes when a mesh node has no m2m edgeTalvez já em andamento @tygvi assumiu hoje. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
mllam/neural-lam#883 · 1 comentário ·
Mantenedores costumam responder em até 2 dias
-
enhancement possible-duplicate
Dificuldade 1/5 1-3 horas Facilidade para iniciantes 78/100
AOSSIE-Org/PictoPy#1715 · 2 comentários ·
Mantenedores costumam responder em até 6 dias
-
SQLite connector omits generated columns from the catalog, and drops relationships sourced on themAberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
NVIDIA/auto-ontology#511 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
ethereum/execution-specs#3996 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 62/100
cpp-best-practices/project_validator#267 ·
Mantenedores costumam responder em até 1 dia