Skip to content

Commit 90e9bf2

Browse files
committed
alias import statements to fix issues with name collisions
Signed-off-by: Aidan Jensen <aidandj.github@gmail.com>
1 parent 2283415 commit 90e9bf2

File tree

264 files changed

+52452
-52321
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

264 files changed

+52452
-52321
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ jobs:
108108
with:
109109
# By default, shellcheck tries to make sure any external files referenced actually exist
110110
shellcheck_flags: -e SC1091
111+
exclude: |
112+
*/third_party/*
111113
112114
sanity_check_windows:
113115
name: Sanity Check Windows Executable

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
- Add `_WhichOneofArgType_<oneof_name>` and `_WhichOneofReturnType_<oneof_name>` type aliases
1414
- Use `__new__` overloads for async stubs instead of `TypeVar` based `__init__` overloads.
1515
- https://github.com/nipunn1313/mypy-protobuf/issues/707
16+
- Use `builtins.property` to handle conflicts with fields named `property`
17+
- Mangle all non provided message type imports, this prevents conflicts with field names like `collections`, `builtins`, etc.
18+
- Do not mangle message imports, as that would be a breaking change.
1619

1720
## 3.7.0
1821

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ protoc --python_out=output/location --mypy_grpc_out=generate_concrete_servicer_s
350350

351351
### `sync_only/async_only`
352352

353-
By default, generated GRPC stubs are compatible with both sync and async variants. If you only
353+
By default, generated GRPC stubs are compatible with both sync and async variants. If you only
354354
want sync or async GRPC stubs, use this option:
355355

356356
```
@@ -445,6 +445,26 @@ protoc --python_out=output/location --mypy_out=output/location
445445
mypy --target-version=2.7 {files}
446446
```
447447

448+
## 3rd Party Tests
449+
450+
3rd Party proto files can be added to `third_party` using git subtree. These can be used for large scale testing of changes.
451+
452+
### Adding
453+
454+
```bash
455+
git subtree add --prefix=third_party/googleapis https://github.com/googleapis/googleapis.git master --squash
456+
```
457+
458+
### Updating
459+
460+
```bash
461+
git subtree pull --prefix=third_party/googleapis https://github.com/googleapis/googleapis.git master --squash
462+
```
463+
464+
## 4.0 Breaking change proposals
465+
466+
* De-dot all import statements
467+
448468
## Contributing
449469

450470
Contributions to the implementation are welcome. Please run tests using `./run_test.sh`.

mypy_protobuf/extensions_pb2.pyi

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,53 @@
33
isort:skip_file
44
"""
55

6-
import builtins
7-
import google.protobuf.descriptor
6+
import builtins as _builtins
7+
import google.protobuf.descriptor as _google_protobuf_descriptor
88
import google.protobuf.descriptor_pb2
9-
import google.protobuf.internal.extension_dict
10-
import google.protobuf.message
9+
import google.protobuf.internal.extension_dict as _google_protobuf_internal_extension_dict
10+
import google.protobuf.message as _google_protobuf_message
1111
import sys
12-
import typing
12+
import typing as _typing
1313

1414
if sys.version_info >= (3, 10):
15-
import typing as typing_extensions
15+
import typing as _typing_extensions
1616
else:
17-
import typing_extensions
17+
import typing_extensions as _typing_extensions
1818

19-
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
19+
DESCRIPTOR: _google_protobuf_descriptor.FileDescriptor
2020

21-
@typing.final
22-
class FieldOptions(google.protobuf.message.Message):
23-
DESCRIPTOR: google.protobuf.descriptor.Descriptor
21+
@_typing.final
22+
class FieldOptions(_google_protobuf_message.Message):
23+
DESCRIPTOR: _google_protobuf_descriptor.Descriptor
2424

25-
CASTTYPE_FIELD_NUMBER: builtins.int
26-
KEYTYPE_FIELD_NUMBER: builtins.int
27-
VALUETYPE_FIELD_NUMBER: builtins.int
28-
casttype: builtins.str
25+
CASTTYPE_FIELD_NUMBER: _builtins.int
26+
KEYTYPE_FIELD_NUMBER: _builtins.int
27+
VALUETYPE_FIELD_NUMBER: _builtins.int
28+
casttype: _builtins.str
2929
"""Tells mypy-protobuf to use a specific newtype rather than the normal type for this field."""
30-
keytype: builtins.str
30+
keytype: _builtins.str
3131
"""Tells mypy-protobuf to use a specific type for keys; only makes sense on map fields"""
32-
valuetype: builtins.str
32+
valuetype: _builtins.str
3333
"""Tells mypy-protobuf to use a specific type for values; only makes sense on map fields"""
3434
def __init__(
3535
self,
3636
*,
37-
casttype: builtins.str = ...,
38-
keytype: builtins.str = ...,
39-
valuetype: builtins.str = ...,
37+
casttype: _builtins.str = ...,
38+
keytype: _builtins.str = ...,
39+
valuetype: _builtins.str = ...,
4040
) -> None: ...
41-
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]
41+
_ClearFieldArgType: _typing_extensions.TypeAlias = _typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"] # noqa: Y015
4242
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
4343

44-
Global___FieldOptions: typing_extensions.TypeAlias = FieldOptions
44+
Global___FieldOptions: _typing_extensions.TypeAlias = FieldOptions # noqa: Y015
4545

46-
OPTIONS_FIELD_NUMBER: builtins.int
47-
CASTTYPE_FIELD_NUMBER: builtins.int
48-
KEYTYPE_FIELD_NUMBER: builtins.int
49-
VALUETYPE_FIELD_NUMBER: builtins.int
50-
options: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, Global___FieldOptions]
46+
OPTIONS_FIELD_NUMBER: _builtins.int
47+
CASTTYPE_FIELD_NUMBER: _builtins.int
48+
KEYTYPE_FIELD_NUMBER: _builtins.int
49+
VALUETYPE_FIELD_NUMBER: _builtins.int
50+
options: _google_protobuf_internal_extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, Global___FieldOptions]
5151
"""Custom field options from mypy-protobuf"""
52-
casttype: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, builtins.str]
52+
casttype: _google_protobuf_internal_extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, _builtins.str]
5353
"""Legacy fields. Prefer to use ones within `options` instead."""
54-
keytype: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, builtins.str]
55-
valuetype: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, builtins.str]
54+
keytype: _google_protobuf_internal_extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, _builtins.str]
55+
valuetype: _google_protobuf_internal_extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, _builtins.str]

mypy_protobuf/main.py

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def __init__(
191191
self.indent = ""
192192

193193
# Set of {x}, where {x} corresponds to to `import {x}`
194-
self.imports: Set[str] = set()
194+
self.imports: Dict[str, bool] = {}
195195
# dictionary of x->(y,z) for `from {x} import {y} as {z}`
196196
# if {z} is None, then it shortens to `from {x} import {y}`
197197
self.from_imports: Dict[str, Set[Tuple[str, str | None]]] = defaultdict(set)
@@ -201,29 +201,46 @@ def __init__(
201201
# Comments
202202
self.source_code_info_by_scl = {tuple(location.path): location for location in fd.source_code_info.location}
203203

204-
def _import(self, path: str, name: str) -> str:
204+
@property
205+
def _deprecated_name(self) -> str:
206+
return "_deprecated"
207+
208+
@property
209+
def _typing_extensions_name(self) -> str:
210+
return "_typing_extensions"
211+
212+
def _import_alias(self, path: str) -> str:
213+
"""import as prefixed with underscore to avoid conflicts with message/enum names"""
214+
return f"_{path.replace('.', '_')}"
215+
216+
def _import(self, path: str, name: str, *, alias: bool = True) -> str:
205217
"""Imports a stdlib path and returns a handle to it
206218
eg. self._import("typing", "Literal") -> "Literal"
219+
220+
If alias is true, then it will prefix the import with an underscore to prevent conflicts with builtin names
207221
"""
208222
if path == "typing_extensions":
209-
stabilization = {"TypeAlias": (3, 10), "TypeVar": (3, 13), "type_check_only": (3, 12)}
223+
stabilization = {"TypeAlias": (3, 10), "TypeVar": (3, 13), "type_check_only": (3, 12), "Self": (3, 11)}
210224
assert name in stabilization
211225
if not self.typing_extensions_min or self.typing_extensions_min < stabilization[name]:
212226
self.typing_extensions_min = stabilization[name]
213-
return "typing_extensions." + name
227+
return self._typing_extensions_name + "." + name
214228

215229
if path == "warnings" and name == "deprecated":
216230
if not self.deprecated_min or self.deprecated_min < (3, 11):
217231
self.deprecated_min = (3, 13)
218-
return name
232+
return self._deprecated_name
219233

220234
imp = path.replace("/", ".")
221235
if self.readable_stubs:
222236
self.from_imports[imp].add((name, None))
223237
return name
224238
else:
225-
self.imports.add(imp)
226-
return imp + "." + name
239+
self.imports[imp] = alias
240+
return (self._import_alias(imp) if alias else imp) + "." + name
241+
242+
def _property(self) -> str:
243+
return f"@{self._import('builtins', 'property')}"
227244

228245
def _import_message(self, name: str) -> str:
229246
"""Import a referenced message and return a handle"""
@@ -252,7 +269,7 @@ def _import_message(self, name: str) -> str:
252269
# Not in file. Must import
253270
# Python generated code ignores proto packages, so the only relevant factor is
254271
# whether it is in the file or not.
255-
import_name = self._import(message_fd.name[:-6].replace("-", "_") + "_pb2", split[0])
272+
import_name = self._import(message_fd.name[:-6].replace("-", "_") + "_pb2", split[0], alias=False)
256273

257274
remains = ".".join(split[1:])
258275
if not remains:
@@ -427,7 +444,7 @@ def write_enums(
427444
self._builtin("int"),
428445
)
429446
# Alias to the classic shorter definition "V"
430-
wl("V: {} = ValueType", self._import("typing_extensions", "TypeAlias"))
447+
wl("V: {} = ValueType # noqa: Y015", self._import("typing_extensions", "TypeAlias"))
431448
wl("")
432449
wl(
433450
"class {}({}[{}], {}):",
@@ -467,7 +484,7 @@ def write_enums(
467484
scl + [d.EnumDescriptorProto.VALUE_FIELD_NUMBER],
468485
)
469486
if prefix == "" and not self.readable_stubs:
470-
wl(f"{_mangle_global_identifier(class_name)}: {self._import('typing_extensions', 'TypeAlias')} = {class_name}")
487+
wl(f"{_mangle_global_identifier(class_name)}: {self._import('typing_extensions', 'TypeAlias')} = {class_name} # noqa: Y015")
471488
wl("")
472489

473490
def write_messages(
@@ -544,7 +561,7 @@ def write_messages(
544561
if not (is_scalar(field) and field.label != d.FieldDescriptorProto.LABEL_REPEATED):
545562
# r/o Getters for non-scalar fields and scalar-repeated fields
546563
scl_field = scl + [d.DescriptorProto.FIELD_FIELD_NUMBER, idx]
547-
wl("@property")
564+
wl(self._property())
548565
body = " ..." if not self._has_comments(scl_field) else ""
549566
wl(f"def {field.name}(self) -> {field_type}:{body}")
550567
if self._has_comments(scl_field):
@@ -580,7 +597,7 @@ def write_messages(
580597

581598
if prefix == "" and not self.readable_stubs:
582599
wl("")
583-
wl(f"{_mangle_global_identifier(class_name)}: {self._import('typing_extensions', 'TypeAlias')} = {class_name}")
600+
wl(f"{_mangle_global_identifier(class_name)}: {self._import('typing_extensions', 'TypeAlias')} = {class_name} # noqa: Y015")
584601
wl("")
585602

586603
def write_stringly_typed_fields(self, desc: d.DescriptorProto) -> None:
@@ -604,29 +621,29 @@ def write_stringly_typed_fields(self, desc: d.DescriptorProto) -> None:
604621
return
605622

606623
if hf_fields:
607-
wl("_HasFieldArgType: {} = {}[{}]", self._import("typing_extensions", "TypeAlias"), self._import("typing", "Literal"), hf_fields_text)
624+
wl("_HasFieldArgType: {} = {}[{}] # noqa: Y015", self._import("typing_extensions", "TypeAlias"), self._import("typing", "Literal"), hf_fields_text)
608625
wl(
609626
"def HasField(self, field_name: _HasFieldArgType) -> {}: ...",
610627
self._builtin("bool"),
611628
)
612629
if cf_fields:
613-
wl("_ClearFieldArgType: {} = {}[{}]", self._import("typing_extensions", "TypeAlias"), self._import("typing", "Literal"), cf_fields_text)
630+
wl("_ClearFieldArgType: {} = {}[{}] # noqa: Y015", self._import("typing_extensions", "TypeAlias"), self._import("typing", "Literal"), cf_fields_text)
614631
wl(
615632
"def ClearField(self, field_name: _ClearFieldArgType) -> None: ...",
616633
)
617634

618635
# Write type aliases first so overloads are not interrupted
619636
for wo_field, members in sorted(wo_fields.items()):
620637
wl(
621-
"_WhichOneofReturnType_{}: {} = {}[{}]",
638+
"_WhichOneofReturnType_{}: {} = {}[{}] # noqa: Y015",
622639
wo_field,
623640
self._import("typing_extensions", "TypeAlias"),
624641
self._import("typing", "Literal"),
625642
# Returns `str`
626643
", ".join(f'"{m}"' for m in members),
627644
)
628645
wl(
629-
"_WhichOneofArgType_{}: {} = {}[{}]",
646+
"_WhichOneofArgType_{}: {} = {}[{}] # noqa: Y015",
630647
wo_field,
631648
self._import("typing_extensions", "TypeAlias"),
632649
self._import("typing", "Literal"),
@@ -751,7 +768,7 @@ def _import_casttype(self, casttype: str) -> str:
751768
split = casttype.split(".")
752769
assert len(split) == 2, "mypy_protobuf.[casttype,keytype,valuetype] is expected to be of format path/to/file.TypeInFile"
753770
pkg = split[0].replace("/", ".")
754-
return self._import(pkg, split[1])
771+
return self._import(pkg, split[1], alias=False)
755772

756773
def _map_key_value_types(
757774
self,
@@ -983,7 +1000,7 @@ def write_grpc_services(
9831000
wl(
9841001
"def __new__(cls, channel: {}) -> {}: ...",
9851002
self._import("grpc", "Channel"),
986-
class_name,
1003+
self._import("typing_extensions", "Self"),
9871004
)
9881005

9891006
# Write async overload
@@ -1168,21 +1185,25 @@ def write(self) -> str:
11681185
self.from_imports[reexport_imp].update((n, n) for n in names)
11691186

11701187
if self.typing_extensions_min or self.deprecated_min:
1171-
self.imports.add("sys")
1172-
for pkg in sorted(self.imports):
1173-
self._write_line(f"import {pkg}")
1188+
# Special case for `sys` as it is needed for version checks
1189+
self.imports["sys"] = False
1190+
for pkg, dedot in sorted(self.imports.items()):
1191+
if dedot:
1192+
self._write_line(f"import {pkg} as {self._import_alias(pkg)}")
1193+
else:
1194+
self._write_line(f"import {pkg}")
11741195
if self.typing_extensions_min:
11751196
self._write_line("")
11761197
self._write_line(f"if sys.version_info >= {self.typing_extensions_min}:")
1177-
self._write_line(" import typing as typing_extensions")
1198+
self._write_line(f" import typing as {self._typing_extensions_name}")
11781199
self._write_line("else:")
1179-
self._write_line(" import typing_extensions")
1200+
self._write_line(f" import typing_extensions as {self._typing_extensions_name}")
11801201
if self.deprecated_min:
11811202
self._write_line("")
11821203
self._write_line(f"if sys.version_info >= {self.deprecated_min}:")
1183-
self._write_line(" from warnings import deprecated")
1204+
self._write_line(f" from warnings import deprecated as {self._deprecated_name}")
11841205
self._write_line("else:")
1185-
self._write_line(" from typing_extensions import deprecated")
1206+
self._write_line(f" from typing_extensions import deprecated as {self._deprecated_name}")
11861207

11871208
for pkg, items in sorted(self.from_imports.items()):
11881209
self._write_line(f"from {pkg} import (")

proto/testproto/test.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ message Simple1 {
7474
(mypy_protobuf.valuetype)="test/test_generated_mypy.Email"
7575
];
7676

77+
optional string property = 24;
78+
optional string collections = 25;
79+
// This only works when all imports are de-dotted. Consider for a breaking release
80+
// optional string testproto = 26;
81+
7782
extensions 1000 to max;
7883
}
7984

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ exclude = [
3333
"**/*_pb2.py",
3434
"**/*_pb2_grpc.py",
3535
"test/test_concrete.py",
36+
"test/**/*unittest*_pb2.pyi"
3637
]
3738

3839
executionEnvironments = [

run_test.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ MYPY_PROTOBUF_VENV=venv_$PY_VER_MYPY_PROTOBUF
149149
echo -e "${RED}Some .pyi files did not match. Please commit those files${NC}"
150150
exit 1
151151
fi
152+
153+
# Generate 3rd party protos
154+
mkdir -p third_party/out/generated_googleapis
155+
# Known conflict with extensions proto in googleapis - skip that one
156+
find third_party/googleapis -name "*.proto" \
157+
! -path "third_party/googleapis/google/cloud/compute/v1/compute.proto" \
158+
! -path "third_party/googleapis/google/cloud/compute/v1beta/compute.proto" \
159+
! -path "third_party/googleapis/google/cloud/compute/v1small/compute_small.proto" \
160+
-print0 | xargs -0 "$PROTOC" --proto_path=third_party/googleapis --mypy_out=third_party/out/generated_googleapis --mypy_grpc_out=third_party/out/generated_googleapis --python_out=third_party/out/generated_googleapis
152161
)
153162

154163
ERROR_FILE=$(mktemp)
@@ -181,6 +190,14 @@ for PY_VER in $PY_VER_UNIT_TESTS; do
181190
ASYNC_ONLY_MODULES=( -m test.async_only.test_async_only )
182191
MYPYPATH=$MYPYPATH:test/generated_async_only mypy ${CUSTOM_TYPESHED_DIR_ARG:+"$CUSTOM_TYPESHED_DIR_ARG"} --report-deprecated-as-note --python-executable="$UNIT_TESTS_VENV"/bin/python --python-version="$PY_VER_MYPY_TARGET" "${ASYNC_ONLY_MODULES[@]}"
183192

193+
# Run google/protobuf mypy
194+
GOOGLE_PROTOBUF=( test/generated/google/protobuf )
195+
MYPYPATH=$MYPYPATH:test/generated PYTHONPATH=test/generated mypy --explicit-package-bases ${CUSTOM_TYPESHED_DIR_ARG:+"$CUSTOM_TYPESHED_DIR_ARG"} --report-deprecated-as-note --python-executable="$UNIT_TESTS_VENV"/bin/python --python-version="$PY_VER_MYPY_TARGET" "${GOOGLE_PROTOBUF[@]}"
196+
197+
# Run googleapis mypy
198+
GOOGLEAPIS=( third_party/out/generated_googleapis )
199+
MYPYPATH=$MYPYPATH:third_party/out/generated_googleapis mypy --explicit-package-bases ${CUSTOM_TYPESHED_DIR_ARG:+"$CUSTOM_TYPESHED_DIR_ARG"} --report-deprecated-as-note --python-executable="$UNIT_TESTS_VENV"/bin/python --python-version="$PY_VER_MYPY_TARGET" "${GOOGLEAPIS[@]}"
200+
184201
export MYPYPATH=$MYPYPATH:test/generated
185202

186203
# Run mypy
@@ -233,6 +250,10 @@ for PY_VER in $PY_VER_UNIT_TESTS; do
233250
)
234251
done
235252

253+
254+
# Clean up googleapis
255+
rm -rf third_party/out/generated_googleapis
256+
236257
# Report all errors at the end
237258
if [ -s "$ERROR_FILE" ]; then
238259
echo -e "\n${RED}===============================================${NC}"

0 commit comments

Comments
 (0)