Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 3f1a7cb

Browse files
authored
Add generated unit test coverage for lazy import, part 1 (#303)
The Python 3.8 showcase unit tests (and all 'real' generated unit tests when using the lazy import feature) include a test to verify that the logic used for lazy import is covered. If the classes and submodules are available it is assumed that they are the correct ones; asserting that there is no mismatch between lazy import path and corresponding object is outside the current scope of these tests. Lazy import is currently only available for the top level of a module and its corresponding types submodule. Other submodules will be converted to use lazy imports as part of subsequent commits.
1 parent c0cb290 commit 3f1a7cb

4 files changed

Lines changed: 131 additions & 14 deletions

File tree

gapic/templates/%namespace/%name_%version/%sub/__init__.py.j2

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ def from_snake_case(s):
3030

3131

3232
if sys.version_info < (3, 7):
33-
raise ImportError('This module requires Python 3.7 or later.')
33+
raise ImportError('This module requires Python 3.7 or later.') # pragma: NO COVER
3434

3535
_lazy_name_to_package_map = {
3636
'types': '{% if api.naming.module_namespace %}{{ api.naming.module_namespace|join(".") }}.{% endif -%}{{ api.naming.versioned_module_name }}.types',
3737
{%- for service in api.services.values()|sort(attribute='name')|unique(attribute='name') if service.meta.address.subpackage == api.subpackage_view %}
3838
'{{ service.client_name|snake_case }}': '{% if api.naming.module_namespace %}{{ api.naming.module_namespace|join(".") }}.{% endif -%}{{ api.naming.versioned_module_name }}.services.{{ service.name|snake_case }}.client',
39-
'{{ service.transport_name|snake_case }}': '{% if api.naming.module_namespace %}{{ api.naming.module_namespace|join(".") }}.{% endif -%}{{ api.naming.versioned_module_name }}.services.transports.base',
40-
'{{ service.grpc_transport_name|snake_case }}': '{% if api.naming.module_namespace %}{{ api.naming.module_namespace|join(".") }}.{% endif -%}{{ api.naming.versioned_module_name }}.services.transports.grpc',
39+
'{{ service.transport_name|snake_case }}': '{% if api.naming.module_namespace %}{{ api.naming.module_namespace|join(".") }}.{% endif -%}{{ api.naming.versioned_module_name }}.services.{{ service.name|snake_case }}.transports.base',
40+
'{{ service.grpc_transport_name|snake_case }}': '{% if api.naming.module_namespace %}{{ api.naming.module_namespace|join(".") }}.{% endif -%}{{ api.naming.versioned_module_name }}.services.{{ service.name|snake_case }}.transports.grpc',
4141
{%- endfor %}
4242
}
4343

@@ -56,8 +56,9 @@ def __getattr__(name): # Requires Python >= 3.7
5656
if name == '__all__':
5757
all_names = globals()['__all__'] = sorted(
5858
chain(
59-
(from_snake_case(k) for k in _lazy_name_to_package_map),
59+
(from_snake_case(k) for k in _lazy_name_to_package_map if k != 'types'),
6060
_lazy_type_to_package_map,
61+
['types'],
6162
)
6263
)
6364
return all_names
Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,53 @@
11
{% extends '_base.py.j2' %}
22

33
{% block content %}
4+
{% if opts.lazy_import -%} {# lazy import #}
5+
import importlib
6+
import sys
7+
8+
9+
if sys.version_info < (3, 7):
10+
raise ImportError('This module requires Python 3.7 or later.') # pragma: NO COVER
11+
12+
13+
_lazy_type_to_package_map = {
14+
{%- filter sort_lines %}
15+
{%- for proto in api.protos.values() if proto.meta.address.subpackage == api.subpackage_view %}{%- for message in proto.messages.values() %}
16+
'{{ message.name }}': '{% if api.naming.module_namespace %}{{ api.naming.module_namespace|join(".") }}.{% endif -%}{{ api.naming.versioned_module_name }}.types.{{ proto.module_name }}',
17+
{%- endfor %}
18+
{%- for enum in proto.enums.values() %}
19+
'{{ enum.name }}': '{% if api.naming.module_namespace %}{{ api.naming.module_namespace|join(".") }}.{% endif -%}{{ api.naming.versioned_module_name }}.types.{{ proto.module_name }}',
20+
{%- endfor %}{%- endfor %}{%- endfilter %}
21+
}
22+
23+
24+
# Background on how this behaves: https://www.python.org/dev/peps/pep-0562/
25+
def __getattr__(name): # Requires Python >= 3.7
26+
if name == '__all__':
27+
all_names = globals()['__all__'] = sorted(_lazy_type_to_package_map)
28+
return all_names
29+
elif name in _lazy_type_to_package_map:
30+
module = importlib.import_module(f'{_lazy_type_to_package_map[name]}')
31+
klass = getattr(module, name)
32+
{# new_klass = type(name, (klass,), {'__doc__': klass.__doc__}) #}
33+
globals()[name] = klass
34+
return klass
35+
else:
36+
raise AttributeError(f'unknown sub-module {name!r}.')
37+
38+
39+
def __dir__():
40+
return globals().get('__all__') or __getattr__('__all__')
41+
42+
{% else -%}
443
{% for p in api.protos.values() if p.file_to_generate and p.messages -%}
5-
from .{{ p.module_name }} import ({% for m in p.messages.values() %}{{ m.name }}, {% endfor %})
44+
from .{{p.module_name }} import ({% for m in p.messages.values() %}{{ m.name }}, {% endfor %})
645
{% endfor %}
746

847
__all__ = (
948
{%- for p in api.protos.values() if p.file_to_generate %}{% for m in p.messages.values() %}
1049
'{{ m.name }}',
1150
{%- endfor %}{% endfor %}
1251
)
52+
{% endif -%} {# lazy import #}
1353
{% endblock %}

gapic/templates/tests/unit/%name_%version/%sub/test_%service.py.j2

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,75 @@ def test_{{ method.name|snake_case }}_raw_page_lro():
312312

313313
{% endfor -%} {#- method in methods #}
314314

315+
{% if opts.lazy_import -%} {# lazy import #}
316+
def test_module_level_imports():
317+
# Use the other transport import path so that code gets tested.
318+
from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }} import {{ service.name }}GrpcTransport
319+
transport = {{ service.name }}GrpcTransport(
320+
credentials=credentials.AnonymousCredentials(),
321+
)
322+
323+
from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }} import {{ service.client_name }}
324+
client = {{ service.client_name }}(transport=transport)
325+
assert client._transport is transport
326+
327+
from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }} import {{ service.name|snake_case }}_grpc_transport
328+
transport2 = {{ service.name|snake_case }}_grpc_transport.{{ service.name }}GrpcTransport(
329+
credentials=credentials.AnonymousCredentials(),
330+
)
331+
332+
client2 = {{ service.client_name }}(transport=transport2)
333+
assert client2._transport is transport2
334+
335+
{% with type_name = cycler(*service.methods.values()).next().input.name -%}
336+
from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }} import {{ type_name }}
337+
type_ = {{ type_name }}()
338+
339+
try:
340+
from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }} import {{ type_name|lower }}_squidification
341+
except (AttributeError, ImportError) as e:
342+
pass
343+
else:
344+
assert False
345+
{% endwith -%}
346+
347+
import {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }} as mod
348+
all_names = dir(mod)
349+
expected_names = sorted([
350+
'types',
351+
{%- for service in api.services.values()|sort(attribute='name')|unique(attribute='name') if service.meta.address.subpackage == api.subpackage_view %}
352+
'{{ service.client_name }}',
353+
'{{ service.transport_name }}',
354+
'{{ service.grpc_transport_name }}',
355+
{%- endfor %}
356+
{%- for proto in api.protos.values() if proto.meta.address.subpackage == api.subpackage_view %}{%- for message in proto.messages.values() %}
357+
'{{ message.name }}',
358+
{%- endfor %}
359+
{%- for enum in proto.enums.values() %}
360+
'{{ enum.name }}'
361+
{% endfor %}{%- endfor %}
362+
])
363+
assert all_names == expected_names
364+
365+
{% with type_name = cycler(*service.methods.values()).next().input.name -%}
366+
from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.types import {{ type_name }}
367+
type_ = {{ type_name }}()
368+
{% endwith -%}
369+
370+
import {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.types as types
371+
all_types = dir(types)
372+
expected_types = sorted([
373+
{%- for proto in api.protos.values() if proto.meta.address.subpackage == api.subpackage_view %}{%- for message in proto.messages.values() %}
374+
'{{ message.name }}',
375+
{%- endfor %}
376+
{%- for enum in proto.enums.values() %}
377+
'{{ enum.name }}',
378+
{% endfor %}{%- endfor %}
379+
])
380+
assert all_types == expected_types
381+
382+
{% endif -%} {# lazy import #}
383+
315384
def test_credentials_transport_error():
316385
# It is an error to provide credentials and a transport instance.
317386
transport = transports.{{ service.name }}GrpcTransport(

noxfile.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,22 @@ def showcase_unit(session):
109109
)
110110

111111
# Write out a client library for Showcase.
112-
session.run('protoc',
113-
f'--descriptor_set_in={tmp_dir}{os.path.sep}showcase.desc',
114-
f'--python_gapic_out={tmp_dir}',
115-
'google/showcase/v1beta1/echo.proto',
116-
'google/showcase/v1beta1/identity.proto',
117-
'google/showcase/v1beta1/messaging.proto',
118-
'google/showcase/v1beta1/testing.proto',
119-
external=True,
120-
)
112+
args = [
113+
'protoc',
114+
f'--descriptor_set_in={tmp_dir}{os.path.sep}showcase.desc',
115+
f'--python_gapic_out={tmp_dir}',
116+
'google/showcase/v1beta1/echo.proto',
117+
'google/showcase/v1beta1/identity.proto',
118+
'google/showcase/v1beta1/messaging.proto',
119+
'google/showcase/v1beta1/testing.proto',
120+
]
121+
if session.python == '3.8':
122+
args.append('--python_gapic_opt=lazy-import')
123+
124+
session.run(
125+
*args,
126+
external=True,
127+
)
121128

122129
# Install the library.
123130
session.chdir(tmp_dir)

0 commit comments

Comments
 (0)