Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement command line interface #165

Draft
wants to merge 54 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
0d6ced5
Implement Message.__bool__ for #130
Gobot1234 Aug 24, 2020
7746b91
Add a test for it
Gobot1234 Aug 24, 2020
53a7df0
Merge branch 'master' into master
Gobot1234 Sep 2, 2020
9da923d
Blacken
Gobot1234 Sep 2, 2020
17e31f4
Update tests
Gobot1234 Sep 19, 2020
a53d805
Fix bool
Gobot1234 Sep 19, 2020
b3b7c00
Fix failing tests
Gobot1234 Sep 19, 2020
18f22fa
Make plugin use betterproto generated classes internally
nat-n Oct 18, 2020
2cc3e05
Update deps & add generate_lib task
nat-n Oct 18, 2020
230721f
Fix template bug resulting in empty __post_init__ methods
nat-n Oct 19, 2020
de9c0a0
Implement command line interface
Gobot1234 Oct 19, 2020
4a4429d
Update docs
Gobot1234 Oct 19, 2020
48e80cf
Merge branch 'master' into master
Gobot1234 Oct 19, 2020
86d7c30
Add __bool__ to special members
Gobot1234 Oct 20, 2020
5c8e926
Update __init__.py
Gobot1234 Oct 20, 2020
f10bec4
Simplify bool
Gobot1234 Oct 27, 2020
e0eb291
Fix some typos
Gobot1234 Nov 7, 2020
e04fcb6
Tweak __bool__ docstring
nat-n Nov 24, 2020
53b2bca
Sort the list of sources in generated file headers
nat-n Oct 19, 2020
9c4e8d8
Implement command line interface
Gobot1234 Oct 19, 2020
2e9ec7a
Fix some typos
Gobot1234 Nov 7, 2020
4fde7b1
Implement command line interface
Gobot1234 Oct 19, 2020
ee40943
Fix some typos
Gobot1234 Nov 7, 2020
a68e36f
Merge remote-tracking branch 'origin/better-cli-interface' into bette…
Gobot1234 Jan 16, 2021
e6d1eaa
Initial update
Gobot1234 Jan 17, 2021
231dc05
Write files concurrently and general improvements
Gobot1234 Jan 18, 2021
1f5df3d
Fix up everything
Gobot1234 Jan 19, 2021
b908c58
Improve stuff a lot
Gobot1234 Jan 21, 2021
8b4380f
Final stuff to make it work
Gobot1234 Jan 22, 2021
16af499
More stuff
Gobot1234 Jan 22, 2021
7faec49
Fix some weird bugs
Gobot1234 Jan 23, 2021
305b7df
Boring stuff
Gobot1234 Jan 26, 2021
3553e0e
Fix some bugs
Gobot1234 Jan 27, 2021
8eb7c90
Ensure paths are sorted
Gobot1234 Jan 27, 2021
0c4277b
Redo error handling to be much more reliable
Gobot1234 Jan 31, 2021
f807e39
Finish up
Gobot1234 Jan 31, 2021
944a6be
Update docs
Gobot1234 Feb 1, 2021
5a1819e
Update docs again
Gobot1234 Feb 1, 2021
f31eb98
Regen lock
Gobot1234 Feb 1, 2021
9f636d0
Merge branch 'master' into better-cli-interface
Gobot1234 Mar 4, 2021
48a5e7c
Cleanup code
Gobot1234 Mar 21, 2021
8596a61
Regen lock
Gobot1234 Mar 21, 2021
07a2e9d
Make the CLI better behaved
Gobot1234 Mar 21, 2021
c2d1fdb
Merge remote-tracking branch 'origin/master' into better-cli-interface
Gobot1234 Mar 27, 2021
1f69bdd
Merge remote-tracking branch 'upstream/master' into better-cli-interface
Gobot1234 Apr 2, 2021
dd54d54
Merge remote-tracking branch 'upstream/master' into better-cli-interface
Gobot1234 Apr 2, 2021
23747c5
Rebase stuff
Gobot1234 Apr 2, 2021
a72b907
Pick this back up
Gobot1234 Jul 18, 2021
329d25d
More stuff
Gobot1234 Aug 6, 2021
63c9fd4
Merge remote-tracking branch 'upstream/master' into better-cli-interface
Gobot1234 Feb 7, 2022
e595356
Revive this
Gobot1234 Feb 7, 2022
66f96c4
Revive this
Gobot1234 Feb 7, 2022
8f1746f
Add NoopProgress
Gobot1234 Feb 15, 2022
61f0335
Make stuff work slightly more
Gobot1234 Feb 17, 2022
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
50 changes: 17 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ This project exists because I am unhappy with the state of the official Google p
- Uses `SerializeToString()` rather than the built-in `__bytes__()`
- Special wrapped types don't use Python's `None`
- Timestamp/duration types don't use Python's built-in `datetime` module

This project is a reimplementation from the ground up focused on idiomatic modern Python to help fix some of the above. While it may not be a 1:1 drop-in replacement due to changed method names and call patterns, the wire format is identical.

## Installation

First, install the package. Note that the `[compiler]` feature flag tells it to install extra dependencies only needed by the `protoc` plugin:
First, install the package. Note that the `[compiler]` feature flag tells it to install extra dependencies only needed by the code generator:

```sh
# Install both the library and compiler
Expand Down Expand Up @@ -71,18 +72,10 @@ message Greeting {
}
```

You can run the following to invoke protoc directly:
To compile the protobuf you would run the following:

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

or run the following to invoke protoc via grpcio-tools:

```sh
pip install grpcio-tools
python -m grpc_tools.protoc -I . --python_betterproto_out=lib example.proto
betterproto compile example.proto --output=lib
```

This will generate `lib/hello/__init__.py` which looks like:
Expand Down Expand Up @@ -160,12 +153,6 @@ service Echo {
}
```

Generate echo proto file:

```
python -m grpc_tools.protoc -I . --python_betterproto_out=. echo.proto
```

A client can be implemented as follows:
```python
import asyncio
Expand All @@ -175,16 +162,13 @@ from grpclib.client import Channel


async def main():
channel = Channel(host="127.0.0.1", port=50051)
service = echo.EchoStub(channel)
response = await service.echo(echo.EchoRequest(value="hello", extra_times=1))
print(response)

async for response in service.echo_stream(echo.EchoRequest(value="hello", extra_times=1)):
print(response)

# don't forget to close the channel when done!
channel.close()
async with Channel(host="127.0.0.1", port=50051) as channel:
service = echo.EchoStub(channel)
response = await service.echo(echo.EchoRequest(value="hello", extra_times=1))
print(response)

async for response in service.echo_stream(echo.EchoRequest(value="hello", extra_times=1)):
print(response)


if __name__ == "__main__":
Expand Down Expand Up @@ -278,23 +262,23 @@ You can use `betterproto.which_one_of(message, group_name)` to determine which o
```py
>>> test = Test()
>>> betterproto.which_one_of(test, "foo")
["", None]
("", None)

>>> test.on = True
>>> betterproto.which_one_of(test, "foo")
["on", True]
("on", True)

# Setting one member of the group resets the others.
>>> test.count = 57
>>> betterproto.which_one_of(test, "foo")
["count", 57]
("count", 57)
>>> test.on
False

# Default (zero) values also work.
>>> test.name = ""
>>> betterproto.which_one_of(test, "foo")
["name", ""]
("name", "")
>>> test.count
0
>>> test.on
Expand All @@ -310,7 +294,7 @@ Again this is a little different than the official Google code generator:

# New way (this project)
>>> betterproto.which_one_of(message, "group")
["foo", "foo's value"]
("foo", "foo's value")
```

### Well-Known Google Types
Expand Down Expand Up @@ -445,7 +429,7 @@ poe full-test
Betterproto includes compiled versions for Google's well-known types at [betterproto/lib/google](betterproto/lib/google).
Be sure to regenerate these files when modifying the plugin output format, and validate by running the tests.

Normally, the plugin does not compile any references to `google.protobuf`, since they are pre-compiled. To force compilation of `google.protobuf`, use the option `--custom_opt=INCLUDE_GOOGLE`.
Normally, the plugin does not compile any references to `google.protobuf`, since they are pre-compiled. To force compilation of `google.protobuf`, use the option `--custom_opt=INCLUDE_GOOGLE`.

Assuming your `google.protobuf` source files (included with all releases of `protoc`) are located in `/usr/local/include`, you can regenerate them as follows:

Expand Down
2 changes: 1 addition & 1 deletion docs/migrating.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Migrating Guide
Google's protocolbuffers
------------------------

betterproto has a mostly 1 to 1 drop in replacement for Google's protocolbuffers (after
betterproto has a mostly 1 to 1 drop in replacement for Google's protocol buffers (after
regenerating your protobufs of course) although there are some minor differences.

.. note::
Expand Down
28 changes: 7 additions & 21 deletions docs/quick-start.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,11 @@ Given you installed the compiler and have a proto file, e.g ``example.proto``:
string message = 1;
}

To compile the proto you would run the following:

You can run the following to invoke protoc directly:
To compile the protobuf you would run the following:

.. code-block:: sh

mkdir hello
protoc -I . --python_betterproto_out=lib example.proto

or run the following to invoke protoc via grpcio-tools:

.. code-block:: sh

pip install grpcio-tools
python -m grpc_tools.protoc -I . --python_betterproto_out=lib example.proto

betterproto compile example.proto --output=lib

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

Expand Down Expand Up @@ -141,16 +130,13 @@ The generated client can be used like so:


async def main():
channel = Channel(host="127.0.0.1", port=50051)
service = echo.EchoStub(channel)
response = await service.echo(value="hello", extra_times=1)
print(response)

async for response in service.echo_stream(value="hello", extra_times=1):
async with Channel(host="127.0.0.1", port=50051) as channel:
service = echo.EchoStub(channel)
response = await service.echo(value="hello", extra_times=1)
print(response)

# don't forget to close the channel when you're done!
channel.close()
async for response in service.echo_stream(value="hello", extra_times=1):
print(response)

asyncio.run(main()) # python 3.7 only

Expand Down
14 changes: 6 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ black = { version = ">=19.3b0", optional = true }
dataclasses = { version = "^0.7", python = ">=3.6, <3.7" }
grpclib = "^0.4.1"
jinja2 = { version = ">=2.11.2", optional = true }
typer = { version = "^0.3.2", optional = true }
rich = { version = "^11.2.0", optional = true }
python-dateutil = "^2.8"

[tool.poetry.dev-dependencies]
Expand All @@ -36,13 +38,14 @@ sphinx = "3.1.2"
sphinx-rtd-theme = "0.5.0"
tomlkit = "^0.7.0"
tox = "^3.15.1"

# protobuf_parser = "1.0.0"

[tool.poetry.scripts]
betterproto = "betterproto:__main__.main"
protoc-gen-python_betterproto = "betterproto.plugin:main"

[tool.poetry.extras]
compiler = ["black", "jinja2"]
compiler = ["black", "jinja2", "typer", "rich", "protobuf_parser"]


# Dev workflow tasks
Expand Down Expand Up @@ -81,12 +84,7 @@ help = "Clean out generated files from the workspace"

[tool.poe.tasks.generate_lib]
cmd = """
protoc
--plugin=protoc-gen-custom=src/betterproto/plugin/main.py
--custom_opt=INCLUDE_GOOGLE
--custom_out=src/betterproto/lib
-I /usr/local/include/
/usr/local/include/google/protobuf/**/*.proto
betterproto compile /usr/local/include/google/protobuf/**/*.proto
"""
help = "Regenerate the types in betterproto.lib.google"

Expand Down
4 changes: 2 additions & 2 deletions src/betterproto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from abc import ABC
from base64 import b64decode, b64encode
from datetime import datetime, timedelta, timezone
from dateutil.parser import isoparse
from typing import (
Any,
Callable,
Expand All @@ -25,12 +24,13 @@
get_type_hints,
)

from dateutil.parser import isoparse

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


# Proto 3 data types
TYPE_ENUM = "enum"
TYPE_BOOL = "bool"
Expand Down
7 changes: 7 additions & 0 deletions src/betterproto/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .plugin.exception_hook import install_exception_hook

install_exception_hook()
from .plugin.cli import app as main

if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions src/betterproto/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

if TYPE_CHECKING:
from grpclib._typing import IProtoMessage

from . import Message

# Bound type variable to allow methods to return `self` of subclasses
Expand Down
1 change: 0 additions & 1 deletion src/betterproto/plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
from .main import main
4 changes: 2 additions & 2 deletions src/betterproto/plugin/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .main import main


main()
if __name__ == "__main__":
main()
6 changes: 6 additions & 0 deletions src/betterproto/plugin/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
VERBOSE = False

from black.const import DEFAULT_LINE_LENGTH as DEFAULT_LINE_LENGTH

from .commands import app as app
from .runner import compile_protobufs as compile_protobufs
115 changes: 115 additions & 0 deletions src/betterproto/plugin/cli/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import sys
from pathlib import Path
from typing import List, Optional

import protobuf_parser
import rich
import typer
from rich.syntax import Syntax

from ... import __version__
from ..models import monkey_patch_oneof_index
from . import DEFAULT_LINE_LENGTH, VERBOSE, utils
from .runner import compile_protobufs

monkey_patch_oneof_index()
app = typer.Typer()


@app.callback(context_settings={"help_option_names": ["-h", "--help"]})
def callback(ctx: typer.Context) -> None:
"""The callback for all things betterproto"""
if ctx.invoked_subcommand is None:
rich.print(ctx.get_help())


@app.command()
def version(ctx: typer.Context) -> None:
rich.print("betterproto version:", __version__)


@app.command(context_settings={"help_option_names": ["-h", "--help"]})
@utils.run_sync
async def compile(
verbose: bool = typer.Option(
VERBOSE, "-v", "--verbose", help="Whether or not to be verbose"
),
line_length: int = typer.Option(
DEFAULT_LINE_LENGTH,
"-l",
"--line-length",
help="The line length to format with",
),
generate_services: bool = typer.Option(
True, help="Whether or not to generate servicer stubs"
),
output: Optional[Path] = typer.Option(
None,
help="The name of the output directory",
file_okay=False,
allow_dash=True,
),
paths: List[Path] = typer.Argument(
...,
help="The protobuf files to compile",
exists=True,
allow_dash=True,
readable=False,
),
) -> None:
"""The recommended way to compile your protobuf files."""
files = utils.get_files(paths)

if not files:
return rich.print("[bold]No files found to compile")

for output_path, protos in files.items():
output = output or (Path(output_path.parent.name) / output_path.name).resolve()
output.mkdir(exist_ok=True, parents=True)

results = await compile_protobufs(
*protos,
output=output,
verbose=verbose,
generate_services=generate_services,
line_length=line_length,
from_cli=True,
)

for result in results:
for error in result.errors:
if error.message.startswith("Syntax error"):
rich.print(
f"[red]File {str(result.file)}:\n",
Syntax.from_path(
str(result.file),
line_numbers=True,
line_range=(max(error.line - 5, 0), error.line),
),
f"{' ' * (error.column + 3)}^\nSyntaxError: {error.message}[red]",
file=sys.stderr,
)
elif isinstance(error, protobuf_parser.Warning):
rich.print(f"Warning: {error}", file=sys.stderr)
else:
failed_files = "\n".join(f" - {file}" for file in protos)
rich.print(
f"[red]Protoc failed to generate outputs for:\n\n"
f"{failed_files}\n\nSee the output for the issue:\n{error}[red]",
file=sys.stderr,
)

# has_warnings = all(isinstance(e, Warning) for e in errors)
# if not errors or has_warnings:
if True:
rich.print(
f"[bold green]Finished generating output for "
f"{len(protos)} file{'s' if len(protos) != 1 else ''}, "
f"output is in {output.as_posix()}"
)

# if errors:
# if not has_warnings:
# exit(2)
# exit(1)
exit(0)
Loading