Repository: mitmproxy/mitmproxy

Issue:
Addons: TypeError: unexpected object
#### Problem Description
I'm trying to set up [zerorpc](https://github.com/0rpc/zerorpc-python) inside an addon. It does work but mitmproxy spits out a stack trace when it encounters a reference to an instance of `zerorpc.Client` anywhere.

#### Steps to reproduce the behavior:
1. `pipx inject mitmproxy zerorpc`
2. `mitmdump --scripts addon.py`

```python
import zerorpc

class MyAddon:
  def __init__(self):
    self.rpc = zerorpc.Client()

addons = [
  MyAddon()
]
```

```
Addon error: Traceback (most recent call last):
  File "/home/alex/.local/pipx/venvs/mitmproxy/lib/python3.7/site-packages/mitmproxy/addonmanager.py", line 42, in safecall
    yield
  File "/home/alex/.local/pipx/venvs/mitmproxy/lib/python3.7/site-packages/mitmproxy/addons/script.py", line 102, in loadscript
    ctx.master.addons.register(ns)
  File "/home/alex/.local/pipx/venvs/mitmproxy/lib/python3.7/site-packages/mitmproxy/addonmanager.py", line 161, in register
    self.master.commands.collect_commands(a)
  File "/home/alex/.local/pipx/venvs/mitmproxy/lib/python3.7/site-packages/mitmproxy/command.py", line 158, in collect_commands
    self.add(o.command_name, o)
  File "/home/alex/.local/pipx/venvs/mitmproxy/lib/python3.7/site-packages/mitmproxy/command.py", line 165, in add
    self.commands[path] = Command(self, path, func)
  File "/home/alex/.local/pipx/venvs/mitmproxy/lib/python3.7/site-packages/mitmproxy/command.py", line 64, in __init__
    self.signature = inspect.signature(self.func)
  File "/usr/lib/python3.7/inspect.py", line 3083, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
  File "/usr/lib/python3.7/inspect.py", line 2833, in from_callable
    follow_wrapper_chains=follow_wrapped)
  File "/usr/lib/python3.7/inspect.py", line 2246, in _signature_from_callable
    'attribute'.format(sig))
TypeError: unexpected object <function ClientBase.__getattr__.<locals>.<lambda> at 0x7f66616d0b00> in __signature__ attribute

```

the absolute minimal example is this, which does not error when run directly outside of mitmproxy but does when used as an addon

```python
import zerorpc
c = zerorpc.Client()
```

I don't know enough Python to understand the underlying issue. I assume there is nothing wrong with zerorpc but with the assumptions mitmproxy makes about stuff that addons expose?


#### System Information

```
Mitmproxy: 5.0.1
Python:    3.7.5
OpenSSL:   OpenSSL 1.1.0j  20 Nov 2018
Platform:  Linux-5.3.0-26-generic-x86_64-with-Ubuntu-19.10-eoan
```



Discussion:
For now I've worked around it by lazily creating an instance. I can't even use `rpcPlease()` inside `__init__`. Even without assigning it to a variable.

This works:

```python
import mitmproxy.websocket
import zerorpc

_rpc = None

def rpcPlease():
  global _rpc
  if _rpc is None:
    _rpc = zerorpc.Client()
    _rpc.connect("tcp://127.0.0.1:4242")
  return _rpc

class MyAddon:
  # def __init__(self):
  #   print(rpcPlease().hello("__init__"))

  def websocket_message(self, flow: mitmproxy.websocket.WebSocketFlow):
    rpc = rpcPlease()
    print(rpc.hello("websocket_message"))

addons = [
  MyAddon()
]
```

this does not (I only uncommented the two lines)

```python
import mitmproxy.websocket
import zerorpc

_rpc = None

def rpcPlease():
  global _rpc
  if _rpc is None:
    _rpc = zerorpc.Client()
    _rpc.connect("tcp://127.0.0.1:4242")
  return _rpc

class MyAddon:
  def __init__(self):
    print(rpcPlease().hello("__init__"))

  def websocket_message(self, flow: mitmproxy.websocket.WebSocketFlow):
    rpc = rpcPlease()
    print(rpc.hello("websocket_message"))

addons = [
  MyAddon()
]
```
Since zerorpc is not thread safe I've switched to gRPC and the issue is gone (I can have a global channel and stub instance without any problem). So maybe it was triggering an edge case that nobody else will ever encounter again. I'll leave this open and let you decide to close it. Maybe if one of you peeks at the stack trace above they'll know right away what was happening.

Resolving patch:
diff --git a/mitmproxy/command.py b/mitmproxy/command.py
--- a/mitmproxy/command.py
+++ b/mitmproxy/command.py
@@ -149,9 +149,10 @@ def collect_commands(self, addon):
             if not i.startswith("__"):
                 o = getattr(addon, i)
                 try:
-                    is_command = hasattr(o, "command_name")
+                    # hasattr is not enough, see https://github.com/mitmproxy/mitmproxy/issues/3794
+                    is_command = isinstance(getattr(o, "command_name", None), str)
                 except Exception:
-                    pass  # hasattr may raise if o implements __getattr__.
+                    pass  # getattr may raise if o implements __getattr__.
                 else:
                     if is_command:
                         try:


Test patch: