Skip to content

Commit 07f7b53

Browse files
Naming collision resolution. (#51)
This adds a system for resolving naming conflicts between imported modules and field or method names.
1 parent 467fe26 commit 07f7b53

File tree

17 files changed

+497
-131
lines changed

17 files changed

+497
-131
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
8+
E128, E131

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

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
import collections
2121
import dataclasses
2222
import sys
23-
from typing import Callable, List, Mapping, Sequence, Tuple
23+
from itertools import chain
24+
from typing import Callable, List, Mapping, Sequence, Set, Tuple
2425

2526
from google.api import annotations_pb2
2627
from google.protobuf import descriptor_pb2
@@ -81,6 +82,32 @@ def module_name(self) -> str:
8182
"""
8283
return to_snake_case(self.name.split('/')[-1][:-len('.proto')])
8384

85+
@cached_property
86+
def names(self) -> Set[str]:
87+
"""Return a set of names used by this proto.
88+
89+
This is used for detecting naming collisions in the module names
90+
used for imports.
91+
"""
92+
# Add names of all enums, messages, and fields.
93+
answer = {e.name for e in self.enums.values()}
94+
for message in self.messages.values():
95+
answer = answer.union({f.name for f in message.fields.values()})
96+
answer.add(message.name)
97+
98+
# Identify any import module names where the same module name is used
99+
# from distinct packages.
100+
modules = {}
101+
for t in chain(*[m.field_types for m in self.messages.values()]):
102+
modules.setdefault(t.ident.module, set())
103+
modules[t.ident.module].add(t.ident.package)
104+
for module_name, packages in modules.items():
105+
if len(packages) > 1:
106+
answer.add(module_name)
107+
108+
# Return the set of collision names.
109+
return frozenset(answer)
110+
84111
@cached_property
85112
def python_modules(self) -> Sequence[Tuple[str, str]]:
86113
"""Return a sequence of Python modules, for import.
@@ -94,19 +121,14 @@ def python_modules(self) -> Sequence[Tuple[str, str]]:
94121
of statement.
95122
"""
96123
answer = set()
97-
for message in self.messages.values():
98-
for field in message.fields.values():
99-
# We only need to add imports for fields that
100-
# are messages or enums.
101-
if not field.message and not field.enum:
102-
continue
103-
104-
# Add the appropriate Python import for the field.
105-
answer.add(field.type.ident.python_import)
106-
107-
# We may have gotten an import for this proto.
108-
# Obviously no Python module may import itself; get rid of that.
109-
answer = answer.difference({self.meta.address.python_import})
124+
self_reference = self.meta.address.context(self).python_import
125+
for t in chain(*[m.field_types for m in self.messages.values()]):
126+
# Add the appropriate Python import for the field.
127+
# Sanity check: We do make sure that we are not trying to have
128+
# a module import itself.
129+
imp = t.ident.context(self).python_import
130+
if imp != self_reference:
131+
answer.add(imp)
110132

111133
# Done; return the sorted sequence.
112134
return tuple(sorted(list(answer)))
@@ -130,6 +152,17 @@ def top(self) -> 'Proto':
130152
meta=self.meta,
131153
)
132154

155+
def disambiguate(self, string: str) -> str:
156+
"""Return a disambiguated string for the context of this proto.
157+
158+
This is used for avoiding naming collisions. Generally, this method
159+
returns the same string, but it returns a modified version if
160+
it will cause a naming collision with messages or fields in this proto.
161+
"""
162+
if string in self.names:
163+
return self.disambiguate(f'_{string}')
164+
return string
165+
133166

134167
@dataclasses.dataclass(frozen=True)
135168
class API:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2018 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import dataclasses
16+
from typing import Tuple
17+
18+
19+
@dataclasses.dataclass(frozen=True, order=True)
20+
class Import:
21+
package: Tuple[str]
22+
module: str
23+
alias: str = ''
24+
25+
def __eq__(self, other) -> bool:
26+
return self.package == other.package and self.module == other.module
27+
28+
def __str__(self) -> str:
29+
answer = f'import {self.module}'
30+
if self.package:
31+
answer = f"from {'.'.join(self.package)} {answer}"
32+
if self.alias:
33+
answer += f' as {self.alias}'
34+
return answer

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

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
"""
2828

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

3232
from google.protobuf import descriptor_pb2
3333

34+
from gapic.schema import imp
3435
from gapic.schema import naming
36+
from gapic.utils import cached_property
3537

3638

3739
@dataclasses.dataclass(frozen=True)
@@ -44,6 +46,11 @@ class Address:
4446
api_naming: naming.Naming = dataclasses.field(
4547
default_factory=naming.Naming,
4648
)
49+
collisions: Set[str] = dataclasses.field(default_factory=frozenset)
50+
51+
def __eq__(self, other) -> bool:
52+
return all([getattr(self, i) == getattr(other, i) for i
53+
in ('name', 'module', 'module_path', 'package', 'parent')])
4754

4855
def __str__(self) -> str:
4956
"""Return the Python identifier for this type.
@@ -52,9 +59,36 @@ def __str__(self) -> str:
5259
members from modules, this is consistently `module.Name`.
5360
"""
5461
if self.module:
55-
return '.'.join((self.module,) + self.parent + (self.name,))
62+
# If collisions are registered and conflict with our module,
63+
# use the module alias instead.
64+
module_name = self.module
65+
if self.module_alias:
66+
module_name = self.module_alias
67+
68+
# Return the dot-separated Python identifier.
69+
return '.'.join((module_name,) + self.parent + (self.name,))
70+
71+
# Return the Python identifier for this module-less identifier.
5672
return '.'.join(self.parent + (self.name,))
5773

74+
@property
75+
def module_alias(self) -> str:
76+
"""Return an appropriate module alias if necessary.
77+
78+
If the module name is not a collision, return empty string.
79+
80+
This method provides a mechanism for resolving naming conflicts,
81+
while still providing names that are fundamentally readable
82+
to users (albeit looking auto-generated).
83+
"""
84+
if self.module in self.collisions:
85+
return '_'.join((
86+
''.join([i[0] for i in self.package
87+
if i != self.api_naming.version]),
88+
self.module,
89+
))
90+
return ''
91+
5892
@property
5993
def proto(self) -> str:
6094
"""Return the proto selector for this type."""
@@ -65,27 +99,36 @@ def proto_package(self) -> str:
6599
"""Return the proto package for this type."""
66100
return '.'.join(self.package)
67101

68-
@property
69-
def python_import(self) -> Tuple[Sequence[str], str]:
102+
@cached_property
103+
def python_import(self) -> imp.Import:
70104
"""Return the Python import for this type."""
71105
# If there is no naming object, this is a special case for operation.
72106
# FIXME(#34): OperationType does not work well. Fix or expunge it.
73107
if not self.api_naming:
74-
return ('.'.join(self.package), self.module)
108+
return imp.Import(
109+
package=self.package,
110+
module=self.module,
111+
alias=self.module_alias,
112+
)
75113

76114
# If this is part of the proto package that we are generating,
77115
# rewrite the package to our structure.
78116
if self.proto_package.startswith(self.api_naming.proto_package):
79-
return (
80-
'.'.join(self.api_naming.module_namespace + (
117+
return imp.Import(
118+
package=self.api_naming.module_namespace + (
81119
self.api_naming.versioned_module_name,
82120
'types',
83-
)),
84-
self.module,
121+
),
122+
module=self.module,
123+
alias=self.module_alias,
85124
)
86125

87126
# Return the standard import.
88-
return ('.'.join(self.package), f'{self.module}_pb2')
127+
return imp.Import(
128+
package=self.package,
129+
module=f'{self.module}_pb2',
130+
alias=self.module_alias if self.module_alias else self.module,
131+
)
89132

90133
@property
91134
def sphinx(self) -> str:
@@ -104,15 +147,21 @@ def child(self, child_name: str, path: Tuple[int]) -> 'Address':
104147
Returns:
105148
~.Address: The new address object.
106149
"""
107-
return type(self)(
108-
api_naming=self.api_naming,
109-
module=self.module,
150+
return dataclasses.replace(self,
110151
module_path=self.module_path + path,
111152
name=child_name,
112-
package=self.package,
113153
parent=self.parent + (self.name,) if self.name else self.parent,
114154
)
115155

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+
116165
def rel(self, address: 'Address') -> str:
117166
"""Return an identifier for this type, relative to the given address.
118167
@@ -161,7 +210,7 @@ def rel(self, address: 'Address') -> str:
161210
return '.'.join(self.parent + (self.name,))
162211

163212
# Return the usual `module.Name`.
164-
return f'_.{str(self)}'
213+
return str(self)
165214

166215
def resolve(self, selector: str) -> str:
167216
"""Resolve a potentially-relative protobuf selector.
@@ -226,3 +275,7 @@ def sphinx(self) -> str:
226275
if self.repeated:
227276
return f'Sequence[{self.ident.sphinx}]'
228277
return self.ident.sphinx
278+
279+
def context(self, arg) -> 'FieldIdentifier':
280+
"""Return self. Provided for compatibility with Address."""
281+
return self

0 commit comments

Comments
 (0)