Giter Club home page Giter Club logo

mypy-protobuf's Introduction

mypy-protobuf: Generate mypy stub files from protobuf specs

CI pypi license

2.10 is the last version of mypy-protobuf which supports targeting python 2.7.

Built originally with love at Dropbox

See Changelog for recent changes.

Requirements to run mypy-protobuf

Earlier releases might work, but aren't tested

Requirements to run typecheckers on stubs generated by mypy-protobuf

Earlier releases might work, but aren't tested

To run typecheckers on code generated with grpc plugin - you'll additionally need

Earlier releases might work, but aren't tested

Other configurations may work, but are not continuously tested currently. We would be open to expanding this list - file an issue on the issue tracker.

Installation

The plugin can be installed with

pip3 install mypy-protobuf

To install unreleased

REV=main  # or whichever unreleased git rev you'd like
pip3 install git+https://github.com/nipunn1313/mypy-protobuf.git@$REV

# For older (1.x) versions of mypy protobuf - you may need
pip3 install git+https://github.com/nipunn1313/mypy-protobuf.git@$REV#subdirectory=python

In order to run mypy on the generated code, you'll need to install

pip3 install mypy>=0.910 types-protobuf>=0.1.14

Usage

On posix, protoc-gen-mypy is installed to python's executable bin. Assuming that's on your $PATH, you can run

protoc --python_out=output/location --mypy_out=output/location

Alternately, you can explicitly provide the path:

protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=output/location --mypy_out=output/location

Check the version number with

> protoc-gen-mypy --version

Implementation

The implementation of the plugin is in mypy_protobuf/main.py, which installs to an executable protoc-gen-mypy. On windows it installs to protoc-gen-mypy.exe

Features

See Changelog for full listing

Bring comments from .proto files to docstrings in .pyi files

Comments in the .proto files on messages, fields, enums, enum variants, extensions, services, and methods will appear as docstrings in .pyi files. Useful in IDEs for showing completions with comments.

Types enum int values more strongly

Enum int values produce stubs which wrap the int values in NewType

enum MyEnum {
  HELLO = 0;
  WORLD = 1;
}

Will yield an enum type wrapper whose methods type to MyEnum.ValueType (a NewType(int) rather than int. This allows mypy to catch bugs where the wrong enum value is being used.

Calling code may be typed as follows.

In python >= 3.7

# May need [PEP 563](https://www.python.org/dev/peps/pep-0563/) to postpone evaluation of annotations
# from __future__ import annotations  # Not needed with python>=3.11 or protobuf>=3.20.0
def f(x: MyEnum.ValueType):
    print(x)
f(MyEnum.Value("HELLO"))

With protobuf <= 3.20.0, for usages of cast, the type of x must be quoted After protobuf >= 3.20.0 - ValueType exists in the python code and quotes aren't needed until upstream protobuf includes ValueType

cast('MyEnum.ValueType', x)

Similarly, for type aliases with protobuf < 3.20.0, you must either quote the type or hide it behind TYPE_CHECKING

from typing import Tuple, TYPE_CHECKING
HELLO = Tuple['MyEnum.ValueType', 'MyEnum.ValueType']
if TYPE_CHECKING:
    HELLO = Tuple[MyEnum.ValueType, MyEnum.ValueType]

Enum int impl details

mypy-protobuf autogenerates an instance of the EnumTypeWrapper as follows.

class _MyEnum:
    ValueType = typing.NewType('ValueType', builtins.int)
    V: typing_extensions.TypeAlias = ValueType
class _MyEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_MyEnum.ValueType], builtins.type):
    DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
    HELLO: _MyEnum.ValueType  # 0
    WORLD: _MyEnum.ValueType  # 1
class MyEnum(_MyEnum, metaclass=_MyEnumEnumTypeWrapper):
    pass

HELLO: MyEnum.ValueType  # 0
WORLD: MyEnum.ValueType  # 1

_MyEnumEnumTypeWrapper extends the EnumTypeWrapper to take/return MyEnum.ValueType rather than int MyEnum is an instance of the EnumTypeWrapper.

  • Use _MyEnum and of metaclass is an implementation detail to make MyEnum.ValueType a valid type w/o a circular dependency
  • V is supported as an alias of ValueType for backward compatibility

Supports generating type wrappers for fields and maps

M.proto

message M {
  uint32 user_id = 1 [(mypy_protobuf.options).casttype="mymod.UserId"];
  map<uint32, string> email_by_uid = 2 [
    (mypy_protobuf.options).keytype="path/to/mymod.UserId",
    (mypy_protobuf.options).valuetype="path/to/mymod.Email"
  ];
}

mymod.py

UserId = NewType("UserId", int)
Email = NewType("Email", Text)

py_generic_services

If py_generic_services is set in your proto file, then mypy-protobuf will generate service stubs. If you want GRPC stubs instead - use the GRPC instructions.

readable_stubs

If readable_stubs is set, mypy-protobuf will generate easier-to-read stubs. The downside to this approach - is that it's possible to generate stubs which do not pass mypy - particularly in the case of name collisions. mypy-protobuf defaults to generating stubs with fully qualified imports and mangled global-level identifiers to defend against name collisions between global identifiers and field names.

If you're ok with this risk, try it out!

protoc --python_out=output/location --mypy_out=readable_stubs:output/location

relax_strict_optional_primitives

If you are using proto3, then primitives cannot be represented as NULL on the wire - only as their zero value. By default mypy-protobuf types message constructors to have non-nullable primitives (eg int instead of Optional[int]). python-protobuf itself will internally convert None -> zero value. If you intentionally want to use this behavior, set this flag! We recommend avoiding this, as it can lead to developer error - confusing NULL and 0 as distinct on the wire. However, it may be helpful when migrating existing proto2 code, where the distinction is meaningful

protoc --python_out=output/location --mypy_out=relax_strict_optional_primitives:output/location

Output suppression

To suppress output, you can run

protoc --python_out=output/location --mypy_out=quiet:output/location

GRPC

This plugin provides stubs generation for grpcio generated code.

protoc \
    --python_out=output/location \
    --mypy_out=output/location \
    --grpc_out=output/location \
    --mypy_grpc_out=output/location

Note that generated code for grpc will work only together with code for python and locations should be the same. If you need stubs for grpc internal code we suggest using this package https://github.com/shabbyrobe/grpc-stubs

Targeting python2 support

mypy-protobuf's drops support for targeting python2 with version 3.0. If you still need python2 support -

python3 -m pip install mypy_protobuf==2.10
protoc --python_out=output/location --mypy_out=output/location
mypy --target-version=2.7 {files}

Contributing

Contributions to the implementation are welcome. Please run tests using ./run_test.sh. Ensure code is formatted using black.

pip3 install black
black .

Contributors

Licence etc.

  1. License: Apache 2.0.
  2. Copyright attribution: Copyright (c) 2022 Nipunn Koorapati

mypy-protobuf's People

Contributors

bradenaw avatar chadrik avatar dependabot[bot] avatar dzbarsky avatar epronovost avatar evgenus avatar gvanrossum avatar henribru avatar icgood avatar jaredkhan avatar jcppkkk avatar juzna avatar ketouem avatar mhdante avatar miaachan avatar mikolajz avatar msullivan avatar nipunn1313 avatar nmiculinic avatar pcorpet avatar peterlvilim avatar reorx avatar robinmccorkell avatar shabbyrobe avatar smessmer avatar sobolevn avatar twmr avatar wwuck avatar zifter avatar zozoens31 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mypy-protobuf's Issues

module.DESCRIPTOR is not supported

When attempting to access the DESCRIPTOR attribute on a protobuf module, i get a Module has no attribute "DESCRIPTOR" error. From looking at the code, it seems we generate a DESCRIPTOR attributer on messages and enums, but not one for the module itself.

This should probably be generated, since it is valid syntax and is the correct way to access the FileDescriptor for the file that corresponds to the current pb2.py file.

Function is not valid as a type

Hi,

Thank you for this wonderful project. I'm integrating mypy with a codebase that uses protobufs and feel that this will be quite helpful in doing so!

I'm able to generate the *.pyi type stub files along with the other definitions by invoking the following command:

$ python -m grpc_tools.protoc -I=github.com/gogo/protobuf/gogoproto -I=. \
    --python_out=generated/python \
    --grpc_python_out=generated/python \
    --mypy_out=generated/python

The generated *.pyi files look correct, however when I try to run mypy over modules that import these definitions, I receive error messages like the following (names/line #s scrubbed):

generated/python/foo_pb2.pyi:123: error: Function "Foo" is not valid as a type
generated/python/foo_pb2.pyi:123: note: Perhaps you need "Callable[...]" or a callback protocol?

Inside of foo_pb2.py, Foo is defined like follows:

Foo = _reflection.GeneratedProtocolMessageType('Foo', (_message.Message,), {
  'DESCRIPTOR' : _FOO,
  '__module__' : 'path.to.foo'
  # @@protoc_insertion_point(class_scope:Foo)
  })
_sym_db.RegisterMessage(Foo)

Is there something obvious I'm doing wrong? I'm using the following versions for my dependencies:

  • mypy 0.761
  • mypy-protobuf 1.18
  • Python 3.6.5
  • python-protobuf 3.11.3
$ python -m grpc_tools.protoc --version
libprotoc 3.8.0

Thanks!

Add testing for python protoc-gen-mypy

Generate some example protos and verify the output of the protoc-gen-mypy script

  • Verify output matches expected
  • Verify output runs through mypy (syntactically correct)

Support for nested imports

What will it take to add support for nested imports? This seems to be supported in the Go version but not in Python.

Syntax error output if a field is named 'from'

This tool generates .pyi files with syntax error when parsing a proto-file with a field named from. E.g. foo.proto:

message FooRequest {
	int64 from = 5;
}

Running mypy on a project with this kind of autogenerated files results in syntax error:

$ mypy
package/schemas/foo_pb2.pyi:356: error: invalid syntax

This is because the foo_pb2.pyi file has this in it:

class FooRequest(google___protobuf___message___Message):
    from = ... # type: int

The from = ... is invalid syntax, and so we have a problem.

Just to say I've set up my mypy to ignore these files, mypy.ini:

[mypy]

[mypy-package.schemas.*]
ignore_errors = True

But this doesn't ignore syntax errors. As per python/mypy#6897 mypy can't ignore syntax errors. It would be favorable if this project did not generate syntax-invalid output.

I'm using versions:

$ python --version Python 3.6.7
mypy==0.701
mypy-protobuf==1.10

And here is a gist with the entire contents of the .proto file, the generated .py file (which does not have syntax errors and the problematic and unexcludable .pyi file:
https://gist.github.com/gaggle/8aef6102d314936c9eb33724194682b0

HasField doesn't accept dynamically generated strings

We started using mypy-protobuf for a bit larger codebase and hit two problems. For the first I have PR #106, the second is harder.

mypy-protobuf create this nice Literal union to validate the parameter to HasField when it's a literal. However, when it's dynamically generated, it fails the type check (e.g., foo.HasField(arg + "_timestamp")). I don't know - is it possible to still validate the literal strings while allowing for non-literal strings as well?

When the protobuf type stubs don't live as top level packages, it's hard to use mypy-protobuf

Thanks to @smessmer for the report
Currently, mypy-protobuf uses absolute imports. If the protobuf stubs aren't a top level package, they won't be found.

Fixing mypy-protobuf would involve either

  • Using relative imports everywhere (including in/out of nested packages)
  • Prepending the "python_out" path from the protoc invocation (this is not provided to us through CodeGeneratorRequest, so it may need to be an additional argument)

Workarounds in the meantime might include

  • rejigger the protoc invocation to set python_out="." (top level)
  • Adding the sub path of the stubs to PYTHONPATH

python2/3 incompatibility

Writing mypy to oracle_pb2.pyi
Writing mypy to types_pb2.pyi
Traceback (most recent call last):
  File "/home/lpp/go/src/github.com/nmiculinic/x/.venv/bin/protoc-gen-mypy", line 313, in <module>
    main()
  File "/home/lpp/go/src/github.com/nmiculinic/x/.venv/bin/protoc-gen-mypy", line 310, in main
    sys.stdout.write(output)
TypeError: write() argument must be str, not bytes
--mypy_out: protoc-gen-mypy: Plugin failed with status code 1.

python is mapped to: Python 3.6.4

Import for standard protobuf formats is wrong

Generates:

from google.protobuf.timestamp import (
    Timestamp,
)

and it should be:

from google.protobuf.timestamp_pb2 import (
    Timestamp,
)

similarly for other standard types:

from google.protobuf.empty_pb2 import (
    Empty,
)

Generated constructors incorrectly accept positional arguments

The annotated __init__ method generated for a protobuf message accepts positional arguments, but the underlying implementation accepts only named arguments. The first parameter after self should be a nameless * parameter. This will tell mypy and other type checkers that positional arguments are not allowed when calling the constructor.

For example, a protobuf message with a string: name field will generate the following annotated stub:

class MyMessage(google___protobuf___message___Message):
    name = ... # type: typing___Text

    def __init__(self,
        name : typing___Optional[typing___Text] = None) -> None: ...

If callers attempt to construct this message by calling MyMessage('bob'), a runtime error occurs indicating that positional arguments are not allowed for this call.

To match the underlying implementation, it should be:

    def __init__(self, *,
        name : typing___Optional[typing___Text] = None) -> None: ...

mypy import RepeatedScalarFieldContainer, not found

Hey guys, great work! This is already really helpful, thanks :).

I am having an issue with the generated code for repeated fields, I see you are using RepeatedScalarFieldContainer from mypy, but with version 0.560, I can't find it. Which version of mypy were you using for this to work?

For the record, in myproto_pb2.pyi, I am talking about the import

from mypy import (
    RepeatedScalarFieldContainer,
)

Thank you

Plugin does not generate gRPC stubs to *_pb2_grpc.py

Starting from some version of the protoc compiler, gRPC service stubs are no longer generated into *_pb2.py files. Now they are generated into separate *_pb2_grpc.py files. So to properly support grpcio library *_pb2_grpc.pyi files should be generated for service stubs.

And it would be great to generate gRPC stubs optionally, this would be possible if two separate CLI options (plugins) for protoc were created:

  • --mypy_out for messages
  • --mypy_gprc_out for gRPC stubs

I'm asking for this feature because I'm the author of the grpclib library, which is an alternative gRPC implementation for asyncio, and users of this library don't need gRPC stubs for grpcio.

Local Imports out of nested packages are mis-annotated

From #18 (thanks @jcppkkk)

This PR cause mypy-protobuf generate error pyi files that fail mypy check.

For example, in https://developers.google.com/protocol-buffers/docs/reference/python-generated there is a example command:

protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto
What if baz.proto imports foo.proto?

import "foo.proto";
It generate

from .foo_pb2 import (
and fails mypy check: cannot find module named foo_pb2; The same error happens to our project after update to 1.4.

This change need to calculate relative filepath between current file to target file.

FromString argument type doesn't work for Python2

As I understand, the typeshed definition is used for the definition of the base Message class and a commit in October ([1]) changed the argument of FromString from "Any" to "bytes" in Python3 and a Union of "bytes", "buffer" and "unicode"(strange) in Python 2. mypy-protobuf emits bytes as the type of the argument of FromString, thus in Python2 we get lots of errors about the argument type of the subclass being more narrow that the superclass.

The code in the attachment seems to make it work. Will you want to include such a fix?

mypy-diff.txt

[1] python/typeshed@036abc7#diff-2492d636b6705e307c9cb32d169f3614

Proposal: Enum Literals Support

See Discussion in #56

I actually think we're able to get best of both worlds here by using the mypy Literal type. This way, the typing will be indeed an int, but the int is constrained to only the legitimate values.

https://mypy.readthedocs.io/en/latest/literal_types.html

First attempt at a diff was #79
Discussion after landing:

@Kami

First thanks for adding enum literal support.

This indeed seems to fix direct enum literal notation, but it breaks the Value() method scenario which takes a string (enum name) and returns an enum value.

For example, with the following definition:

enum ExecutionStatus {
    REQUESTED = 0;
    SCHEDULED = 1;
    RUNNING = 2;
}

This used to work:

from messages_pb2 import ExecutionStatus

status_str = 'requested'
obj.status = ExecutionStatus.Value(status_str.upper())  # this should return enum value (aka Enum type / int)

Now it returns the following error:

error: Incompatible return value type (got "Union[Literal[0], Literal[1], Literal[2]]", expected "ExecutionStatus")

It works fine with just the change from #81, but not with this change.

It may also brake Name() and other enum class methods, but I didn't have time to dig in yet.

@nipunn1313

I poked around a bit and I believe that the issue here is that obj.status is typed to ExecutionStatus, but it would now need to be typed to ExecutionStatus.ClosedValueType

since ExecutionStatus is the enum itself and ExecutionStatus.ClosedValueType is the type of the Values in the enum (and ExecutionStatus.ClosedKeyType is the type of the Keys in the enum)

The merged PR was non-backward-compatible in that it changed the return value of Typing to literals, and move the int-y-ness of the enum to the ClosedValueType member.

This is more accurate based on this example I found

enum UserFSWAction {
   UNKNOWN = 0;
   ALLOW = 1;
}
>>> type(ALLOW)
<class 'int'>
>>> ALLOW
1
>>> UserFSWAction.Name(1)
'ALLOW'
>>> UserFSWAction.Value('ALLOW')
1
>>> type(UserFSWAction.Value('ALLOW'))
<class 'int'>

@Kami - does that make sense directionally?

I'll hold off on publishing to pypi until we have some alignment here. I'm ok with making a non-backward compatible change, but I'll want to have some clear changelist and bump the big version number.

Couple more considerations for proposal

change typing of the constants to be the ClosedValueType for better error messages

- ALLOW: typing_extensions___Literal[1]
+ ALLOW: UserFSWAction.ClosedValueType

change ClosedValueType to just ValueType to simplify notation required by callers.

Mypy currently does not support module import references

eg

import dropbox.proto.nucleus.types_pb2
from dropbox.proto.nucleus.types_pb2 import FileId
reveal_type(FileId)
reveal_type(dropbox.proto.nucleus.types_pb2.FileId)

results in

dropbox/proto/nucleus/app_service_pb2.pyi:17: error: Revealed type is 'def (id: Union[builtins.str, None] =) -> dropbox.proto.nucleus.types_pb2.FileId'
dropbox/proto/nucleus/app_service_pb2.pyi:18: error: Revealed type is 'Any'

Proposed workaround is:

from dropbox.proto.nucleus.types_pb2 import FileId as dropbox___proto___nucleus___types_pb2___FileId

Until mypy gains support for module imports like this (see python/mypy#5583).

Ambiguous import paths with mypy 0.780?

Hi!

We currently use mypy-protobuf in a typechecking CI step for buildgrid. With the release of mypy 0.780, we began to see the following error in the CI.

buildgrid/_protos/build/bazel/semver/semver_pb2.pyi: error: Source file found twice under different module names: 'buildgrid._protos.build.bazel.semver.semver_pb2' and 'build.bazel.semver.semver_pb2'
Found 1 error in 1 file (checked 144 source files)

We tried regenerating the .pyi files with the newer version of mypy, but the issue persisted, so we've had to roll back our mypy version.

Some additional information:

I'm not sure if this is an issue with mypy, mypy-protobuf, or our configuration, so please let us know if we should post this elsewhere!

Using within bazel?

Hey team๐Ÿ™Œ
I know dropbox is using Bazel and wanted to know if you can publish a BUILD file with mypy-protobuf as a dependency (go impl in better)

Thanks and really helpfull project!

Message enum attributes have incorrect type annotations

Running mypy-protobuf on the following proto definition

syntax = "proto3";

enum Enum {
  OK = 0;
  NOT_OK = 1;
}

message Message {
  Enum enum = 1;
}

Produces a .pyi file with

class Enum(builtin___int):
    DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ...
    @classmethod
    def Name(cls, number: builtin___int) -> builtin___str: ...
    @classmethod
    def Value(cls, name: builtin___str) -> 'Enum': ...
    @classmethod
    def keys(cls) -> typing___List[builtin___str]: ...
    @classmethod
    def values(cls) -> typing___List['Enum']: ...
    @classmethod
    def items(cls) -> typing___List[typing___Tuple[builtin___str, 'Enum']]: ...
    OK = typing___cast('Enum', 0)
    NOT_OK = typing___cast('Enum', 1)
OK = typing___cast('Enum', 0)
NOT_OK = typing___cast('Enum', 1)
global___Enum = Enum

class Message(google___protobuf___message___Message):
    DESCRIPTOR: google___protobuf___descriptor___Descriptor = ...
    enum = ... # type: global___Enum

which suggests to mypy that the enum attributes exist on the enum object e.g. that Message().enum.OK is valid. In reality the generated code (using grpc_tools) has type(Message().enum) == int, which obviously doesn't have the OK attributes (or keys, values etc) and Message().enum.OK fails at runtime

Plugin doesn't support all type of notations for accessing Enum values

Currently it appears that this plugin doesn't support all type of notations for accessing Enum values.

Example protobuf definition (execution.proto):

enum ExecutionStatus {
    REQUESTED = 0;
    SCHEDULED = 1;
    RUNNING = 2;
    SUCCEEDED = 3;
    FAILED = 4;
    TIMED_OUT = 5;
    CANCELED = 6;
}

Right now, the following ways to refer to the enum value work and don't result in a mypy error:

import execution_pb2

execution_pb2.REQUESTED
execution_pb2.Value('REQUESTED')
...

The problem is that the following more verbose approach (where you access the value via enum type class) returns an error:

execution_pb2.ExecutionStatus.REQUESTED
foo.py: error: "Type[ExecutionStatus]" has no attribute "SUCCEEDED"

This approach is supported by the protobuf generated Python code. I also personally prefer it since it's more explicit and obvious when there are multiple top-level enum types defined in a single proto file.

Using Value('') method is also not desired since it doesn't provide any static typing guarantees - if you pass in an invalid / unsupported value, this will only be detected during run-time.

I checked the generated annotations file and it looks like it only writes type hints for accessing enum values via global module level variables.

From the generated type hints code:

class ExecutionStatus(int):
    DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ...
    @classmethod
    def Name(cls, number: int) -> str: ...
    @classmethod
    def Value(cls, name: str) -> ExecutionStatus: ...
    @classmethod
    def keys(cls) -> typing___List[str]: ...
    @classmethod
    def values(cls) -> typing___List[ExecutionStatus]: ...
    @classmethod
    def items(cls) -> typing___List[typing___Tuple[str, ExecutionStatus]]: ...
REQUESTED = typing___cast(ExecutionStatus, 0)
SCHEDULED = typing___cast(ExecutionStatus, 1)
RUNNING = typing___cast(ExecutionStatus, 2)
SUCCEEDED = typing___cast(ExecutionStatus, 3)
FAILED = typing___cast(ExecutionStatus, 4)
TIMED_OUT = typing___cast(ExecutionStatus, 5)
CANCELED = typing___cast(ExecutionStatus, 6)

I propose to update it so it looks like this:

class ExecutionStatus(int):
    DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ...
    @classmethod
    def Name(cls, number: int) -> str: ...
    @classmethod
    def Value(cls, name: str) -> ExecutionStatus: ...
    @classmethod
    def keys(cls) -> typing___List[str]: ...
    @classmethod
    def values(cls) -> typing___List[ExecutionStatus]: ...
    @classmethod
    def items(cls) -> typing___List[typing___Tuple[str, ExecutionStatus]]: ...
    REQUESTED = typing___cast(ExecutionStatus, 0)
    SCHEDULED = typing___cast(ExecutionStatus, 1)
    RUNNING = typing___cast(ExecutionStatus, 2)
    SUCCEEDED = typing___cast(ExecutionStatus, 3)
    FAILED = typing___cast(ExecutionStatus, 4)
    TIMED_OUT = typing___cast(ExecutionStatus, 5)
    CANCELED = typing___cast(ExecutionStatus, 6)

REQUESTED = typing___cast(ExecutionStatus, 0)
SCHEDULED = typing___cast(ExecutionStatus, 1)
RUNNING = typing___cast(ExecutionStatus, 2)
SUCCEEDED = typing___cast(ExecutionStatus, 3)
FAILED = typing___cast(ExecutionStatus, 4)
TIMED_OUT = typing___cast(ExecutionStatus, 5)
CANCELED = typing___cast(ExecutionStatus, 6)

This way those values can be access using both supported way - directly via top level variable or via enum class level variable.

Somewhat related issue #56.

Add `protobuf` to setup.py

When using mypy-protobuf, the plugin expects pypi/protobuf to be installed.
If it's not, the build fails:

Traceback (most recent call last):
  File "/Users/gtalarico/dev/repos/gtalarico/schema/.venv/bin/protoc-gen-mypy", line 13, in <module>
    import google.protobuf.descriptor_pb2 as d
ModuleNotFoundError: No module named 'google'
--mypy_out: protoc-gen-mypy: Plugin failed with status code 1.

If protobuf pkg is required, seems to me we could just add it to setup.py so the dependency is automatically when mypy-protobuf is installed

Thoughts?

# setup.py
setup(
    install_requires=["protobuf"],
    # ...
)

Any way to use with PyCharm

Hello, sorry for offtopic, but as PyCharm supports pyi files internally, are there known way to configure PyCharm to understand mypy-protobuf generated files? I tried to configure it, but failed, no completion achieved.

Thanks.

Pulling a few more active maintainers?

Just curious if this repository is going to be actively maintained going forward, since it's gone somewhat stagnant. We definitely appreciate having this utility around (big quality of life improvement when using Python and gRPC/protobuf).

Wondering whether we should either (a.) invite in a few more active maintainers for this repo (i.e., to help review/merge PR's, etc.) or (b.) fork it and move on.

mypy raises error on .append on repeated field

Proto

# rules.proto
message Result {
  string name = 1;
}

message ResultList {
  repeated Result results = 2;
}

Code

import rules_pb2
r = Results(name="x")
result_list = ResultList()
result_list.results.append(r)
# Append works as expected, mypy raises: 
# RepeatedCompositeFieldContainer[Results]" has no attribute "append" mypy(error)

Mypy stub

class Results(google___protobuf___message___Message):

    @property
    def results(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[ResultList]: ...

False positive for accessing enum "instance fields"

For example, let's assume a Money message (containing amount and currency) and a Money.Currency enum.

This should be valid:

some_money = Money()
some_money.amount = 200
some_money.currency = Money.USD

and

some_money.currency = Money.Currency.Value('CAD')

However, this should be invalid (but is currently valid):

cad = some_money.currency.Value('CAD')

Similarly, this is a false positive:

Money.Currency('CAD')

since it finds int.__init__ although in runtime it would result in:

TypeError: 'EnumTypeWrapper' object is not callable

One way to solve it would be introduce a CurrencyValue:

class Money(google___protobuf___message___Message):
  CurrencyValue = NewType('int')
  class Currency:
    DESCRIPTOR: ...
    ...
  USD = typing___cast(CurrencyValue, 1)
  ...

This would be a breaking change since it'll require changing any signature accepting Money.Currency. Also, since it exists only in the type stubs, you'd have to quote it, i.e.

def do_something(currency: 'Money.CurrencyValue'):

Alternatively, there might be some (new?) mypy construct that allows to designate fields and functions as class-access only -- basically the same as its built-in (hardcoded, from what I could tell) support for enum.

What is the minimum proto version and python version needed?

On trying to use this with proto 2.5.0 and python 2.7 I am getting the below error

  [exec] Traceback (most recent call last):
     [exec]   File "/Users/dmanna/Documents/fresh_code/github/vnera/main/main/venv/bin/protoc-gen-mypy", line 364, in <module>
     [exec]     main()
     [exec]   File "/Users/dmanna/Documents/fresh_code/github/vnera/main/main/venv/bin/protoc-gen-mypy", line 351, in main
     [exec]     generate_mypy_stubs(Descriptors(request), response, "quiet" in request.parameter)
     [exec]   File "/Users/dmanna/Documents/fresh_code/github/vnera/main/main/venv/bin/protoc-gen-mypy", line 293, in generate_mypy_stubs
     [exec]     pkg_writer.write_messages(fd.message_type, "")
     [exec]   File "/Users/dmanna/Documents/fresh_code/github/vnera/main/main/venv/bin/protoc-gen-mypy", line 169, in write_messages
     [exec]     if msg.options.map_entry:
     [exec] AttributeError: 'MessageOptions' object has no attribute 'map_entry'
     [exec] --mypy_out: protoc-gen-mypy: Plugin failed with status code 1.
     [exec] Result: 1

  • Proto Version - 2.5.0
  • Python Version - 2.7.6

The split enum value types break type checks when used with dataclasses

Changes made in #120 are breaking typechecks when compiled types are used with dataclasses, or in general when enums are used as parameters or return values.

Quick example:
proto:

syntax = "proto3";

package bikes;
    
enum EngineType {
    TWO_STROKE = 0;
    FOUR_STROKE = 1;
}

message Motorcycle {
    string name = 1;
    EngineType engine_type = 2;
}

python:

from dataclasses import dataclass
from motorcycles_pb2 import Engine, EngineType, Motorcycle

@dataclass
class DemoEngine:
    engine_type: EngineType
    name: str

    def __init__(self, engine_type: EngineType, name: str):
        self.engine_type = engine_type
        self.name = name

d = DemoEngine(EngineType.TWO_STROKE, "demo")

type check failure:

motorcycles/__main__.py:17: error: Argument 1 to "DemoEngine" has incompatible type "EngineTypeValue"; expected "EngineType"

Protos compiled with protoc 3.12.2 using python -m grpc_tools.protoc

Invalid syntax error in generated .pyi file

Hey! I've started using your plugin, and it's working to an extent that the type hints are picked up by my IDE, which is helpful.

When trying to run mypy on the command line I get this error however:

com/fish/time/time_range_pb2.pyi:45: error: invalid syntax

The relevant lines:

 42 class TimeRange(google___protobuf___message___Message):
 43 
 44     @property
 45     def from(self) -> google___protobuf___timestamp_pb2___Timestamp: ...
 46 

I'm using this command line to check the types:

mypy --ignore-missing-imports --python-version 3.7 --namespace-packages --check-untyped-defs -p fish

where the com and fish directories reside inside the same directory, and fish is importing protobufs from com.

Before adding the --namespace-packages mypy did finish, but it didn't seem like it took the types of the protobufs into account.

The protobufs are generated by

python3 -m grpc_tools.protoc -I ./proto --python_out=. --mypy_out=. --grpc_python_out=. ./proto/com/fish/*/*.proto

mypy version 0.701, Python 3.7.3

Any ideas?

Incompatible types in assignment (x: SomeMessage = Message)

I have the following code:

from proto.example_pb2 import SomeMessage
from google.protobuf.message import Message


def request(...) -> Message:
	...

result: SomeMessage = request(
    ...
)

Request will return a protobuf message which can vary depending on the input parameters. This is giving the following mypy error:

error: Incompatible types in assignment (expression has type "Message", variable has type "SomeMessage")

Now, I understand why this is happening: Message is not assignable to SomeMessage. It would be valid the other way around (SomeMessage -> Message) because SomeMessage is a subclass of Message.

The reasoning behind this code is that while request can return any message, when I use it I already know which specific message it will return, and I want to be able to type it like this. I just don't know how to go about it.

The only solution I thought about is to use @overload with some Literal types to set the return of every message type, as it really only depends on the value a string parameter, but it seems a bit overkill. I have some other situations in my code where this happens (even without protobuf) so what I'm really asking is for help on what pattern I should be using in these kind of situations, because I'm all out of ideas.

Can anyone help me figure this out?

Thanks :)

Doesn't work with `--grpc_python_out`

The plugin runs fine for pure protobuf files, but it seems to trip up when using gRPC:

(venv) python [master ?] -> protoc -I../protos --python_out=. --grpc_python_out=. --mypy_out=. ../protos/pingpong.proto
protoc-gen-grpc_python: program not found or is not executable
--grpc_python_out: protoc-gen-grpc_python: Plugin failed with status code 1.

New version on PyPI?

Hi,

I was wondering when a new version would be released on PyPI. We're very much looking forward to some of the changes introduced since last release, but have had some trouble specifying a git SHA in our requirements.txt to point at a non-PyPI verison. Thanks so much!

Replace dash with underscore in file names

Protoc replaces dashes with underscore when creating python modules (i.e. my-file.proto will generate my_file_pb2.py). However, mypy-protobuf still creates my-file_pb2.pyi. It should probably also generate my_file_pb2.pyi.

pycharm completions are broken

Generating this file with protoc --python_out=. --mypy_out=. tetration_network_policy.proto on python 3.7. Python 3.6 tested with same results.

The resulting file is full of Unresolved reference 'TetrationNetworkPolicyProto' errors and auto-completions does not work. You can check the screenshot here:
image

When I simply delete all the TetrationNetworkPolicyProto. occurrences in the file, tada! All errors are gone and completions throughout the project work great. I'm not versed enough with the mechanics of python typing yet so I've had no insights for where the problem could be coming from.

Mypy errors out on enum and nested types (they are mis-annotated?)

I noticed mypy check fails on enum and nested types usage. I tweaked the test_generated_mypy.py to minimally reproduce the issue.

https://github.com/dropbox/mypy-protobuf/blob/master/test/test_generated_mypy.py#L52

Passing an enum instead:

s = Simple1(a_enum=1)

The example runs fine though, but mypy complains of incompatible type. Please see error below:

mypy --py2 test/test_generated_mypy.py

test/test_generated_mypy.py:70: error: Argument "a_enum" to "Simple1" has incompatible type "int"; expected "Optional[OuterEnum]"

Please see the type hint here:
https://github.com/dropbox/mypy-protobuf/blob/master/test/proto/test_pb2.pyi.expected#L126

Should the type hint say int (or str?) instead of the enum type (OuterEnum here)? Or am I missing something here?

HasField, ClearField, WhichOneof are stringly typed - mypy doesn't catch issues

def ClearField(self, field_name: Text) -> None: ...
def WhichOneof(self, oneof_group) -> Optional[Text]: ...

We could type these more strongly in mypy-protobuf using Literal, which would only allow valid field names.

Eg.

Union[Literal["field1"], Literal["field2"]]

Currently, if we try this in mypy-protobuf, we get errors of the format

dropbox/proto/nucleus/types_pb2.pyi:40: error: Argument 1 of "HasField" incompatible with supertype "Message"

however, I confirmed if we either

  1. remove HasField from the supertype
  2. mark the supertype as Any rather than Text

then mypy-protobuf can provide better typing on the specific instances of Message

I'll propose (1) to typeshed under the argument that you can't usefully call HasField on a Message without some additional knowledge of the real type.

This proposal would prevent a duck-typey use case

Message1 {
   a,
   b,
}
Message2 {
   b,
   c,
}
if m.HasField("b"):
    quack(b)

But if a caller is trying to ducktype, they could work around this by requiring m to be typed with a more specific protocol type.

For most cases, forcing HasField to be a literal will be better.

Document maintenance/compatibility of python vs go implementation

I'm curious about (a) what the decisionmaking process / motivation was behind the creation of each implementation (including motivation for language selection), and (b) how you see the overall tech investment/support for each of these playing out over time. Will you be making an effort to keep one implementation functionally and qualitatively equivalent to the other?

If it's possible, could you also please characterize how each of these implementations is used inside dropbox?

Create py3-syntax test

@nipunn1313 In your PR you're only using comments for types. As such all you use are strings (they are never considered as actual Python object). What I use is full typing annotations but using strings:

e: 'test_pb2.OuterEnumValue' = test_pb2.BAR

Previously I was able to use without the quotes but not anymore

e: test_pb2.OuterEnum = test_pb2.BAR

Originally posted by @pcorpet in #126 (comment)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.