4

I'm using the python-mpd2 module to control a media player on a Raspberry Pi in a GUI application. Thus, I'd like to gracefully handle connection errors and timeouts (the player in question drops MPD connections after 60 seconds) in the background. However, the MPD module has no single point of entry through which all commands are sent or information is retrieved that I could patch.

I'd like a class which allows access to all of the same methods as mpd.MPDClient, but let's me add my own error handling. In other words, if I do:

client.play()

And a connectione error is thrown, I'd like to catch it and resend the same command. Other than the small delay caused by having to reconnect to the server, the user shouldn't notice that anything is amiss.

So far, here is the solution I've come up with. It is working in my application, but doesn't really fulfill my objectives.

from functools import partial
from mpd import MPDClient, ConnectionError

class PersistentMPDClient(object):
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.client = MPDClient()
        self.client.connect(self.host, self.port)

    def command(self, cmd, *args, **kwargs):
        command_callable = partial(self.client.__getattribute__(cmd), *args, **kwargs)
        try:
            return command_callable()
        except ConnectionError:
            # Mopidy drops our connection after a while, so reconnect to send the command
            self.client._soc = None
            self.client.connect(self.host, self.port)
            return command_callable()

I could add a method to this class for every single MPD command, e.g.:

def play(self):
    return self.command("play")

But this seems far from the best way to accomplish it.

shx2
  • 61,779
  • 13
  • 130
  • 153
James Scholes
  • 7,686
  • 3
  • 19
  • 20
  • how many *commands* are there to handle? – shx2 Apr 12 '14 at 17:52
  • There are 91 commands. Not all of them are necessary or will be used in my app, but a good portion of them are. – James Scholes Apr 12 '14 at 17:55
  • If you don't mind creating a list of all 91 strings forming *command names*, you can do something along the lines of [this answer](http://stackoverflow.com/a/534597/2096752). I believe this approach has many advantages because it involves less magic. OTOH, 91 is indeed a lot, so a more magical `__getattr__`-based solution might be more appropriate. – shx2 Apr 12 '14 at 18:15

1 Answers1

4

If you don't mind creating a list of all 91 strings forming command names, you can do something along the lines of this answer. I believe this approach has many advantages because it involves less magic.

OTOH, 91 is indeed a lot. So here's an automagical solution, making use of a custom __getattr__, which returns a wrapper:

from functools import partial
import types

class DummyClient(object):
    def connect(self, *a, **kw): print 'connecting %r %r' % (a, kw)
    def play(self): print 'playing'
    def stop(self): print 'stopping'

class PersistentMPDClient(object):
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.client = DummyClient()
        self.client.connect(self.host, self.port)

    def __getattr__(self, attr, *args):
        cmd = getattr(self.client, attr, *args)
        if isinstance(cmd, types.MethodType):
            # a method -- wrap
            return lambda *a, **kw: self.command(attr, *a, **kw)
        else:
            # anything else -- return unchanged
            return cmd

    def command(self, cmd, *args, **kwargs):
        command_callable = partial(self.client.__getattribute__(cmd), *args, **kwargs)
        try:
            return command_callable()
        except ConnectionError:
            # Mopidy drops our connection after a while, so reconnect to send the command
            self.client._soc = None
            self.client.connect(self.host, self.port)
            return command_callable()

c = PersistentMPDClient(hostname, port)
c.play()
c.stop()

As I was writing this up, I noticed @MatToufoutu had posted a similar solution (there are some differences, though). I don't know why s/he deleted it... If that answer gets undeleted, I'd gladly give it the credit it deserves.

Community
  • 1
  • 1
shx2
  • 61,779
  • 13
  • 130
  • 153
  • In fact, I deleted my answer because this approach doesn't allow to perform error handling when the `command` method is called (which the OP wants to). Making error handling possible makes things a bit more complicated, but another answer is coming :) – mdeous Apr 12 '14 at 18:33
  • 1
    my bad, got things wrong (saturday, you know ^^), though, your answer is more clever that what I had posted earlier (type checking and arguments handling), I'll leave it deleted ;) – mdeous Apr 12 '14 at 18:38
  • @MatToufoutu as you wish – shx2 Apr 12 '14 at 18:49
  • 1
    The python-mpd2 module contains a dictionary of command names I could import and use, however your proposed solution is working like a charm for the moment. Many thanks. – James Scholes Apr 13 '14 at 12:42