diff --git a/README.md b/README.md index 836968d..89f2edd 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ You can use it like so (enable async in the interactive shell first): EchoResponse(values=["hello", "hello"]) >>> async for response in service.echo_stream(value="hello", extra_times=1) - print(response) + print(response) EchoStreamResponse(value="hello") EchoStreamResponse(value="hello") @@ -296,13 +296,13 @@ $ pipenv run tests - [ ] Any support - [x] Enum strings - [ ] Well known types support (timestamp, duration, wrappers) - - [ ] Support different casing (orig vs. camel vs. others?) + - [x] Support different casing (orig vs. camel vs. others?) - [ ] Async service stubs - [x] Unary-unary - [x] Server streaming response - [ ] Client streaming request - [x] Renaming messages and fields to conform to Python name standards -- [ ] Renaming clashes with language keywords and standard library top-level packages +- [x] Renaming clashes with language keywords - [x] Python package - [x] Automate running tests - [ ] Cleanup! diff --git a/betterproto/__init__.py b/betterproto/__init__.py index a80f6d8..c698f95 100644 --- a/betterproto/__init__.py +++ b/betterproto/__init__.py @@ -26,6 +26,8 @@ import grpclib.client import grpclib.const import stringcase +from .casing import safe_snake_case + # Proto 3 data types TYPE_ENUM = "enum" TYPE_BOOL = "bool" @@ -642,7 +644,7 @@ class Message(ABC): for field in dataclasses.fields(self): meta = FieldMetadata.get(field) v = getattr(self, field.name) - cased_name = casing(field.name) + cased_name = casing(field.name).rstrip("_") if meta.proto_type == "message": if isinstance(v, list): # Convert each item. @@ -686,7 +688,7 @@ class Message(ABC): self._serialized_on_wire = True fields_by_name = {f.name: f for f in dataclasses.fields(self)} for key in value: - snake_cased = stringcase.snakecase(key) + snake_cased = safe_snake_case(key) if snake_cased in fields_by_name: field = fields_by_name[snake_cased] meta = FieldMetadata.get(field) diff --git a/betterproto/casing.py b/betterproto/casing.py new file mode 100644 index 0000000..67ca9a2 --- /dev/null +++ b/betterproto/casing.py @@ -0,0 +1,41 @@ +import stringcase + + +def safe_snake_case(value: str) -> str: + """Snake case a value taking into account Python keywords.""" + value = stringcase.snakecase(value) + if value in [ + "and", + "as", + "assert", + "break", + "class", + "continue", + "def", + "del", + "elif", + "else", + "except", + "finally", + "for", + "from", + "global", + "if", + "import", + "in", + "is", + "lambda", + "nonlocal", + "not", + "or", + "pass", + "raise", + "return", + "try", + "while", + "with", + "yield", + ]: + # https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles + value += "_" + return value diff --git a/betterproto/plugin.py b/betterproto/plugin.py index a227947..2865138 100755 --- a/betterproto/plugin.py +++ b/betterproto/plugin.py @@ -27,6 +27,8 @@ from google.protobuf.descriptor_pb2 import ( ServiceDescriptorProto, ) +from betterproto.casing import safe_snake_case + def get_ref_type(package: str, imports: set, type_name: str) -> str: """ @@ -255,7 +257,7 @@ def generate_code(request, response): data["properties"].append( { "name": f.name, - "py_name": stringcase.snakecase(f.name), + "py_name": safe_snake_case(f.name), "number": f.number, "comment": get_comment(proto_file, path + [2, i]), "proto_type": int(f.type), diff --git a/betterproto/tests/keywords.json b/betterproto/tests/keywords.json new file mode 100644 index 0000000..c4f7b0c --- /dev/null +++ b/betterproto/tests/keywords.json @@ -0,0 +1,5 @@ +{ + "for": 1, + "with": 2, + "as": 3 +} diff --git a/betterproto/tests/keywords.proto b/betterproto/tests/keywords.proto new file mode 100644 index 0000000..25b87f1 --- /dev/null +++ b/betterproto/tests/keywords.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +message Test { + int32 for = 1; + int32 with = 2; + int32 as = 3; +}