Skip to content

Commit 72b8857

Browse files
author
Luke Sneeringer
authored
[feat] Type-check the generator. (#130)
This adds type-checking to the generator, and fixes a lot of erroneous or incomplete type annotations. This does _not_ add type-checking to the generated code (that will come next).
1 parent 6c84edb commit 72b8857

File tree

16 files changed

+133
-79
lines changed

16 files changed

+133
-79
lines changed

packages/gapic-generator/.circleci/config.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ workflows:
2828
only: /^\d+\.\d+\.\d+$/
2929
- showcase:
3030
requires:
31+
- docs
32+
- mypy
3133
- showcase-unit-3.6
3234
- showcase-unit-3.7
3335
filters:
@@ -37,10 +39,13 @@ workflows:
3739
filters:
3840
tags:
3941
only: /^\d+\.\d+\.\d+$/
42+
- mypy:
43+
filters:
44+
tags:
45+
only: /^\d+\.\d+\.\d+$/
4046
- publish_package:
4147
requires:
4248
- showcase
43-
- docs
4449
filters:
4550
branches:
4651
ignore: /.*/
@@ -49,7 +54,6 @@ workflows:
4954
- publish_image:
5055
requires:
5156
- showcase
52-
- docs
5357
filters:
5458
branches:
5559
ignore: /.*/
@@ -67,6 +71,17 @@ jobs:
6771
- run:
6872
name: Build the documentation.
6973
command: nox -s docs
74+
mypy:
75+
docker:
76+
- image: python:3.7-slim
77+
steps:
78+
- checkout
79+
- run:
80+
name: Install nox.
81+
command: pip install nox
82+
- run:
83+
name: Check type annotations.
84+
command: nox -s mypy
7085
publish_image:
7186
docker:
7287
- image: docker

packages/gapic-generator/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,6 @@ coverage.xml
5656
# Make sure a generated file isn't accidentally committed.
5757
pylintrc
5858
pylintrc.test
59+
60+
# Mypy
61+
.mypy_cache

packages/gapic-generator/gapic/cli/dump.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818

1919
import click
2020

21-
from google.protobuf.compiler import plugin_pb2
22-
2321

2422
@click.command()
2523
@click.option('--request', type=click.File('rb'), default=sys.stdin.buffer,
@@ -41,6 +39,6 @@ def dump(request: typing.BinaryIO) -> None:
4139
click.secho(
4240
'Request dumped to `request.desc`. '
4341
'This script will now exit 1 to satisfy protoc.',
44-
file=sys.stderr, color='green',
42+
file=sys.stderr, fg='green',
4543
)
4644
sys.exit(1)

packages/gapic-generator/gapic/generator/generator.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import collections
15+
from collections import OrderedDict
16+
from typing import Dict, Mapping
1617
import os
1718
import re
18-
from typing import Mapping, Sequence
1919

2020
import jinja2
2121

@@ -66,7 +66,7 @@ def get_response(self, api_schema: api.API) -> CodeGeneratorResponse:
6666
~.CodeGeneratorResponse: A response describing appropriate
6767
files and contents. See ``plugin.proto``.
6868
"""
69-
output_files = collections.OrderedDict()
69+
output_files: Dict[str, CodeGeneratorResponse.File] = OrderedDict()
7070

7171
# Iterate over each template and add the appropriate output files
7272
# based on that template.
@@ -88,7 +88,7 @@ def _render_template(
8888
self,
8989
template_name: str, *,
9090
api_schema: api.API,
91-
) -> Sequence[CodeGeneratorResponse.File]:
91+
) -> Dict[str, CodeGeneratorResponse.File]:
9292
"""Render the requested templates.
9393
9494
Args:
@@ -103,7 +103,7 @@ def _render_template(
103103
Sequence[~.CodeGeneratorResponse.File]: A sequence of File
104104
objects for inclusion in the final response.
105105
"""
106-
answer = collections.OrderedDict()
106+
answer: Dict[str, CodeGeneratorResponse.File] = OrderedDict()
107107
skip_subpackages = False
108108

109109
# Sanity check: Rendering per service and per proto would be a

packages/gapic-generator/gapic/generator/options.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import Tuple
15+
from typing import Dict, List, Tuple
1616
import dataclasses
1717
import os
1818
import warnings
@@ -26,8 +26,8 @@ class Options:
2626
on unrecognized arguments (essentially, we throw them away, but we do
2727
warn if it looks like it was meant for us).
2828
"""
29-
templates: Tuple[str] = dataclasses.field(default=('DEFAULT',))
30-
namespace: Tuple[str] = dataclasses.field(default=())
29+
templates: Tuple[str, ...] = dataclasses.field(default=('DEFAULT',))
30+
namespace: Tuple[str, ...] = dataclasses.field(default=())
3131
name: str = ''
3232

3333
@classmethod
@@ -45,10 +45,10 @@ def build(cls, opt_string: str) -> 'Options':
4545
~.Options: The Options instance.
4646
"""
4747
# Parse out every option beginning with `python-gapic`
48-
opts = {}
48+
opts: Dict[str, List[str]] = {}
4949
for opt in opt_string.split(','):
5050
# Parse out the key and value.
51-
value = True
51+
value = 'true'
5252
if '=' in opt:
5353
opt, value = opt.split('=')
5454

packages/gapic-generator/gapic/schema/api.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
import dataclasses
2222
import sys
2323
from itertools import chain
24-
from typing import Callable, List, Mapping, Sequence, Set, Tuple
24+
from typing import Callable, Dict, FrozenSet, Mapping, Sequence, Set, Tuple
2525

26-
from google.longrunning import operations_pb2
26+
from google.longrunning import operations_pb2 # type: ignore
2727
from google.protobuf import descriptor_pb2
2828

2929
from gapic.generator import options
@@ -100,21 +100,21 @@ def module_name(self) -> str:
100100
return to_snake_case(self.name.split('/')[-1][:-len('.proto')])
101101

102102
@cached_property
103-
def names(self) -> Set[str]:
103+
def names(self) -> FrozenSet[str]:
104104
"""Return a set of names used by this proto.
105105
106106
This is used for detecting naming collisions in the module names
107107
used for imports.
108108
"""
109109
# Add names of all enums, messages, and fields.
110-
answer = {e.name for e in self.all_enums.values()}
110+
answer: Set[str] = {e.name for e in self.all_enums.values()}
111111
for message in self.all_messages.values():
112112
answer = answer.union({f.name for f in message.fields.values()})
113113
answer.add(message.name)
114114

115115
# Identify any import module names where the same module name is used
116116
# from distinct packages.
117-
modules = {}
117+
modules: Dict[str, Set[str]] = {}
118118
for t in chain(*[m.field_types for m in self.all_messages.values()]):
119119
modules.setdefault(t.ident.module, set())
120120
modules[t.ident.module].add(t.ident.package)
@@ -175,7 +175,7 @@ class API:
175175
"""
176176
naming: api_naming.Naming
177177
all_protos: Mapping[str, Proto]
178-
subpackage_view: Tuple[str] = dataclasses.field(default_factory=tuple)
178+
subpackage_view: Tuple[str, ...] = dataclasses.field(default_factory=tuple)
179179

180180
@classmethod
181181
def build(cls,
@@ -202,7 +202,7 @@ def build(cls,
202202

203203
# Iterate over each FileDescriptorProto and fill out a Proto
204204
# object describing it, and save these to the instance.
205-
protos = {}
205+
protos: Dict[str, Proto] = {}
206206
for fd in file_descriptors:
207207
protos[fd.name] = _ProtoBuilder(
208208
file_descriptor=fd,
@@ -256,7 +256,7 @@ def subpackages(self) -> Mapping[str, 'API']:
256256
Each value in the mapping is another API object, but the ``protos``
257257
property only shows protos belonging to the subpackage.
258258
"""
259-
answer = collections.OrderedDict()
259+
answer: Dict[str, API] = collections.OrderedDict()
260260

261261
# Get the actual subpackages we have.
262262
#
@@ -294,9 +294,9 @@ def __init__(self, file_descriptor: descriptor_pb2.FileDescriptorProto,
294294
file_to_generate: bool,
295295
naming: api_naming.Naming,
296296
prior_protos: Mapping[str, Proto] = None):
297-
self.proto_messages = {}
298-
self.proto_enums = {}
299-
self.proto_services = {}
297+
self.proto_messages: Dict[str, wrappers.MessageType] = {}
298+
self.proto_enums: Dict[str, wrappers.EnumType] = {}
299+
self.proto_services: Dict[str, wrappers.Service] = {}
300300
self.file_descriptor = file_descriptor
301301
self.file_to_generate = file_to_generate
302302
self.prior_protos = prior_protos or {}
@@ -307,7 +307,8 @@ def __init__(self, file_descriptor: descriptor_pb2.FileDescriptorProto,
307307
# the "path", which is a sequence of integers described in more
308308
# detail below; this code simply shifts from a list to a dict,
309309
# with tuples of paths as the dictionary keys.
310-
self.docs = {}
310+
self.docs: Dict[Tuple[int, ...],
311+
descriptor_pb2.SourceCodeInfo.Location] = {}
311312
for location in file_descriptor.source_code_info.location:
312313
self.docs[tuple(location.path)] = location
313314

@@ -414,8 +415,9 @@ def api_messages(self) -> Mapping[str, wrappers.MessageType]:
414415
*[p.all_messages for p in self.prior_protos.values()],
415416
)
416417

417-
def _load_children(self, children: Sequence, loader: Callable, *,
418-
address: metadata.Address, path: Tuple[int]) -> Mapping:
418+
def _load_children(self,
419+
children: Sequence, loader: Callable, *,
420+
address: metadata.Address, path: Tuple[int, ...]) -> Mapping:
419421
"""Return wrapped versions of arbitrary children from a Descriptor.
420422
421423
Args:
@@ -445,9 +447,10 @@ def _load_children(self, children: Sequence, loader: Callable, *,
445447
answer[wrapped.name] = wrapped
446448
return answer
447449

448-
def _get_fields(self, field_pbs: List[descriptor_pb2.FieldDescriptorProto],
449-
address: metadata.Address, path: Tuple[int],
450-
) -> Mapping[str, wrappers.Field]:
450+
def _get_fields(self,
451+
field_pbs: Sequence[descriptor_pb2.FieldDescriptorProto],
452+
address: metadata.Address, path: Tuple[int, ...],
453+
) -> Dict[str, wrappers.Field]:
451454
"""Return a dictionary of wrapped fields for the given message.
452455
453456
Args:
@@ -472,7 +475,7 @@ def _get_fields(self, field_pbs: List[descriptor_pb2.FieldDescriptorProto],
472475
# message wrapper is not yet created, because it needs this object
473476
# first) and this will be None. This case is addressed in the
474477
# `_load_message` method.
475-
answer = collections.OrderedDict()
478+
answer: Dict[str, wrappers.Field] = collections.OrderedDict()
476479
for field_pb, i in zip(field_pbs, range(0, sys.maxsize)):
477480
answer[field_pb.name] = wrappers.Field(
478481
field_pb=field_pb,
@@ -487,9 +490,10 @@ def _get_fields(self, field_pbs: List[descriptor_pb2.FieldDescriptorProto],
487490
# Done; return the answer.
488491
return answer
489492

490-
def _get_methods(self, methods: List[descriptor_pb2.MethodDescriptorProto],
491-
address: metadata.Address, path: Tuple[int],
492-
) -> Mapping[str, wrappers.Method]:
493+
def _get_methods(self,
494+
methods: Sequence[descriptor_pb2.MethodDescriptorProto],
495+
address: metadata.Address, path: Tuple[int, ...],
496+
) -> Mapping[str, wrappers.Method]:
493497
"""Return a dictionary of wrapped methods for the given service.
494498
495499
Args:
@@ -505,7 +509,7 @@ def _get_methods(self, methods: List[descriptor_pb2.MethodDescriptorProto],
505509
:class:`~.wrappers.Method` objects.
506510
"""
507511
# Iterate over the methods and collect them into a dictionary.
508-
answer = collections.OrderedDict()
512+
answer: Dict[str, wrappers.Method] = collections.OrderedDict()
509513
for meth_pb, i in zip(methods, range(0, sys.maxsize)):
510514
lro = None
511515

packages/gapic-generator/gapic/schema/imp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
@dataclasses.dataclass(frozen=True, order=True)
2020
class Import:
21-
package: Tuple[str]
21+
package: Tuple[str, ...]
2222
module: str
2323
alias: str = ''
2424

packages/gapic-generator/gapic/schema/metadata.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"""
2828

2929
import dataclasses
30-
from typing import Tuple, Set
30+
from typing import FrozenSet, Tuple
3131

3232
from google.protobuf import descriptor_pb2
3333

@@ -40,13 +40,13 @@
4040
class Address:
4141
name: str = ''
4242
module: str = ''
43-
module_path: Tuple[int] = dataclasses.field(default_factory=tuple)
44-
package: Tuple[str] = dataclasses.field(default_factory=tuple)
45-
parent: Tuple[str] = dataclasses.field(default_factory=tuple)
43+
module_path: Tuple[int, ...] = dataclasses.field(default_factory=tuple)
44+
package: Tuple[str, ...] = dataclasses.field(default_factory=tuple)
45+
parent: Tuple[str, ...] = dataclasses.field(default_factory=tuple)
4646
api_naming: naming.Naming = dataclasses.field(
4747
default_factory=naming.Naming,
4848
)
49-
collisions: Set[str] = dataclasses.field(default_factory=frozenset)
49+
collisions: FrozenSet[str] = dataclasses.field(default_factory=frozenset)
5050

5151
def __eq__(self, other) -> bool:
5252
return all([getattr(self, i) == getattr(other, i) for i
@@ -145,13 +145,13 @@ def sphinx(self) -> str:
145145
return self.name
146146

147147
@property
148-
def subpackage(self) -> Tuple[str]:
148+
def subpackage(self) -> Tuple[str, ...]:
149149
"""Return the subpackage below the versioned module name, if any."""
150150
return tuple(
151151
self.package[len(self.api_naming.proto_package.split('.')):]
152152
)
153153

154-
def child(self, child_name: str, path: Tuple[int]) -> 'Address':
154+
def child(self, child_name: str, path: Tuple[int, ...]) -> 'Address':
155155
"""Return a new child of the current Address.
156156
157157
Args:
@@ -236,7 +236,7 @@ def resolve(self, selector: str) -> str:
236236
return f'{".".join(self.package)}.{selector}'
237237
return selector
238238

239-
def with_context(self, *, collisions: Set[str]) -> 'Address':
239+
def with_context(self, *, collisions: FrozenSet[str]) -> 'Address':
240240
"""Return a derivative of this address with the provided context.
241241
242242
This method is used to address naming collisions. The returned
@@ -271,7 +271,7 @@ def doc(self):
271271
return '\n\n'.join(self.documentation.leading_detached_comments)
272272
return ''
273273

274-
def with_context(self, *, collisions: Set[str]) -> 'Metadata':
274+
def with_context(self, *, collisions: FrozenSet[str]) -> 'Metadata':
275275
"""Return a derivative of this metadata with the provided context.
276276
277277
This method is used to address naming collisions. The returned

0 commit comments

Comments
 (0)