Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9fd1c05
Create unit tests for importing
boukeversteegh Jun 4, 2020
e5e61c8
Implement some import scenarios
boukeversteegh Jun 4, 2020
57523a9
Implement importing unrelated package
boukeversteegh Jun 4, 2020
d7ba27d
fix all broken imports
boukeversteegh Jun 5, 2020
d8abb85
Update tests to reflect new generated package structure
boukeversteegh Jun 7, 2020
f7c2fd1
Support nested messages, fix casing. Support test-cases in packages.
boukeversteegh Jun 7, 2020
fdf3b2e
Compile proto files based on package structure
boukeversteegh Jun 7, 2020
c00e2ae
Break up importing logic in methods
boukeversteegh Jun 8, 2020
7c8d47d
Add test cases for cousin imports that break due to aliases starting …
boukeversteegh Jun 8, 2020
3105e95
Fixes issue where importing cousin where path has a package with the …
boukeversteegh Jun 9, 2020
8567892
Simplify logic for generating package init files
boukeversteegh Jun 9, 2020
76db2f1
Add import aliases to ancestor imports
boukeversteegh Jun 9, 2020
1a95a79
Ensure uniquely generated import aliases are not name mangled (python…
boukeversteegh Jun 9, 2020
fb54917
Detect entry-point of tests automatically
boukeversteegh Jun 10, 2020
34c34bd
Add failing test for importing a message from package that looks like…
boukeversteegh Jun 10, 2020
65c1f36
Update readme with new output structure and fix example inconsistencies
boukeversteegh Jun 10, 2020
5d2f3a2
Remove fixed test from xfail list #11
boukeversteegh Jun 10, 2020
3ca75da
Remove dependency on stringcase, apply black
boukeversteegh Jun 10, 2020
83e13aa
Fix method name
boukeversteegh Jun 10, 2020
c88edfd
Support running plugin without installing betterproto
boukeversteegh Jun 12, 2020
d9fa6d2
Fixes issue where generated Google Protobuf messages imported from be…
boukeversteegh Jun 12, 2020
32c8e77
Recompile Google Protobuf files
boukeversteegh Jun 12, 2020
2c360a5
Readability for generating init_files
boukeversteegh Jun 14, 2020
87f4b34
Revert "Support running plugin without installing betterproto"
boukeversteegh Jun 14, 2020
63f5191
Shorten list selectors
boukeversteegh Jun 14, 2020
e2d672a
Fix terminology, improve docstrings and add missing asserts to tests
boukeversteegh Jun 14, 2020
fdbe020
find_module docstring and search for init files instead of directories
boukeversteegh Jun 14, 2020
52eea5c
Added missing tests for casing
boukeversteegh Jun 14, 2020
e3135ce
Add parameter for non-strict cased output that preserves delimiter count
boukeversteegh Jul 1, 2020
81711d2
Avoid naming conflicts when importing multiple types with the same na…
boukeversteegh Jul 1, 2020
f4ebcb0
Merge remote-tracking branch 'daniel/master' into fix/imports
boukeversteegh Jul 1, 2020
0d9387a
Remove stringcase dependency
boukeversteegh Jul 1, 2020
af71154
Expose betterproto.ServiceStub
boukeversteegh Jul 1, 2020
d21cd6e
black
boukeversteegh Jul 1, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,46 +68,47 @@ message Greeting {
You can run the following:

```sh
protoc -I . --python_betterproto_out=. example.proto
mkdir lib
protoc -I . --python_betterproto_out=lib example.proto
```

This will generate `hello.py` which looks like:
This will generate `lib/hello/__init__.py` which looks like:

```py
```python
# Generated by the protocol buffer compiler. DO NOT EDIT!
# sources: hello.proto
# sources: example.proto
# plugin: python-betterproto
from dataclasses import dataclass

import betterproto


@dataclass
class Hello(betterproto.Message):
class Greeting(betterproto.Message):
"""Greeting represents a message you can tell a user."""

message: str = betterproto.string_field(1)
```

Now you can use it!

```py
>>> from hello import Hello
>>> test = Hello()
```python
>>> from lib.hello import Greeting
>>> test = Greeting()
>>> test
Hello(message='')
Greeting(message='')

>>> test.message = "Hey!"
>>> test
Hello(message="Hey!")
Greeting(message="Hey!")

>>> serialized = bytes(test)
>>> serialized
b'\n\x04Hey!'

>>> another = Hello().parse(serialized)
>>> another = Greeting().parse(serialized)
>>> another
Hello(message="Hey!")
Greeting(message="Hey!")

>>> another.to_dict()
{"message": "Hey!"}
Expand Down Expand Up @@ -315,7 +316,7 @@ To benefit from the collection of standard development tasks ensure you have mak

This project enforces [black](https://github.com/psf/black) python code formatting.

Before commiting changes run:
Before committing changes run:

```sh
make format
Expand All @@ -336,7 +337,7 @@ Adding a standard test case is easy.

- Create a new directory `betterproto/tests/inputs/<name>`
- add `<name>.proto` with a message called `Test`
- add `<name>.json` with some test data
- add `<name>.json` with some test data (optional)

It will be picked up automatically when you run the tests.

Expand Down
15 changes: 5 additions & 10 deletions betterproto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,22 @@
from abc import ABC
from base64 import b64decode, b64encode
from datetime import datetime, timedelta, timezone
import stringcase
from typing import (
Any,
AsyncGenerator,
Callable,
Collection,
Dict,
Generator,
Iterator,
List,
Mapping,
Optional,
Set,
SupportsBytes,
Tuple,
Type,
Union,
get_type_hints,
)
from ._types import ST, T
from .casing import safe_snake_case

from ._types import T
from .casing import camel_case, safe_snake_case, safe_snake_case, snake_case
from .grpc.grpclib_client import ServiceStub

if not (sys.version_info.major == 3 and sys.version_info.minor >= 7):
Expand Down Expand Up @@ -124,8 +119,8 @@ def datetime_default_gen():
class Casing(enum.Enum):
"""Casing constants for serialization."""

CAMEL = stringcase.camelcase
SNAKE = stringcase.snakecase
CAMEL = camel_case
SNAKE = snake_case


class _PLACEHOLDER:
Expand Down
83 changes: 81 additions & 2 deletions betterproto/casing.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import stringcase
import re

# Word delimiters and symbols that will not be preserved when re-casing.
# language=PythonRegExp
SYMBOLS = "[^a-zA-Z0-9]*"

# Optionally capitalized word.
# language=PythonRegExp
WORD = "[A-Z]*[a-z]*[0-9]*"

# Uppercase word, not followed by lowercase letters.
# language=PythonRegExp
WORD_UPPER = "[A-Z]+(?![a-z])[0-9]*"


def safe_snake_case(value: str) -> str:
"""Snake case a value taking into account Python keywords."""
value = stringcase.snakecase(value)
value = snake_case(value)
if value in [
"and",
"as",
Expand Down Expand Up @@ -39,3 +51,70 @@ def safe_snake_case(value: str) -> str:
# https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles
value += "_"
return value


def snake_case(value: str, strict: bool = True):
"""
Join words with an underscore into lowercase and remove symbols.
@param value: value to convert
@param strict: force single underscores
"""

def substitute_word(symbols, word, is_start):
if not word:
return ""
if strict:
delimiter_count = 0 if is_start else 1 # Single underscore if strict.
elif is_start:
delimiter_count = len(symbols)
elif word.isupper() or word.islower():
delimiter_count = max(
1, len(symbols)
) # Preserve all delimiters if not strict.
else:
delimiter_count = len(symbols) + 1 # Extra underscore for leading capital.

return ("_" * delimiter_count) + word.lower()

snake = re.sub(
f"(^)?({SYMBOLS})({WORD_UPPER}|{WORD})",
lambda groups: substitute_word(groups[2], groups[3], groups[1] is not None),
value,
)
return snake


def pascal_case(value: str, strict: bool = True):
"""
Capitalize each word and remove symbols.
@param value: value to convert
@param strict: output only alphanumeric characters
"""

def substitute_word(symbols, word):
if strict:
return word.capitalize() # Remove all delimiters

if word.islower():
delimiter_length = len(symbols[:-1]) # Lose one delimiter
else:
delimiter_length = len(symbols) # Preserve all delimiters

return ("_" * delimiter_length) + word.capitalize()

return re.sub(
f"({SYMBOLS})({WORD_UPPER}|{WORD})",
lambda groups: substitute_word(groups[1], groups[2]),
value,
)


def camel_case(value: str, strict: bool = True):
"""
Capitalize all words except first and remove symbols.
"""
return lowercase_first(pascal_case(value, strict=strict))


def lowercase_first(value: str):
return value[0:1].lower() + value[1:]
Loading