[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