How to decorate (monkeypatch...) a Python class with methods from another class?

1.5k views Asked by At

Both httplib.HTTPMessage and email.message.Message classes[1] implements methods for RFC822 headers parsing. Unfortunately, they have different implementations[2] and they do not provide the same level of functionality.

One example that is bugging me is that:

  • httplib.HTTPMessage is missing the get_filename method present in email.Message, that allows you to easily retrieve the filename from a Content-disposition: attachment; filename="fghi.xyz" header;

  • httplib.HTTPMessage has getparam, getplist and parseplist methods but AFAIK, they are not and cannot be used outside of the content-type header parsing;

  • email.Message has a generic get_param method to parse any RFC822 header with parameters, such as content-disposition or content-type.

Thus, I want the get_filename or get_param methods of email.message.Message in httplib.HTTPMessage but, of course, I can't patch httplib.HTTPMessage as it's in the standard library... :-q

And finally, here comes the decorator subject... :-)

I succesfully created a monkeypatch_http_message function to decorate an httplib.HTTPMessage with my missing parsing methods:

def monkeypatch_http_message(obj):
    from email import utils
    from email.message import (
        _parseparam,
        _unquotevalue,
    )
    cls = obj.__class__

    # methods **copied** from email.message.Message source code
    def _get_params_preserve(self, failobj, header): ...
    def get_params(self, failobj=None, header='content-type', 
                   unquote=True): ...
    def get_param(self, param, failobj=None, header='content-type', 
                  unquote=True): ...
    def get_filename(self, failobj=None): ...

    # monkeypatching httplib.Message
    cls._get_params_preserve = _get_params_preserve
    cls.get_params = get_params
    cls.get_param = get_param
    cls.get_filename = get_filename

Now I can do:

import mechanize
from some.module import monkeypatch_http_message
browser = mechanize.Browser()

# in that form, browser.retrieve returns a temporary filename 
# and an httplib.HTTPMessage instance
(tmp_filename, headers) = browser.retrieve(someurl) 

# monkeypatch the httplib.HTTPMessage instance
monkeypatch_http_message(headers)

# yeah... my original filename, finally
filename = headers.get_filename()

The issue here is that I literally copied the decorating methods code from the source class, which I'd like to avoid.

So, I tried decorating by referencing the source methods:

def monkeypatch_http_message(obj):
    from email import utils
    from email.message import (
        _parseparam,
        _unquotevalue,
        Message    # XXX added
    )
    cls = obj.__class__

    # monkeypatching httplib.Message
    cls._get_params_preserve = Message._get_params_preserve
    cls.get_params = Message.get_params
    cls.get_param = Message.get_param
    cls.get_filename = Message.get_filename

But that gives me:

Traceback (most recent call last):
  File "client.py", line 224, in <module>
    filename = headers.get_filename()
TypeError: unbound method get_filename() must be called with Message instance as first argument (got nothing instead)

I'm scratching my head now... how can I decorate my class without literally copying the source methods ?

Any suggestions ? :-)

Regards,

Georges Martin


  1. In Python 2.6. I can't use 2.7 nor 3.x in production.

  2. httplib.HTTPMessage inherits from mimetools.Message and rfc822.Message while email.Message has its own implementation.

2

There are 2 answers

1
ncoghlan On BEST ANSWER

In Python 3.x, unbound methods go away so you'll just get the file objects in this case and your second example will work:

>>> class C():
...   def demo(): pass
... 
>>> C.demo
<function demo at 0x1fed6d8>

In Python 2.x, you can either access the underlying function via the unbound method or by retrieving it directly from the class dictionary (thus bypassing the normal lookup process that turns it into an unbound method):

>>> class C():
...   def demo(): pass
... 
>>> C.demo.im_func                  # Retrieve it from the unbound method
<function demo at 0x7f463486d5f0>
>>> C.__dict__["demo"]              # Retrieve it directly from the class dict
<function demo at 0x7f463486d5f0>

The latter approach has the benefit of being forward compatible with Python 3.x.

1
Georges Martin On

@ncoghlan: I can't put indented code in comments, so here it is again:

def monkeypatch_http_message(obj):
    import httplib
    assert isinstance(obj, httplib.HTTPMessage)
    cls = obj.__class__

    from email import utils
    from email.message import (_parseparam, _unquotevalue, Message)
    funcnames = ('_get_params_preserve', 'get_params', 'get_param', 'get_filename')
    for funcname in funcnames:
        cls.__dict__[funcname] = Message.__dict__[funcname]

Thanks ! :-)