[Pkg-mpd-commits] [python-mpd] 190/262: Imported Upstream version 0.5.0

Simon McVittie smcv at debian.org
Sun May 22 18:16:45 UTC 2016


This is an automated email from the git hooks/post-receive script.

smcv pushed a commit to branch upstream
in repository python-mpd.

commit e15cafd8d19b881f036c67aa0838fe9d20f2b1bc
Author: kaliko <efrim at azylum.org>
Date:   Thu Jan 31 13:53:41 2013 +0100

    Imported Upstream version 0.5.0
---
 .gitignore                 |  24 ++++
 .travis.yml                |   7 +
 CHANGES.txt => CHANGES.rst |  19 +++
 PORTING.rst                |  44 +++++++
 README.md                  | 202 ----------------------------
 README.rst                 | 259 ++++++++++++++++++++++++++++++++++++
 examples/locking.py        |  49 +++++++
 examples/logger.py         |   5 +
 mpd.py                     | 103 ++++++++++++---
 setup.py                   |  24 +++-
 test.py                    | 318 +++++++++++++++++++++++++++++++--------------
 tox.ini                    |  11 ++
 12 files changed, 748 insertions(+), 317 deletions(-)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dafb4dd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+*.py[cod]
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+lib
+lib64
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+nosetests.xml
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..479f1cf
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+language: python
+
+python:
+  - 2.7
+  - 3.2
+
+script: python test.py
diff --git a/CHANGES.txt b/CHANGES.rst
similarity index 82%
rename from CHANGES.txt
rename to CHANGES.rst
index 33e92cd..92b7992 100644
--- a/CHANGES.txt
+++ b/CHANGES.rst
@@ -1,6 +1,25 @@
 python-mpd2 Changes List
 ========================
 
+Changes in 0.5.0
+----------------
+* improved support for sticker
+
+Changes in 0.4.6
+----------------
+* enforce utf8 for encoding/decoding in python3
+
+Changes in 0.4.5
+----------------
+* support for logging
+
+Changes in 0.4.4
+----------------
+
+* fix cleanup after broken connection
+* deprecate timeout parameter added in v0.4.2
+* add timeout and idletimeout property
+
 Changes in 0.4.3
 ----------------
 
diff --git a/PORTING.rst b/PORTING.rst
new file mode 100644
index 0000000..64b4c78
--- /dev/null
+++ b/PORTING.rst
@@ -0,0 +1,44 @@
+=============
+Porting guide
+=============
+
+Until the versions 0.4.x, `python-mpd2`_ was a drop-in replacement for application
+which were using the original `python-mpd`_. That is, you could just replace the
+package's content of the latter one by the former one, and *things should just
+work*.
+
+However, starting from version 0.5, `python-mpd2`_ provides enhanced features
+which are *NOT* backward compatibles with the original `python-mpd`_ package.
+This goal of this document is to explains the differences the releases and if it
+makes sense, how to migrate from one version to another.
+
+
+Stickers API
+============
+
+When fetching stickers, `python-mpd2`_ used to return mostly the raw results MPD
+was providing::
+
+    >>> client.sticker_get('song', 'foo.mp3', 'my-sticker')
+    'my-sticker=some value'
+    >>> client.sticker_list('song', 'foo.mp3')
+    ['my-sticker=some value', 'foo=bar']
+
+Starting from version 0.5, `python-mpd2`_ provides a higher-level representation
+of the stickers' content::
+
+    >>> client.sticker_get('song', 'foo.mp3', 'my-sticker')
+    'some value'
+    >>> client.sticker_list('song', 'foo.mp3')
+    {'my-sticker': 'some value', 'foo': 'bar'}
+
+This removes the burden from the application to do the interpretation of the
+stickers' content by itself.
+
+.. versionadded:: 0.5
+
+
+.. _python-mpd: http://jatreuman.indefero.net/p/python-mpd/
+.. _python-mpd2: https://github.com/Mic92/python-mpd2/
+
+.. vim:ft=rst
diff --git a/README.md b/README.md
deleted file mode 100644
index 360f9e1..0000000
--- a/README.md
+++ /dev/null
@@ -1,202 +0,0 @@
-python-mpd2
-===========
-
-Difference with python-mpd
---------------------------
-
-python-mpd2 is a fork of the python-mpd.
-It is backward compatible to python-mpd, so it could act as drop-in replacement
-(tested with [sonata](http://sonata.berlios.de/)).
-
-Current features list:
-
- - python3 support (python2.6 is minimum python version required)
- - support for the upcoming client-to-client protocol
- - adding new commands of mpd v0.17 (seekcur, prio, prioid, config, searchadd,
-   searchaddpl)
- - remove of deprecated commands (volume)
- - declare mpd commands explicit as method, so they are shown in ipython
- - add unit tests
- - documented API to add new commands (see Future Compatible)
- - use unicode strings in all commands (optionally in python2, default in python3 - see Unicode Handling)
-
-If you like this module, you could try contact the original author <jat at spatialrift.net> or
-join the discussion on the [issue tracker](http://jatreuman.indefero.net/p/python-mpd/issues/7/)
-
-Getting the latest source code
-------------------------------
-
-If you would instead like to use the latest source code, you can grab a copy
-of the development version from git by running the command:
-
-    $ git clone git://github.com/Mic92/python-mpd2.git
-
-
-Installing from source
-----------------------
-
-To install python-mpd from source, simply run the command:
-
-    $ python setup.py install
-
-You can use the *--help* switch to *setup.py* for a complete list of commands
-and their options.  See the [Installing Python Modules](http://docs.python.org/inst/inst.html) document for more details.
-
-
-Getting the latest release
---------------------------
-
-This python-mpd2 can be found on [pypi](http://pypi.python.org/pypi?:action=display&name=python-mpd2)
-
-###pypi:
-
-    $ pip install python-mpd2
-
-
-Until linux distributions adapt this package, here are some ready to use packages to test your applications:
-
-### Debian
-
-Drop this line in */etc/apt/sources.list.d/python-mpd2.list*:
-
-    deb http://sima.azylum.org/debian unstable main
-
-Import the gpg key as root
-
-    $ wget -O - http://sima.azylum.org/sima.gpg | apt-key add -
-
-Key fingerprint :
-
-2255 310A D1A2 48A0 7B59  7638 065F E539 32DC 551D
-
-Controls with *apt-key finger*.
-
-Then simply update/install *python-mpd2* or *python3-mpd* with apt or aptitude:
-
-### Arch Linux
-
-install [python-mpd2-git](https://aur.archlinux.org/packages.php?ID=57738) from AUR
-
-### Gentoo Linux
-
-An ebuid is available in the `laurentb` overlay.
-
-    echo 'dev-python/python-mpd::laurentb **' >> /etc/portage/accept_keywords
-    layman -a laurentb
-    emerge -av python-mpd
-
-
-Packages for other distributions are welcome!
-
-
-Using the client library
-------------------------
-
-The client library can be used as follows:
-
-```python
-client = mpd.MPDClient()           # create client object
-client.connect("localhost", 6600,  # connect to localhost:6600
-                timeout=10)        # optional timeout in seconds (floats allowed), default: None
-print(client.mpd_version)          # print the mpd version
-print(client.find("any", "house")) # print result of the command "find any house"
-client.close()                     # send the close command
-client.disconnect()                # disconnect from the server
-```
-
-A list of supported commands, their arguments (as MPD currently understands
-them), and the functions used to parse their responses can be found in
-*doc/commands.txt*.  See the [MPD protocol documentation](http://www.musicpd.org/doc/protocol/) for more details.
-
-Command lists are also supported using *command_list_ok_begin()* and
-*command_list_end()*:
-
-```python
-client.command_list_ok_begin()       # start a command list
-client.update()                      # insert the update command into the list
-client.status()                      # insert the status command into the list
-results = client.command_list_end()  # results will be a list with the results
-```
-
-Commands may also return iterators instead of lists if *iterate* is set to
-*True*:
-
-```python
-client.iterate = True
-for song in client.playlistinfo():
-    print song["file"]
-```
-
-Each command have a *send_* and a *fetch_* variant, which allows to send a
-mpd command and the fetch the result later. This is useful for the idle
-command:
-
-```python
-client.send_idle()
-# do something else ...
-events = client.fetch_idle()
-```
-
-Some more complex usage example can be found [here](http://jatreuman.indefero.net/p/python-mpd/doc/)
-
-
-Unicode Handling
-----------------
-To quote the mpd protocol documentation:
-
-> All data to be sent between the client and server must be encoded in UTF-8.
-
-In python3 unicode strings are default string type. So just pass these strings as arguments for mpd commands.
-
-For backward compatibility with python-mpd the python2 version accept both unicode strings (ex. u"♥") and unicode encoded 8-bit strings (ex. "♥").
-It returns unicode encoded strings by default for the same reason.
-
-Using unicode strings should be prefered as it makes the transition to python3 easier.
-This way, decoding and encoding strings outside the library, is not needed to make function like len() behave correctly.
-To make MPDClient return unicode strings in python2 create the instance with the use_unicode parameter set to true.
-
-```python
->>> import mpd
->>> client = MPDClient(use_unicode=True)
->>> client.urlhandlers()[0]
-u'http'
->>> client.use_unicode = False # Can be switched back later
->>> client.urlhandlers()[0]
-'http'
-```
-
-Use this option in python3 doesn't have an effect.
-
-Future Compatible
------------------
-
-New commands or special handling of commands can be easily implemented.
-Use *add_command()* or *remove_command()* to modify the commands of the
-*MPDClient* class and all its instances.
-
-```python
-def fetch_cover(client):
-    """"Take a MPDClient instance as its arguments and return mimetype and image"""
-    # this command may come in the future.
-    pass
-self.client.add_command("get_cover", fetch_cover)
-# remove the command, because it doesn't exist already.
-self.client.remove_command("get_cover")
-```
-
-Known Issues
-------------
-
-Currently python-mpd is **NOT** thread-safe. If you need to access the library from multiple threads, you have to either use [locks](http://docs.python.org/library/threading.html#lock-objects) or use one mpd client per thread.
-
-
-Contacting the author
----------------------
-
-Just connect me (Mic92) on github or via email (jthalheim at gmail.com).
-
-Usually I hang around on jabber: sonata at conference.codingteam.net
-
-You can contact the original author by emailing J. Alexander Treuman <jat at spatialrift.net>.
-
-He can also be found idling in #mpd on irc.freenode.net as jat.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..3162c83
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,259 @@
+python-mpd2
+===========
+
+.. image:: https://travis-ci.org/Mic92/python-mpd2.png?branch=master
+    :target: http://travis-ci.org/Mic92/python-mpd2
+    :alt: Build Status
+
+*python-mpd2* is a Python library which provides a client interface for
+the `Music Player Daemon <http://musicpd.org>`_.
+
+Difference with python-mpd
+--------------------------
+
+python-mpd2 is a fork of
+`python-mpd <http://jatreuman.indefero.net/p/python-mpd/>`_. 
+python-mpd2 is a fork of `python-mpd`_. While 0.4.x was backwards compatible
+with python-mpd, starting with 0.5 provides enhanced features
+which are *NOT* backward compatibles with the original `python-mpd`_ package.
+(see PORTING.txt for more information)
+
+The following features were added:
+
+-  Python 3 support (but you neead at least Python 2.6)
+-  support for the upcoming client-to-client protocol
+-  support for new commands from MPD v0.17 (seekcur, prio, prioid,
+   config, searchadd, searchaddpl)
+-  remove deprecated commands (volume)
+-  explicitly declared MPD commands (which is handy when using for
+   example `IPython <http://ipython.org>`_)
+-  a test suite
+-  API documentation to add new commands (see `Future
+   Compatible <#future-compatible>`_)
+-  support for Unicode strings in all commands (optionally in python2,
+   default in python3 - see `Unicode Handling <#unicode-handling>`_)
+-  configureable timeouts
+-  support for `logging <#logging>`_
+-  improved support for sticker
+
+If you like this module, you could try contact the original author
+jat at spatialrift.net or join the discussion on the `issue
+tracker <http://jatreuman.indefero.net/p/python-mpd/issues/7/>`_ so that
+it gets merged upstream.
+
+Getting the latest source code
+------------------------------
+
+If you would like to use the latest source code, you can grab a
+copy of the development version from Git by running the command::
+
+    $ git clone git://github.com/Mic92/python-mpd2.git
+
+Installing from source
+----------------------
+
+To install *python-mpd2* from source, simply run the command::
+
+    $ python setup.py install
+
+You can use the *--help* switch to *setup.py* for a complete list of
+commands and their options. See the `Installing Python
+Modules <http://docs.python.org/inst/inst.html>`_ document for more
+details.
+
+Getting the latest release
+--------------------------
+
+The latest stable release of *python-mpd2* can be found on
+`PyPI <http://pypi.python.org/pypi?:action=display&name=python-mpd2>`_
+
+PyPI:
+~~~~~
+
+::
+
+    $ pip install python-mpd2
+
+Until Linux distributions adapt this package, here are some ready to use
+packages to test your applications:
+
+Debian
+~~~~~~
+
+Drop this line in */etc/apt/sources.list.d/python-mpd2.list*::
+
+    deb http://sima.azylum.org/debian unstable main
+
+Import the gpg key as root::
+
+    $ wget -O - http://sima.azylum.org/sima.gpg | apt-key add -
+
+Key fingerprint::
+
+    2255 310A D1A2 48A0 7B59  7638 065F E539 32DC 551D
+
+Controls with *apt-key finger*.
+
+Then simply update/install *python-mpd2* or *python3-mpd* with apt or
+aptitude:
+
+Arch Linux
+~~~~~~~~~~
+
+Install `python-mpd2 <http://aur.archlinux.org/packages.php?ID=59276>`_
+from AUR.
+
+Gentoo Linux
+~~~~~~~~~~~~
+
+Replaces the original python-mpd beginning with version 0.4.2::
+
+    echo dev-python/python-mpd >> /etc/portage/accept_keywords
+    emerge -av python-mpd
+
+Packages for other distributions are welcome!
+
+Using the client library
+------------------------
+
+The client library can be used as follows::
+
+    client = mpd.MPDClient()           # create client object
+    client.timeout = 10                # network timeout in seconds (floats allowed), default: None
+    client.idletimeout = None          # timeout for fetching the result of the idle command is handled seperately, default: None
+    client.connect("localhost", 6600)  # connect to localhost:6600
+    print(client.mpd_version)          # print the MPD version
+    print(client.find("any", "house")) # print result of the command "find any house"
+    client.close()                     # send the close command
+    client.disconnect()                # disconnect from the server
+
+A list of supported commands, their arguments (as MPD currently
+understands them), and the functions used to parse their responses can
+be found in *doc/commands.txt*. See the `MPD protocol
+documentation <http://www.musicpd.org/doc/protocol/>`_ for more details.
+
+Command lists are also supported using *command\_list\_ok\_begin()* and
+*command\_list\_end()*::
+
+    client.command_list_ok_begin()       # start a command list
+    client.update()                      # insert the update command into the list
+    client.status()                      # insert the status command into the list
+    results = client.command_list_end()  # results will be a list with the results
+
+Commands may also return iterators instead of lists if *iterate* is set
+to *True*::
+
+    client.iterate = True
+    for song in client.playlistinfo():
+        print song["file"]
+
+Each command have a *send\_* and a *fetch\_* variant, which allows to
+send a MPD command and then fetch the result later. This is useful for
+the idle command::
+
+    client.send_idle()
+    # do something else or use function like select(): http://docs.python.org/howto/sockets.html#non-blocking-sockets
+    # ex. select([client], [], []) or with gobject: http://jatreuman.indefero.net/p/python-mpd/page/ExampleIdle/
+    events = client.fetch_idle()
+
+Some more complex usage examples can be found
+`here <http://jatreuman.indefero.net/p/python-mpd/doc/>`_
+
+Unicode Handling
+----------------
+
+To quote the mpd protocol documentation:
+
+> All data to be sent between the client and server must be encoded in UTF-8.
+
+With Python 3:
+~~~~~~~~~~~~~~
+
+In Python 3, Unicode string is the default string type. So just pass
+these strings as arguments for MPD commands and *python-mpd2* will also
+return such Unicode string.
+
+With Python 2.x
+~~~~~~~~~~~~~~~
+
+For backward compatibility with *python-mpd*, when running with Python
+2.x, *python-mpd2* accepts both Unicode strings (ex. u"♥") and UTF-8
+encoded strings (ex. "♥").
+
+In order for *MPDClient* to return Unicode strings with Python 2, create
+the instance with the ``use_unicode`` parameter set to ``True``.
+
+Using Unicode strings should be prefered as it is done transparently by
+the library for you, and makes the transition to Python 3 easier.
+
+``python >>> import mpd >>> client = MPDClient(use_unicode=True) >>> client.urlhandlers()[0] u'http' >>> client.use_unicode = False # Can be switched back later >>> client.urlhandlers()[0] 'http'``
+Using this option in Python 3 doesn't have any effect.
+
+Logging
+-------
+
+By default messages are sent to the logger named ``mpd``::
+
+    >>> import logging, mpd
+    >>> logging.basicConfig(level=logging.DEBUG)
+    >>> client = mpd.MPDClient()
+    >>> client.connect("localhost", 6600)
+    INFO:mpd:Calling MPD connect('localhost', 6600, timeout=None)
+    >>> client.find('any', 'dubstep')
+    DEBUG:mpd:Calling MPD find('any', 'dubstep')
+
+For more information about logging configuration, see
+http://docs.python.org/2/howto/logging.html
+
+Future Compatible
+-----------------
+
+New commands or special handling of commands can be easily implemented.
+Use ``add_command()`` or ``remove_command()`` to modify the commands of
+the *MPDClient* class and all its instances.::
+
+    def fetch_cover(client):
+        """"Take a MPDClient instance as its arguments and return mimetype and image"""
+        # this command may come in the future.
+        pass
+
+    self.client.add_command("get_cover", fetch_cover)
+    # you can then use:
+    self.client.fetch_cover()
+
+    # remove the command, because it doesn't exist already.
+    self.client.remove_command("get_cover")
+
+Thread-Safety
+-------------
+
+Currently ``MPDClient`` is **NOT** thread-safe. As it use a socket
+internaly, only one thread can send or receive at the time.
+
+But ``MPDClient`` can be easily extended to be thread-safe using
+`locks <http://docs.python.org/library/threading.html#lock-objects>`_.
+Take a look at ``examples/locking.py`` for further informations.
+
+Testing
+-------
+
+Just run::
+
+    $ python setup.py test
+
+This will install `Tox <http://tox.testrun.org/>`_.
+Tox will take care of testing against all the supported Python versions (at least available) on our computer, with the required dependencies
+
+Contacting the author
+---------------------
+
+Just contact me (Mic92) on Github or via email (joerg at higgsboson.tk).
+
+Usually I hang around on Jabber: sonata at conference.codingteam.net
+
+You can contact the original author by emailing J. Alexander Treuman
+jat at spatialrift.net.
+
+He can also be found idling in #mpd on irc.freenode.net as jat.
+
+.. |Build Status| image:: https://travis-ci.org/Mic92/python-mpd2.png
diff --git a/examples/locking.py b/examples/locking.py
new file mode 100644
index 0000000..d6bb916
--- /dev/null
+++ b/examples/locking.py
@@ -0,0 +1,49 @@
+from threading import Lock, Thread
+from random import choice
+from mpd import MPDClient
+
+class LockableMPDClient(MPDClient):
+    def __init__(self, use_unicode=False):
+        super(LockableMPDClient, self).__init__()
+        self.use_unicode = use_unicode
+        self._lock = Lock()
+    def acquire(self):
+        self._lock.acquire()
+    def release(self):
+        self._lock.release()
+    def __enter__(self):
+        self.acquire()
+    def __exit__(self, type, value, traceback):
+        self.release()
+
+client = LockableMPDClient()
+client.connect("localhost", 6600)
+# now whenever you need thread-safe access
+# use the 'with' statement like this:
+with client: # acquire lock
+    status = client.status()
+# if you leave the block, the lock is released
+# it is recommend to leave it soon,
+# otherwise your other threads will blocked.
+
+# Let's test if it works ....
+def fetch_playlist():
+    for i in range(10):
+        if choice([0, 1]) == 0:
+            with client:
+                song = client.currentsong()
+            assert isinstance(song, dict)
+        else:
+            with client:
+                playlist = client.playlist()
+            assert isinstance(playlist, list)
+
+threads = []
+for i in range(5):
+    t = Thread(target=fetch_playlist)
+    threads.append(t)
+    t.start()
+for t in threads:
+    t.join()
+
+print("Done...")
diff --git a/examples/logger.py b/examples/logger.py
new file mode 100644
index 0000000..ee48fe8
--- /dev/null
+++ b/examples/logger.py
@@ -0,0 +1,5 @@
+import logging, mpd
+logging.basicConfig(level=logging.DEBUG)
+client = mpd.MPDClient()
+client.connect("localhost", 6600)
+client.find("any", "house")
diff --git a/mpd.py b/mpd.py
index de73609..c93b4bd 100644
--- a/mpd.py
+++ b/mpd.py
@@ -15,10 +15,13 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with python-mpd2.  If not, see <http://www.gnu.org/licenses/>.
 
+import logging
 import sys
 import socket
+import warnings
 from collections import Callable
 
+VERSION = (0, 5, 0)
 HELLO_PREFIX = "OK MPD "
 ERROR_PREFIX = "ACK "
 SUCCESS = "OK"
@@ -32,6 +35,16 @@ else:
     decode_str = lambda s: s
     encode_str = lambda s: str(s)
 
+try:
+    from logging import NullHandler
+except ImportError: # NullHandler was introduced in python2.7
+    class NullHandler(logging.Handler):
+        def emit(self, record):
+            pass
+
+logger = logging.getLogger(__name__)
+logger.addHandler(NullHandler())
+
 class MPDError(Exception):
     pass
 
@@ -65,7 +78,7 @@ _commands = {
     # Status Commands
     "clearerror":         "_fetch_nothing",
     "currentsong":        "_fetch_object",
-    "idle":               "_fetch_list",
+    "idle":               "_fetch_idle",
     "noidle":             None,
     "status":             "_fetch_object",
     "stats":              "_fetch_object",
@@ -136,10 +149,10 @@ _commands = {
     "update":             "_fetch_item",
     "rescan":             "_fetch_item",
     # Sticker Commands
-    "sticker get":        "_fetch_item",
+    "sticker get":        "_fetch_sticker",
     "sticker set":        "_fetch_nothing",
     "sticker delete":     "_fetch_nothing",
-    "sticker list":       "_fetch_list",
+    "sticker list":       "_fetch_stickers",
     "sticker find":       "_fetch_songs",
     # Connection Commands
     "close":              None,
@@ -223,6 +236,12 @@ class MPDClient(object):
         parts = [command]
         for arg in args:
             parts.append('"%s"' % escape(encode_str(arg)))
+        # Minimize logging cost if the logging is not activated.
+        if logger.isEnabledFor(logging.DEBUG):
+            if command == "password":
+                logger.debug("Calling MPD password(******)")
+            else:
+                logger.debug("Calling MPD %s%r", command, args)
         self._write_line(" ".join(parts))
 
     def _read_line(self):
@@ -230,6 +249,7 @@ class MPDClient(object):
         if self.use_unicode:
             line = decode_str(line)
         if not line.endswith("\n"):
+            self.disconnect()
             raise ConnectionError("Connection lost while reading line")
         line = line.rstrip("\n")
         if line.startswith(ERROR_PREFIX):
@@ -299,6 +319,15 @@ class MPDClient(object):
             self._command_list = None
         self._fetch_nothing()
 
+    def _read_stickers(self):
+        for key, sticker in self._read_pairs():
+            value = sticker.split('=', 1)
+
+            if len(value) < 2:
+                raise ProtocolError("Could not parse sticker: %r" % sticker)
+
+            yield tuple(value)
+
     def _iterator_wrapper(self, iterator):
         try:
             for item in iterator:
@@ -323,6 +352,14 @@ class MPDClient(object):
             return
         return pairs[0][1]
 
+    def _fetch_sticker(self):
+        # Either we get one or we get an error while reading the line
+        key, value = list(self._read_stickers())[0]
+        return value
+
+    def _fetch_stickers(self):
+        return dict(self._read_stickers())
+
     def _fetch_list(self):
         return self._wrap_iterator(self._read_list())
 
@@ -341,6 +378,12 @@ class MPDClient(object):
     def _fetch_changes(self):
         return self._fetch_objects(["cpos"])
 
+    def _fetch_idle(self):
+        self._sock.settimeout(self.idletimeout)
+        ret = self._fetch_list()
+        self._sock.settimeout(self._timeout)
+        return ret
+
     def _fetch_songs(self):
         return self._fetch_objects(["file"])
 
@@ -365,6 +408,7 @@ class MPDClient(object):
     def _hello(self):
         line = self._rfile.readline()
         if not line.endswith("\n"):
+            self.disconnect()
             raise ConnectionError("Connection lost while reading MPD hello")
         line = line.rstrip("\n")
         if not line.startswith(HELLO_PREFIX):
@@ -380,16 +424,16 @@ class MPDClient(object):
         self._rfile = _NotConnected()
         self._wfile = _NotConnected()
 
-    def _connect_unix(self, path, timeout):
+    def _connect_unix(self, path):
         if not hasattr(socket, "AF_UNIX"):
             raise ConnectionError("Unix domain sockets not supported "
                                   "on this platform")
         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        sock.settimeout(timeout)
+        sock.settimeout(self.timeout)
         sock.connect(path)
         return sock
 
-    def _connect_tcp(self, host, port, timeout):
+    def _connect_tcp(self, host, port):
         try:
             flags = socket.AI_ADDRCONFIG
         except AttributeError:
@@ -403,7 +447,7 @@ class MPDClient(object):
             try:
                 sock = socket.socket(af, socktype, proto)
                 sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
-                sock.settimeout(timeout)
+                sock.settimeout(self.timeout)
                 sock.connect(sa)
                 return sock
             except socket.error as e:
@@ -415,15 +459,40 @@ class MPDClient(object):
         else:
             raise ConnectionError("getaddrinfo returns an empty list")
 
+    def _settimeout(self, timeout):
+        self._timeout = timeout
+        if self._sock != None:
+            self._sock.settimeout(timeout)
+    def _gettimeout(self):
+        return self._timeout
+    timeout = property(_gettimeout, _settimeout)
+    _timeout = None
+    idletimeout = None
+
     def connect(self, host, port, timeout=None):
+        logger.info("Calling MPD connect(%r, %r, timeout=%r)", host,
+                     port, timeout)
         if self._sock is not None:
             raise ConnectionError("Already connected")
+        if timeout != None:
+            warnings.warn("The timeout parameter in connect() is deprecated! "
+                          "Use MPDClient.timeout = yourtimeout instead.",
+                          DeprecationWarning)
+            self.timeout = timeout
         if host.startswith("/"):
-            self._sock = self._connect_unix(host, timeout)
+            self._sock = self._connect_unix(host)
         else:
-            self._sock = self._connect_tcp(host, port, timeout)
-        self._rfile = self._sock.makefile("r")
-        self._wfile = self._sock.makefile("w")
+            self._sock = self._connect_tcp(host, port)
+
+        if IS_PYTHON2:
+            self._rfile = self._sock.makefile("r")
+            self._wfile = self._sock.makefile("w")
+        else:
+            # Force UTF-8 encoding, since this is dependant from the LC_CTYPE
+            # locale.
+            self._rfile = self._sock.makefile("r", encoding="utf-8")
+            self._wfile = self._sock.makefile("w", encoding="utf-8")
+
         try:
             self._hello()
         except:
@@ -431,9 +500,13 @@ class MPDClient(object):
             raise
 
     def disconnect(self):
-        self._rfile.close()
-        self._wfile.close()
-        self._sock.close()
+        logger.info("Calling MPD disconnect()")
+        if not self._rfile is None:
+            self._rfile.close()
+        if not self._wfile is None:
+            self._wfile.close()
+        if not self._sock is None:
+            self._sock.close()
         self._reset()
 
     def fileno(self):
@@ -466,7 +539,7 @@ class MPDClient(object):
         send_method = newFunction(cls._send, key, callback)
         fetch_method = newFunction(cls._fetch, key, callback)
 
-        # create new mpd commands as function in the tree flavors:
+        # create new mpd commands as function in three flavors:
         # normal, with "send_"-prefix and with "fetch_"-prefix
         escaped_name = name.replace(" ", "_")
         setattr(cls, escaped_name, method)
diff --git a/setup.py b/setup.py
index 51c7c07..e6e08a3 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,9 @@
 
 from distutils.core import setup
 from setuptools import Extension
-
+from setuptools.command.test import test as TestCommand
+import sys
+import mpd
 
 DESCRIPTION = """\
 An MPD (Music Player Daemon) client library written in pure Python.\
@@ -15,12 +17,13 @@ CLASSIFIERS = [
     "Natural Language :: English",
     "Operating System :: OS Independent",
     "Programming Language :: Python",
+    "Programming Language :: Python :: 3",
     "Topic :: Software Development :: Libraries :: Python Modules",
 ]
 
 LICENSE = """\
 Copyright (C) 2008-2010  J. Alexander Treuman <jat at spatialrift.net>
-Copyright (C) 2012  J. Thalheim <jat at spatialrift.net>
+Copyright (C) 2012  J. Thalheim <joerg at higgsboson.tk>
 
 python-mpd2 is free software: you can redistribute it and/or modify
 it under the terms of the GNU Lesser General Public License as published by
@@ -34,9 +37,20 @@ GNU Lesser General Public License for more details.  You should have received a
 along with python-mpd2.  If not, see <http://www.gnu.org/licenses/>.\
 """
 
+class Tox(TestCommand):
+    def finalize_options(self):
+        TestCommand.finalize_options(self)
+        self.test_args = []
+        self.test_suite = True
+    def run_tests(self):
+        #import here, cause outside the eggs aren't loaded
+        import tox
+        errno = tox.cmdline(self.test_args)
+        sys.exit(errno)
+
 setup(
     name="python-mpd2",
-    version="0.4.2",
+    version=".".join(map(str, mpd.VERSION)),
     description="A Python MPD client library",
     long_description=DESCRIPTION,
     author="J. Thalheim",
@@ -47,8 +61,8 @@ setup(
     classifiers=CLASSIFIERS,
     #license=LICENSE,
     keywords=["mpd"],
-    #platforms=["Independant"],
-    test_suite="test"
+    tests_require=['tox'],
+    cmdclass = {'test': Tox},
 )
 
 
diff --git a/test.py b/test.py
index c1f0866..f216b29 100755
--- a/test.py
+++ b/test.py
@@ -1,10 +1,13 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+import itertools
 import os
-import types
-import sys
 from socket import error as SocketError
+import sys
+import types
+import warnings
+
 import mpd
 
 try:
@@ -17,52 +20,57 @@ except ImportError:
     if sys.version_info >= (2, 7):
         import unittest
     else:
-        print("Please install unittest2 from pypi to run tests!")
+        print("Please install unittest2 from PyPI to run tests!")
         sys.exit(1)
 
-def setup_environment():
-    # Alternate this to your setup
-    # Make sure you have at least one song on your playlist
-    global TEST_MPD_HOST, TEST_MPD_PORT, TEST_MPD_PASSWORD
+try:
+    import mock
+except ImportError:
+    print("Please install mock from PyPI to run tests!")
+    sys.exit(1)
 
-    if 'TEST_MPD_PORT' not in os.environ:
-        sys.stderr.write(
-            "You should set the TEST_MPD_PORT environment variable to point "
-            "to your test MPD running instance.\n")
-        sys.exit(255)
+# show deprecation warnings
+warnings.simplefilter('default')
 
-    TEST_MPD_HOST     = os.environ.get('TEST_MPD_HOST', "localhost")
-    TEST_MPD_PORT     = int(os.environ['TEST_MPD_PORT'])
-    TEST_MPD_PASSWORD = os.environ.get('TEST_MPD_PASSWORD')
 
-setup_environment()
+TEST_MPD_HOST, TEST_MPD_PORT = ('example.com', 10000)
 
 
 class TestMPDClient(unittest.TestCase):
 
     longMessage = True
 
-    @classmethod
-    def setUpClass(self):
-        global TEST_MPD_HOST, TEST_MPD_PORT, TEST_MPD_PASSWORD
+    def setUp(self):
+        self.socket_patch = mock.patch("mpd.socket")
+        self.socket_mock = self.socket_patch.start()
+        self.socket_mock.getaddrinfo.return_value = [range(5)]
+
+        self.socket_mock.socket.side_effect = (
+            lambda *a, **kw:
+            # Create a new socket.socket() mock with default attributes,
+            # each time we are calling it back (otherwise, it keeps set
+            # attributes across calls).
+            # That's probablyy what we want, since reconnecting is like
+            # reinitializing the entire connection, and so, the mock.
+            mock.MagicMock(name="socket.socket"))
+
         self.client = mpd.MPDClient()
-        self.idleclient = mpd.MPDClient()
-        try:
-            self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT)
-            self.idleclient.connect(TEST_MPD_HOST, TEST_MPD_PORT)
-            self.commands = self.client.commands()
-        except SocketError as e:
-            raise Exception("Can't connect mpd! Start it or check the configuration: %s" % e)
-        if TEST_MPD_PASSWORD != None:
-            try:
-                self.client.password(TEST_MPD_PASSWORD)
-                self.idleclient.password(TEST_MPD_PASSWORD)
-            except mpd.CommandError as e:
-                raise Exception("Fail to authenticate to mpd.")
-    @classmethod
-    def tearDownClass(self):
-        self.client.disconnect()
-        self.idleclient.disconnect()
+        self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT)
+        self.client._sock.reset_mock()
+        self.MPDWillReturn("ACK don't forget to setup your mock\n")
+
+    def tearDown(self):
+        self.socket_patch.stop()
+
+    def MPDWillReturn(self, *lines):
+        # Return what the caller wants first, then do as if the socket was
+        # disconnected.
+        self.client._rfile.readline.side_effect = itertools.chain(
+            lines, itertools.repeat(''))
+
+    def assertMPDReceived(self, *lines):
+        self.client._wfile.write.assert_called_with(*lines)
+
     def test_metaclass_commands(self):
         # just some random functions
         self.assertTrue(hasattr(self.client, "commands"))
@@ -75,137 +83,257 @@ class TestMPDClient(unittest.TestCase):
         self.assertTrue(hasattr(self.client, "close"))
         self.assertTrue(hasattr(self.client, "fetch_close"))
         self.assertTrue(hasattr(self.client, "send_close"))
+
     def test_fetch_nothing(self):
+        self.MPDWillReturn('OK\n', 'OK\n')
+
         self.assertIsNone(self.client.ping())
+        self.assertMPDReceived('ping\n')
+
         self.assertIsNone(self.client.clearerror())
+        self.assertMPDReceived('clearerror\n')
+
     def test_fetch_list(self):
+        self.MPDWillReturn('OK\n')
+
         self.assertIsInstance(self.client.list("album"), list)
+        self.assertMPDReceived('list "album"\n')
+
     def test_fetch_item(self):
+        self.MPDWillReturn('updating_db: 42\n', 'OK\n')
         self.assertIsNotNone(self.client.update())
+
     def test_fetch_object(self):
+        # XXX: _read_objects() doesn't wait for the final OK
+        self.MPDWillReturn('volume: 63\n', 'OK\n')
         status = self.client.status()
-        stats = self.client.stats()
+        self.assertMPDReceived('status\n')
         self.assertIsInstance(status, dict)
-        # some keys should be there
-        self.assertIn("volume", status)
-        self.assertIn("song", status)
+
+        # XXX: _read_objects() doesn't wait for the final OK
+        self.MPDWillReturn('OK\n')
+        stats = self.client.stats()
+        self.assertMPDReceived('stats\n')
         self.assertIsInstance(stats, dict)
-        self.assertIn("artists", stats)
-        self.assertIn("uptime", stats)
+
     def test_fetch_songs(self):
+        self.MPDWillReturn("file: my-song.ogg\n", "Pos: 0\n", "Id: 66\n", "OK\n")
         playlist = self.client.playlistinfo()
-        self.assertTrue(type(playlist) is list)
-        if len(playlist) > 0:
-                self.assertIsInstance(playlist[0], dict)
+
+        self.assertMPDReceived('playlistinfo\n')
+        self.assertIsInstance(playlist, list)
+        self.assertEqual(1, len(playlist))
+        e = playlist[0]
+        self.assertIsInstance(e, dict)
+        self.assertEqual('my-song.ogg', e['file'])
+        self.assertEqual('0', e['pos'])
+        self.assertEqual('66', e['id'])
+
     def test_send_and_fetch(self):
-        self.client.send_status()
-        self.client.fetch_status()
+        self.MPDWillReturn("volume: 50\n", "OK\n")
+        result = self.client.send_status()
+        self.assertEqual(None, result)
+        self.assertMPDReceived('status\n')
+
+        status = self.client.fetch_status()
+        self.assertEqual(1, self.client._wfile.write.call_count)
+        self.assertEqual({'volume': '50'}, status)
+
     def test_iterating(self):
+        self.MPDWillReturn("file: my-song.ogg\n", "Pos: 0\n", "Id: 66\n", "OK\n")
         self.client.iterate = True
         playlist = self.client.playlistinfo()
+        self.assertMPDReceived('playlistinfo\n')
         self.assertIsInstance(playlist, types.GeneratorType)
         for song in playlist:
-                self.assertIsInstance(song, dict)
-        self.client.iterate = False
+            self.assertIsInstance(song, dict)
+            self.assertEqual('my-song.ogg', song['file'])
+            self.assertEqual('0', song['pos'])
+            self.assertEqual('66', song['id'])
+
     def test_idle(self):
-        # clean event mask
-        self.idleclient.idle()
+        self.MPDWillReturn('OK\n') # nothing changed after idle-ing
+        self.client.idletimeout = 456
+        res = self.client.idle()
+        self.assertMPDReceived('idle\n')
+        self.client._sock.settimeout.assert_has_calls([mock.call(456),
+                                                       mock.call(None)])
+        self.assertEqual([], res)
 
-        self.idleclient.send_idle()
+        self.client.send_idle()
         # new event
-        self.client.update()
-        event = self.idleclient.fetch_idle()
+        self.MPDWillReturn('changed: update\n', 'OK\n')
+
+        event = self.client.fetch_idle()
         self.assertEqual(event, ['update'])
+
     def test_add_and_remove_command(self):
+        self.MPDWillReturn("ACK awesome command\n")
+
         self.client.add_command("awesome command", mpd.MPDClient._fetch_nothing)
         self.assertTrue(hasattr(self.client, "awesome_command"))
         self.assertTrue(hasattr(self.client, "send_awesome_command"))
         self.assertTrue(hasattr(self.client, "fetch_awesome_command"))
         # should be unknown by mpd
-        with self.assertRaises(mpd.CommandError):
-            self.client.awesome_command()
+        self.assertRaises(mpd.CommandError, self.client.awesome_command)
+
         self.client.remove_command("awesome_command")
         self.assertFalse(hasattr(self.client, "awesome_command"))
         self.assertFalse(hasattr(self.client, "send_awesome_command"))
         self.assertFalse(hasattr(self.client, "fetch_awesome_command"))
+
         # remove non existing command
         self.assertRaises(ValueError, self.client.remove_command,
                           "awesome_command")
+
     def test_client_to_client(self):
         # client to client is at this time in beta!
-        if not "channels" in self.client.commands():
-            return
+
+        self.MPDWillReturn('OK\n')
         self.assertIsNone(self.client.subscribe("monty"))
+        self.assertMPDReceived('subscribe "monty"\n')
+
+        self.MPDWillReturn('channel: monty\n', 'OK\n')
         channels = self.client.channels()
-        self.assertIn("monty", channels)
+        self.assertMPDReceived('channels\n')
+        self.assertEqual(["monty"], channels)
 
+        self.MPDWillReturn('OK\n')
         self.assertIsNone(self.client.sendmessage("monty", "SPAM"))
+        self.assertMPDReceived('sendmessage "monty" "SPAM"\n')
+
+        self.MPDWillReturn('channel: monty\n', 'message: SPAM\n', 'OK\n')
         msg = self.client.readmessages()
+        self.assertMPDReceived('readmessages\n')
         self.assertEqual(msg, [{"channel":"monty", "message": "SPAM"}])
 
+        self.MPDWillReturn('OK\n')
         self.assertIsNone(self.client.unsubscribe("monty"))
+        self.assertMPDReceived('unsubscribe "monty"\n')
+
+        self.MPDWillReturn('OK\n')
         channels = self.client.channels()
-        self.assertNotIn("monty", channels)
-
-    def test_commands_list(self):
-        """
-        Test if all implemented commands are valid
-        and all avaible commands are implemented.
-        This test may fail, if a implement command isn't
-        avaible on older versions of mpd
-        """
-        avaible_cmds = set(self.client.commands() + self.client.notcommands())
-        imple_cmds   = set(mpd._commands.keys())
-        sticker_cmds = set(["sticker get", "sticker set", "sticker delete",
-                        "sticker list", "sticker find"])
-        imple_cmds = (imple_cmds - sticker_cmds)
-        imple_cmds.add("sticker")
-        imple_cmds.remove("noidle")
-
-        self.assertEqual(set(), avaible_cmds - imple_cmds,
-                         "Not all commands supported by mpd are implemented!")
-
-        long_desc = (
-            "Not all commands implemented by this library are supported by "
-            "the current mpd.\n"  +
-            "This either means the command list is wrong or mpd is not "
-            "up-to-date.")
-
-        self.assertEqual(set(), imple_cmds - avaible_cmds, long_desc)
+        self.assertMPDReceived('channels\n')
+        self.assertEqual([], channels)
 
     def test_unicode_as_command_args(self):
         if sys.version_info < (3, 0):
-            raw_unicode = "☯☾☝♖✽".decode("utf-8")
-            res = self.client.find("file", raw_unicode)
+            self.MPDWillReturn("OK\n")
+            res = self.client.find("file", unicode("☯☾☝♖✽", 'utf-8'))
             self.assertIsInstance(res, list)
+            self.assertMPDReceived('find "file" "☯☾☝♖✽"\n')
 
-            encoded_str = "☯☾☝♖✽"
-            res2 = self.client.find("file", encoded_str)
+            self.MPDWillReturn("OK\n")
+            res2 = self.client.find("file", "☯☾☝♖✽")
             self.assertIsInstance(res2, list)
+            self.assertMPDReceived('find "file" "☯☾☝♖✽"\n')
         else:
+            self.MPDWillReturn("OK\n")
             res = self.client.find("file","☯☾☝♖✽")
             self.assertIsInstance(res, list)
+            self.assertMPDReceived('find "file" "☯☾☝♖✽"\n')
 
     @unittest.skipIf(sys.version_info >= (3, 0),
                      "Test special unicode handling only if python2")
     def test_unicode_as_reponse(self):
+        self.MPDWillReturn("handler: http://\n", "OK\n")
         self.client.use_unicode = True
         self.assertIsInstance(self.client.urlhandlers()[0], unicode)
+
+        self.MPDWillReturn("handler: http://\n", "OK\n")
         self.client.use_unicode = False
         self.assertIsInstance(self.client.urlhandlers()[0], str)
 
     def test_numbers_as_command_args(self):
-        res = self.client.find("file", 1)
+        self.MPDWillReturn("OK\n")
+        self.client.find("file", 1)
+        self.assertMPDReceived('find "file" "1"\n')
 
-    def test_empty_callbacks(self):
+    def test_commands_without_callbacks(self):
+        self.MPDWillReturn("\n")
         self.client.close()
+        self.assertMPDReceived('close\n')
+
+        # XXX: what are we testing here?
         self.client._reset()
         self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT)
 
-    def test_timeout(self):
+    def test_set_timeout_on_client(self):
+        self.client.timeout = 1
+        self.client._sock.settimeout.assert_called_with(1)
+        self.assertEqual(self.client.timeout, 1)
+
+        self.client.timeout = None
+        self.client._sock.settimeout.assert_called_with(None)
+        self.assertEqual(self.client.timeout, None)
+
+    def test_set_timeout_from_connect(self):
         self.client.disconnect()
-        self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT, timeout=5)
-        self.assertEqual(self.client._sock.gettimeout(), 5)
+        with warnings.catch_warnings(record=True) as w:
+            self.client.connect("example.com", 10000, timeout=5)
+            self.client._sock.settimeout.assert_called_with(5)
+            self.assertEqual(len(w), 1)
+            self.assertIn('Use MPDClient.timeout', str(w[0].message))
+
+    def test_connection_lost(self):
+        # Simulate a connection lost: the socket returns empty strings
+        self.MPDWillReturn('')
+
+        with self.assertRaises(mpd.ConnectionError):
+            self.client.status()
+
+        # consistent behaviour, solves bug #11 (github)
+        with self.assertRaises(mpd.ConnectionError):
+            self.client.status()
+
+        self.assertIs(self.client._sock, None)
+
+    @unittest.skipIf(sys.version_info < (3, 0),
+                     "Automatic decoding/encoding from the socket is only "
+                     "available in Python 3")
+    def test_force_socket_encoding_to_utf8(self):
+        # Force the reconnection to refill the mock
+        self.client.disconnect()
+        self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT)
+        self.assertEqual([mock.call('r', encoding="utf-8"),
+                          mock.call('w', encoding="utf-8")],
+                         # We are onlyy interested into the 2 first entries,
+                         # otherwise we get all the readline() & co...
+                         self.client._sock.makefile.call_args_list[0:2])
+
+    def test_read_stickers(self):
+        self.MPDWillReturn("sticker: foo=bar\n", "OK\n")
+        res = self.client._read_stickers()
+        self.assertEqual([('foo', 'bar')], list(res))
+
+        self.MPDWillReturn("sticker: foo=bar\n", "sticker: l=b\n", "OK\n")
+        res = self.client._read_stickers()
+        self.assertEqual([('foo', 'bar'), ('l', 'b')], list(res))
+
+    def test_read_sticker_with_special_value(self):
+        self.MPDWillReturn("sticker: foo==uv=vu\n", "OK\n")
+        res = self.client._read_stickers()
+        self.assertEqual([('foo', '=uv=vu')], list(res))
+
+    def test_parse_sticket_get_one(self):
+        self.MPDWillReturn("sticker: foo=bar\n", "OK\n")
+        res = self.client.sticker_get('song', 'baz', 'foo')
+        self.assertEqual('bar', res)
+
+    def test_parse_sticket_get_no_sticker(self):
+        self.MPDWillReturn("ACK [50 at 0] {sticker} no such sticker\n")
+        self.assertRaises(mpd.CommandError,
+                          self.client.sticker_get, 'song', 'baz', 'foo')
+
+    def test_parse_sticker_list(self):
+        self.MPDWillReturn("sticker: foo=bar\n", "sticker: lom=bok\n", "OK\n")
+        res = self.client.sticker_list('song', 'baz')
+        self.assertEqual({'foo': 'bar', 'lom': 'bok'}, res)
+
+        # Even with only one sticker, we get a dict
+        self.MPDWillReturn("sticker: foo=bar\n", "OK\n")
+        res = self.client.sticker_list('song', 'baz')
+        self.assertEqual({'foo': 'bar'}, res)
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..4de6999
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,11 @@
+[tox]
+envlist = py26,py27,py32,py33
+
+[testenv]
+deps = mock
+commands = python test.py
+
+[testenv:py26]
+deps = mock
+       unittest2
+commands = python test.py

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mpd/python-mpd.git



More information about the Pkg-mpd-commits mailing list