protobuf icon indicating copy to clipboard operation
protobuf copied to clipboard

MergeFrom error when using pure python message implementation

Open monouno opened this issue 4 years ago • 11 comments

What version of protobuf and what language are you using? Version: 3.6.1 Language: Python 3.7

What operating system (Linux, Windows, ...) and version?

  • Ubuntu 19.04 (GNU/Linux 5.0.0-13-generic aarch64)
  • macOS 10.15.4

What runtime / compiler are you using (e.g., python version or gcc version) Python 3.7

What did you do? I have two proto file. In our actual project, these two files have a lot of content, only two message are pasted to illustrate the problem.

One is route.proto:

syntax = "proto3";
package a.b.c;

message Route {
    string id = 1; // uuid, generated by minion
    string dst = 2;
    string via = 3;
    string link_name = 4;
    int32 table = 5;
}

Another is network.proto:

syntax = "proto3";
package a.b.c;

message GetDefaultGatewayResponse {
    Route route = 1;
}

I run the following command in Interactive Python:

from rpc.network.network_pb2 import *
from rpc.network.route_pb2 import *
GetDefaultGatewayResponse(route=Route())

Then the following exception was thrown:

~/venv/lib/python3.7/site-packages/google/protobuf/internal/python_message.py in MergeFrom(self, msg)
   1229       raise TypeError(
   1230           "Parameter to MergeFrom() must be instance of same class: "
-> 1231           'expected %s got %s.' % (cls.__name__, msg.__class__.__name__))
   1232 
   1233     assert msg is not self

But I add print(type(msg), cls) got same value is <class 'route_pb2.Route'> but id of that different.

However, there is no such problem with cpp message implementation. What did you expect to see

In [1]: from rpc.network.network_pb2 import * 
   ...: from rpc.network.route_pb2 import * 
   ...: GetDefaultGatewayResponse(route=Route())                                                                                                                                                 
Out[1]: 
route {
}

What did you see instead?

In [1]: from rpc.network.network_pb2 import * 
   ...: from rpc.network.route_pb2 import * 
   ...: GetDefaultGatewayResponse(route=Route())                                                                                                                                                 
<class 'route_pb2.Route'> <class 'route_pb2.Route'>
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~/venv/lib/python3.7/site-packages/google/protobuf/internal/python_message.py in init(self, **kwargs)
    517         try:
--> 518           copy.MergeFrom(new_val)
    519         except TypeError:

~/venv/lib/python3.7/site-packages/google/protobuf/internal/python_message.py in MergeFrom(self, msg)
   1230           "Parameter to MergeFrom() must be instance of same class: "
-> 1231           'expected %s got %s.' % (cls.__name__, msg.__class__.__name__))
   1232 

TypeError: Parameter to MergeFrom() must be instance of same class: expected Route got Route.

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-1-5aaaa755c5d> in <module>
      1 from rpc.network.network_pb2 import *
      2 from rpc.network.route_pb2 import *
----> 3 GetDefaultGatewayResponse(route=Route())

~/venv/lib/python3.7/site-packages/google/protobuf/internal/python_message.py in init(self, **kwargs)
    518           copy.MergeFrom(new_val)
    519         except TypeError:
--> 520           _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name)
    521         self._fields[field] = copy
    522       else:

~/venv/lib/python3.7/site-packages/google/protobuf/internal/python_message.py in _ReraiseTypeErrorWithFieldName(message_name, field_name)
    446 
    447   # re-raise possibly-amended exception with original traceback:
--> 448   six.reraise(type(exc), exc, sys.exc_info()[2])
    449 
    450 

~/venv/lib/python3.7/site-packages/six.py in reraise(tp, value, tb)
    700                 value = tp()
    701             if value.__traceback__ is not tb:
--> 702                 raise value.with_traceback(tb)
    703             raise value
    704         finally:

~/venv/lib/python3.7/site-packages/google/protobuf/internal/python_message.py in init(self, **kwargs)
    516           new_val = field.message_type._concrete_class(**field_value)
    517         try:
--> 518           copy.MergeFrom(new_val)
    519         except TypeError:
    520           _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name)

~/venv/lib/python3.7/site-packages/google/protobuf/internal/python_message.py in MergeFrom(self, msg)
   1229       raise TypeError(
   1230           "Parameter to MergeFrom() must be instance of same class: "
-> 1231           'expected %s got %s.' % (cls.__name__, msg.__class__.__name__))
   1232 
   1233     assert msg is not self

TypeError: Parameter to MergeFrom() must be instance of same class: expected Route got Route. for field GetDefaultGatewayResponse.route

Make sure you include information that can help us debug (full error message, exception listing, stack trace, logs).

Anything else we should know about your project / environment It is similar to another issue #5272 ?

monouno avatar Apr 22 '20 06:04 monouno

I have the exact same issue on Raspberry Pi with python 3.7.3 and python 3.7.5. Somehow, it is does not happen on my Ubuntu laptop with Python 3.7.5. I use protobuf 3.12.2.

I run the exact same code on both but the isinstance check fails on Raspberry Pi.

The class type name is the same on both Raspberry Pi and my Ubuntu PC, but the class object id is different on Raspberry Pi.

I know python3 is compiled on ARM for Raspberry Pi and compiled for AMD64 on my laptop. Maybe there is a difference there.

XnorTheUnforgiven avatar Jul 22 '20 18:07 XnorTheUnforgiven

met same problems too. The weird thing i met is that, when running the same code on x86 platform, everything works fine. While when running code on aarch platform like jetson tx2 or xavier nx, the problem occurs.

kuonangzhe avatar Sep 24 '20 09:09 kuonangzhe

I had the exact same problem. In my case is simple, I used Linux Apline, the error occured. And if I use Ubuntu, everythin is fine. Python: 3.7 Protobuf: 3.11.3

viviencode avatar Dec 02 '20 15:12 viviencode

There appears to be an assumption baked into the generator code for python at https://github.com/protocolbuffers/protobuf/blob/397d34ca0eb71e2af31881cccb6d8ffcdc3a0ee6/src/google/protobuf/compiler/python/python_generator.cc#L879

This explicitly references the module it expects the generated code to be available under. Combined with how the message classes are constructed dynamically via descriptors, this can result in 2 different object id's for the same message class definition appearing under two different modules.

Taking the example above for route.proto:

package a.b.c;

message Route {
    string id = 1; // uuid, generated by minion
    string dst = 2;
    string via = 3;
    string link_name = 4;
    int32 table = 5;
}

and network.proto:

syntax = "proto3";
package a.b.c;

message GetDefaultGatewayResponse {
    Route route = 1;
}

The generated python code will look something like: file: a/b/c/route_pb2.py

Route = _reflection.GeneratedProtocolMessageType('Route', (_message.Message,), {
  'DESCRIPTOR' : _DEPLOYMENTINFO,
  '__module__' : 'a.b.c.route_pb2'
  # @@protoc_insertion_point(class_scope:a.b.c.Route)
  })
_sym_db.RegisterMessage(Route)

file: a/b/c/network_pb2.py

from a.b.c import route_pb2 as a_dot_b_dot_c_dot_route__pb2

....
....

GetDefaultGatewayResponse = _reflection.GeneratedProtocolMessageType('GetDefaultGatewayResponse', (_message.Message,), {
  'DESCRIPTOR' : _GETDEFAULTGATEWAYRESPONSE,
  '__module__' : 'a.b.c.network_pb2'
  # @@protoc_insertion_point(class_scope:a.b.c.GetDefaultGatewayResponse)
  })
_sym_db.RegisterMessage(GetDefaultGatewayResponse)

Now this all works fine if the files are actually structured such as a is the project name and import a.b.c.network_pb2 is how it is imported.

However if you are placing the files elsewhere, or changing the prefix so that the import path is rpc.network.network_pb2 rather than what is defined as __module__ in the generated code, it appears you end up with two identical classes but different object id's from python's perspective.

Part of what happens is that when from rpc.network.network_pb2 import * is done, it imports a.b.c.route_pb2 rather than rpc.network.route_pb2 and this appears to result in one definition of the class being registered for the module a.b.c.route_pb2. When the subsequent import of rpc.network.route_pb2 occurs in the project code, the Route descriptor ends up being evaluated as a different python class even though they are identical. Because of the hard coding of the module attribute for the classes if you try to print __class__.__module__ you'll see the same, so it looks like you are importing the same module, but they are from different modules.

I uncovered this by a snippet a bit like the following to a unit test that was failing on trying to upgrade to python 3.9.1:

    import sys
    for mod in sys.modules:
        if "route_pb2" in mod:
            print(f"{mod}: {sys.modules[mod]}")
            print(id(sys.modules[mod].Route))

combined with adding the line print(f"expected id: {id(cls)}, got id: {id(msg.__class__)}") to where the exception gets thrown from: https://github.com/protocolbuffers/protobuf/blob/d16bf914bc5ba569d2b70376051d15f68ce4322d/python/google/protobuf/internal/python_message.py#L1318-L1322

I don't know enough of what is happening, I could see that there were 2 different imports appearing in sys.modules for the library I was working with, and the class id was different for each one. That allowed me to work out that if I imported the file that would register and then import using the module path hardcoded in the files would result in using the same ones in all locations.

import rpc.network.route_pb2
from a.b.c.route_pb2 import *
import rpc.network.network_pb2
from a.b.c.network_pb2 import *

It appears once rpc.network.route_pb2 is imported, there is an instance added to sys.modules as 'a.b.c.route' which is used by the other files that are part of the protobuf definition, so importing from a.b.c.route ensures I end up using the exact same and any isinstance checks will pass.

electrofelix avatar Jan 07 '21 16:01 electrofelix

#1491, #4614 and #7470 appear to suggest there is a whole bunch of fun around import paths and python generated code. I suspect the divergence between what the generated code is assuming layout to be absolute and how usage of the resulting python code may assume that it can be placed under a different root to suit application layout or even be able to provide the generated protobuf code as a library is at the core of this issue. Having popped up various times and disappearing when import load orders end up switching slightly in python versions resulting in getting the needed class rather than the alternative definition.

electrofelix avatar Jan 07 '21 19:01 electrofelix

I did some more digging and it turned out our local code had injected additional paths into sys.path. This is why the equivalent of from a.b.c.route_pb2 import * worked.

Given the important of paths in python module imports, and the assumptions about the base package name made by the generated code, which isn't made by the generated golang code, because all files in the same directory (ignoring test files for now) are in the same package which is defined as a simple path component, I think supporting relative path imports will make this class of error go away for use in python. Might also be nice if the __module__ value is correct, but I don't think based on my further digging, that the value stored there is anything more than cosmetic?

electrofelix avatar Jan 11 '21 14:01 electrofelix

I have been facing the same issue, where the CopyFrom or MergeFrom works fine on ubuntu system by has the same error as metioned by OP on a raspberry pi. A workaround i found to work was something like this

    default_response = GetDefaultGatewayResponse()
    route = Route()
    default_response.route.ParseFromString(route.SerializeToString())

devenpatel2 avatar Feb 08 '21 16:02 devenpatel2

Got the same issue on jetson-NX, The mergeFrom function is work on my x86 laptop, but run failed on getson-NX. Any solution?

grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with: status = StatusCode.UNKNOWN details = "Exception calling application: Parameter to MergeFrom() must be instance of same class: expected Value got Value."

LemonAniLabs avatar Mar 26 '21 08:03 LemonAniLabs

The problem is related to isinstance inbuilt function of Python version <3.7 . check what does isinstance function returns for the types of the objects.

sauravkumar-volt avatar Apr 08 '21 17:04 sauravkumar-volt

Today I faced same issue on ARM 64 based hardware (Strange things was its working fine in Ubuntu X86 hardware) but @electrofelix workaround worked perfectly. Workaround was to just import these pb2 , pb2_grpc file from * instead of from a.b.*

gaurav36 avatar Sep 09 '21 12:09 gaurav36

Had the same issue, in my case, VSCode added automatically an import, while our codebase usually adds the path to sys paths, so full path is not required. This resulted in code importing same proto file in 2 modes: from blabla_pb2 import DefinedProtoObj while in another file: from module1.module2.blabla_pb2 import DefinedProtoObj

octavflorescu avatar Oct 14 '22 12:10 octavflorescu