Skip to content

Commit e873432

Browse files
Determine naming collisions up front. (#58)
This commit determines and resolves naming collisions logically prior to templates (in other words, so templates do not have to worry about it and they just get the "right thing").
1 parent 04c7a8f commit e873432

File tree

11 files changed

+219
-65
lines changed

11 files changed

+219
-65
lines changed

packages/gapic-generator/.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ ignore =
55
E123, E124
66
# Line over-indented for visual indent.
77
# This works poorly with type annotations in method declarations.
8-
E128, E131
8+
E126, E128, E131

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,13 @@ def python_modules(self) -> Sequence[Tuple[str, str]]:
121121
of statement.
122122
"""
123123
answer = set()
124-
self_reference = self.meta.address.context(self).python_import
124+
self_reference = self.meta.address.python_import
125125
for t in chain(*[m.field_types for m in self.messages.values()]):
126126
# Add the appropriate Python import for the field.
127127
# Sanity check: We do make sure that we are not trying to have
128128
# a module import itself.
129-
imp = t.ident.context(self).python_import
130-
if imp != self_reference:
131-
answer.add(imp)
129+
if t.ident.python_import != self_reference:
130+
answer.add(t.ident.python_import)
132131

133132
# Done; return the sorted sequence.
134133
return tuple(sorted(list(answer)))
@@ -144,10 +143,14 @@ def top(self) -> 'Proto':
144143
return type(self)(
145144
file_pb2=self.file_pb2,
146145
services=self.services,
147-
messages={k: v for k, v in self.messages.items()
148-
if not v.meta.address.parent},
149-
enums={k: v for k, v in self.enums.items()
150-
if not v.meta.address.parent},
146+
messages=collections.OrderedDict([
147+
(k, v) for k, v in self.messages.items()
148+
if not v.meta.address.parent
149+
]),
150+
enums=collections.OrderedDict([
151+
(k, v) for k, v in self.enums.items()
152+
if not v.meta.address.parent
153+
]),
151154
file_to_generate=False,
152155
meta=self.meta,
153156
)
@@ -326,7 +329,10 @@ def __init__(self, file_descriptor: descriptor_pb2.FileDescriptorProto,
326329
@property
327330
def proto(self) -> Proto:
328331
"""Return a Proto dataclass object."""
329-
return Proto(
332+
# Create a "context-naïve" proto.
333+
# This has everything but is ignorant of naming collisions in the
334+
# ultimate file that will be written.
335+
naive = Proto(
330336
enums=self.enums,
331337
file_pb2=self.file_descriptor,
332338
file_to_generate=self.file_to_generate,
@@ -337,6 +343,30 @@ def proto(self) -> Proto:
337343
),
338344
)
339345

346+
# If this is not a file being generated, we do not need to
347+
# do anything else.
348+
if not self.file_to_generate:
349+
return naive
350+
351+
# Return a context-aware proto object.
352+
# Note: The services bind to themselves, because services get their
353+
# own output files.
354+
return dataclasses.replace(naive,
355+
enums=collections.OrderedDict([
356+
(k, v.with_context(collisions=naive.names))
357+
for k, v in naive.enums.items()
358+
]),
359+
messages=collections.OrderedDict([
360+
(k, v.with_context(collisions=naive.names))
361+
for k, v in naive.messages.items()
362+
]),
363+
services=collections.OrderedDict([
364+
(k, v.with_context(collisions=v.names))
365+
for k, v in naive.services.items()
366+
]),
367+
meta=naive.meta.with_context(collisions=naive.names),
368+
)
369+
340370
@cached_property
341371
def all_enums(self) -> Mapping[str, wrappers.EnumType]:
342372
return collections.ChainMap({}, self.enums,

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

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,6 @@ def child(self, child_name: str, path: Tuple[int]) -> 'Address':
153153
parent=self.parent + (self.name,) if self.name else self.parent,
154154
)
155155

156-
def context(self, context) -> 'Address':
157-
"""Return a derivative of this address with the provided context.
158-
159-
This method is used to address naming collisions. The returned
160-
``Address`` object aliases module names to avoid naming collisions in
161-
the file being written.
162-
"""
163-
return dataclasses.replace(self, collisions=frozenset(context.names))
164-
165156
def rel(self, address: 'Address') -> str:
166157
"""Return an identifier for this type, relative to the given address.
167158
@@ -233,6 +224,15 @@ def resolve(self, selector: str) -> str:
233224
return f'{".".join(self.package)}.{selector}'
234225
return selector
235226

227+
def with_context(self, *, collisions: Set[str]) -> 'Address':
228+
"""Return a derivative of this address with the provided context.
229+
230+
This method is used to address naming collisions. The returned
231+
``Address`` object aliases module names to avoid naming collisions in
232+
the file being written.
233+
"""
234+
return dataclasses.replace(self, collisions=frozenset(collisions))
235+
236236

237237
@dataclasses.dataclass(frozen=True)
238238
class Metadata:
@@ -259,6 +259,17 @@ def doc(self):
259259
return '\n\n'.join(self.documentation.leading_detached_comments)
260260
return ''
261261

262+
def with_context(self, *, collisions: Set[str]) -> 'Metadata':
263+
"""Return a derivative of this metadata with the provided context.
264+
265+
This method is used to address naming collisions. The returned
266+
``Address`` object aliases module names to avoid naming collisions in
267+
the file being written.
268+
"""
269+
return dataclasses.replace(self,
270+
address=self.address.with_context(collisions=collisions),
271+
)
272+
262273

263274
@dataclasses.dataclass(frozen=True)
264275
class FieldIdentifier:
@@ -275,7 +286,3 @@ def sphinx(self) -> str:
275286
if self.repeated:
276287
return f'Sequence[{self.ident.sphinx}]'
277288
return self.ident.sphinx
278-
279-
def context(self, arg) -> 'FieldIdentifier':
280-
"""Return self. Provided for compatibility with Address."""
281-
return self

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

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,23 @@ def type(self) -> Union['MessageType', 'EnumType', 'PythonType']:
128128
raise TypeError('Unrecognized protobuf type. This code should '
129129
'not be reachable; please file a bug.')
130130

131+
def with_context(self, *, collisions: Set[str]) -> 'Field':
132+
"""Return a derivative of this field with the provided context.
133+
134+
This method is used to address naming collisions. The returned
135+
``Field`` object aliases module names to avoid naming collisions
136+
in the file being written.
137+
"""
138+
return dataclasses.replace(self,
139+
message=self.message.with_context(
140+
collisions=collisions,
141+
skip_fields=True,
142+
) if self.message else None,
143+
enum=self.enum.with_context(collisions=collisions)
144+
if self.enum else None,
145+
meta=self.meta.with_context(collisions=collisions),
146+
)
147+
131148

132149
@dataclasses.dataclass(frozen=True)
133150
class MessageType:
@@ -152,7 +169,13 @@ def field_types(self) -> Sequence[Union['MessageType', 'EnumType']]:
152169
answer.append(field.type)
153170
return tuple(answer)
154171

155-
def get_field(self, *field_path: Sequence[str]) -> Field:
172+
@property
173+
def ident(self) -> metadata.Address:
174+
"""Return the identifier data to be used in templates."""
175+
return self.meta.address
176+
177+
def get_field(self, *field_path: Sequence[str],
178+
collisions: Set[str] = frozenset()) -> Field:
156179
"""Return a field arbitrarily deep in this message's structure.
157180
158181
This method recursively traverses the message tree to return the
@@ -171,12 +194,21 @@ def get_field(self, *field_path: Sequence[str]) -> Field:
171194
KeyError: If a repeated field is used in the non-terminal position
172195
in the path.
173196
"""
197+
# If collisions are not explicitly specified, retrieve them
198+
# from this message's address.
199+
# This ensures that calls to `get_field` will return a field with
200+
# the same context, regardless of the number of levels through the
201+
# chain (in order to avoid infinite recursion on circular references,
202+
# we only shallowly bind message references held by fields; this
203+
# binds deeply in the one spot where that might be a problem).
204+
collisions = collisions or self.meta.address.collisions
205+
174206
# Get the first field in the path.
175207
cursor = self.fields[field_path[0]]
176208

177209
# Base case: If this is the last field in the path, return it outright.
178210
if len(field_path) == 1:
179-
return cursor
211+
return cursor.with_context(collisions=collisions)
180212

181213
# Sanity check: If cursor is a repeated field, then raise an exception.
182214
# Repeated fields are only permitted in the terminal position.
@@ -191,12 +223,37 @@ def get_field(self, *field_path: Sequence[str]) -> Field:
191223

192224
# Recursion case: Pass the remainder of the path to the sub-field's
193225
# message.
194-
return cursor.message.get_field(*field_path[1:])
226+
return cursor.message.get_field(*field_path[1:], collisions=collisions)
195227

196-
@property
197-
def ident(self) -> metadata.Address:
198-
"""Return the identifier data to be used in templates."""
199-
return self.meta.address
228+
def with_context(self, *,
229+
collisions: Set[str],
230+
skip_fields: bool = False,
231+
) -> 'MessageType':
232+
"""Return a derivative of this message with the provided context.
233+
234+
This method is used to address naming collisions. The returned
235+
``MessageType`` object aliases module names to avoid naming collisions
236+
in the file being written.
237+
238+
The ``skip_fields`` argument will omit applying the context to the
239+
underlying fields. This provides for an "exit" in the case of circular
240+
references.
241+
"""
242+
return dataclasses.replace(self,
243+
fields=collections.OrderedDict([
244+
(k, v.with_context(collisions=collisions))
245+
for k, v in self.fields.items()
246+
]) if not skip_fields else self.fields,
247+
nested_enums=collections.OrderedDict([
248+
(k, v.with_context(collisions=collisions))
249+
for k, v in self.nested_enums.items()
250+
]),
251+
nested_messages=collections.OrderedDict([(k, v.with_context(
252+
collisions=collisions,
253+
skip_fields=skip_fields,
254+
)) for k, v in self.nested_messages.items()]),
255+
meta=self.meta.with_context(collisions=collisions),
256+
)
200257

201258

202259
@dataclasses.dataclass(frozen=True)
@@ -228,6 +285,17 @@ def ident(self) -> metadata.Address:
228285
"""Return the identifier data to be used in templates."""
229286
return self.meta.address
230287

288+
def with_context(self, *, collisions: Set[str]) -> 'EnumType':
289+
"""Return a derivative of this enum with the provided context.
290+
291+
This method is used to address naming collisions. The returned
292+
``EnumType`` object aliases module names to avoid naming collisions in
293+
the file being written.
294+
"""
295+
return dataclasses.replace(self,
296+
meta=self.meta.with_context(collisions=collisions),
297+
)
298+
231299

232300
@dataclasses.dataclass(frozen=True)
233301
class PythonType:
@@ -275,6 +343,7 @@ def meta(self) -> metadata.Metadata:
275343
name='Operation',
276344
module='operation',
277345
package=('google', 'api_core'),
346+
collisions=self.lro_response.meta.address.collisions,
278347
),
279348
documentation=descriptor_pb2.SourceCodeInfo.Location(
280349
leading_comments='An object representing a long-running '
@@ -298,6 +367,18 @@ def name(self) -> str:
298367
# on google.api_core just to get these strings.
299368
return 'Operation'
300369

370+
def with_context(self, *, collisions: Set[str]) -> 'OperationType':
371+
"""Return a derivative of this operation with the provided context.
372+
373+
This method is used to address naming collisions. The returned
374+
``OperationType`` object aliases module names to avoid naming
375+
collisions in the file being written.
376+
"""
377+
return dataclasses.replace(self,
378+
lro_response=self.lro_response.with_context(collisions=collisions),
379+
lro_metadata=self.lro_metadata.with_context(collisions=collisions),
380+
)
381+
301382

302383
@dataclasses.dataclass(frozen=True)
303384
class Method:
@@ -381,6 +462,19 @@ def signatures(self) -> Tuple[signature_pb2.MethodSignature]:
381462
# Done; return a tuple of signatures.
382463
return MethodSignatures(all=tuple(answer))
383464

465+
def with_context(self, *, collisions: Set[str]) -> 'Method':
466+
"""Return a derivative of this method with the provided context.
467+
468+
This method is used to address naming collisions. The returned
469+
``Method`` object aliases module names to avoid naming collisions
470+
in the file being written.
471+
"""
472+
return dataclasses.replace(self,
473+
input=self.input.with_context(collisions=collisions),
474+
output=self.output.with_context(collisions=collisions),
475+
meta=self.meta.with_context(collisions=collisions),
476+
)
477+
384478

385479
@dataclasses.dataclass(frozen=True)
386480
class MethodSignature:
@@ -519,11 +613,26 @@ def python_modules(self) -> Sequence[imp.Import]:
519613
answer = set()
520614
for method in self.methods.values():
521615
for t in method.ref_types:
522-
answer.add(t.ident.context(self).python_import)
616+
answer.add(t.ident.python_import)
523617
return tuple(sorted(list(answer)))
524618

525619
@property
526620
def has_lro(self) -> bool:
527621
"""Return whether the service has a long-running method."""
528622
return any([getattr(m.output, 'lro_response', None)
529623
for m in self.methods.values()])
624+
625+
def with_context(self, *, collisions: Set[str]) -> 'Service':
626+
"""Return a derivative of this service with the provided context.
627+
628+
This method is used to address naming collisions. The returned
629+
``Service`` object aliases module names to avoid naming collisions
630+
in the file being written.
631+
"""
632+
return dataclasses.replace(self,
633+
methods=collections.OrderedDict([
634+
(k, v.with_context(collisions=collisions))
635+
for k, v in self.methods.items()
636+
]),
637+
meta=self.meta.with_context(collisions=collisions),
638+
)

0 commit comments

Comments
 (0)