diff --git a/docs/changes.rst b/docs/changes.rst index 4dcc8c57..2c697ca4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,6 +18,10 @@ v0.8 (in development) Track position and CPID was intermixed, so it would cause a crash if a CPID matching the track position didn't exist. (Fixes: :issue:`162`) +- Added :option:`--list-deps` option to :cmd:`mopidy` command that lists + required and optional dependencies, their current versions, and some other + information useful for debugging. (Fixes: :issue:`74`) + v0.7.3 (2012-08-11) =================== diff --git a/mopidy/core.py b/mopidy/core.py index 596e0fe5..9ae461d8 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -22,6 +22,7 @@ from mopidy import (get_version, settings, OptionalDependencyError, SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE) from mopidy.gstreamer import GStreamer from mopidy.utils import get_class +from mopidy.utils.deps import list_deps_optparse_callback from mopidy.utils.log import setup_logging from mopidy.utils.path import get_or_create_folder, get_or_create_file from mopidy.utils.process import (exit_handler, stop_remaining_actors, @@ -77,6 +78,9 @@ def parse_options(): parser.add_option('--list-settings', action='callback', callback=list_settings_optparse_callback, help='list current settings') + parser.add_option('--list-deps', + action='callback', callback=list_deps_optparse_callback, + help='list dependencies and their versions') return parser.parse_args(args=mopidy_args)[0] def check_old_folders(): diff --git a/mopidy/utils/deps.py b/mopidy/utils/deps.py new file mode 100644 index 00000000..7fce55db --- /dev/null +++ b/mopidy/utils/deps.py @@ -0,0 +1,193 @@ +import os +import platform +import sys + +import pygst +pygst.require('0.10') +import gst + +import pykka + +from mopidy.utils.log import indent + + +def list_deps_optparse_callback(*args): + """ + Prints a list of all dependencies. + + Called by optparse when Mopidy is run with the :option:`--list-deps` + option. + """ + print format_dependency_list() + sys.exit(0) + + +def format_dependency_list(adapters=None): + if adapters is None: + adapters = [ + platform_info, + python_info, + gstreamer_info, + pykka_info, + pyspotify_info, + pylast_info, + dbus_info, + serial_info, + ] + + lines = [] + for adapter in adapters: + dep_info = adapter() + lines.append('%(name)s: %(version)s' % { + 'name': dep_info['name'], + 'version': dep_info.get('version', 'not found'), + }) + if 'path' in dep_info: + lines.append(' Imported from: %s' % ( + os.path.dirname(dep_info['path']))) + if 'other' in dep_info: + lines.append(' Other: %s' % ( + indent(dep_info['other'])),) + return '\n'.join(lines) + + +def platform_info(): + return { + 'name': 'Platform', + 'version': platform.platform(), + } + + +def python_info(): + return { + 'name': 'Python', + 'version': '%s %s' % (platform.python_implementation(), + platform.python_version()), + 'path': platform.__file__, + } + + +def gstreamer_info(): + other = [] + other.append('Python wrapper: gst-python %s' % ( + '.'.join(map(str, gst.get_pygst_version())))) + other.append('Relevant elements:') + for name, status in _gstreamer_check_elements(): + other.append(' %s: %s' % (name, 'OK' if status else 'not found')) + return { + 'name': 'Gstreamer', + 'version': '.'.join(map(str, gst.get_gst_version())), + 'path': gst.__file__, + 'other': '\n'.join(other), + } + + +def _gstreamer_check_elements(): + elements_to_check = [ + # Core playback + 'uridecodebin', + + # External HTTP streams + 'souphttpsrc', + + # Spotify + 'appsrc', + + # Mixers and sinks + 'alsamixer', + 'alsasink', + 'ossmixer', + 'osssink', + 'oss4mixer', + 'oss4sink', + 'pulsemixer', + 'pulsesink', + + # MP3 encoding and decoding + 'mp3parse', + 'mad', + 'id3demux', + 'id3v2mux', + 'lame', + + # Ogg Vorbis encoding and decoding + 'vorbisdec', + 'vorbisenc', + 'vorbisparse', + 'oggdemux', + 'oggmux', + 'oggparse', + + # Flac decoding + 'flacdec', + 'flacparse', + + # Shoutcast output + 'shout2send', + ] + known_elements = [factory.get_name() for factory in + gst.registry_get_default().get_feature_list(gst.TYPE_ELEMENT_FACTORY)] + return [(element, element in known_elements) for element in elements_to_check] + + +def pykka_info(): + if hasattr(pykka, '__version__'): + # Pykka >= 0.14 + version = pykka.__version__ + else: + # Pykka < 0.14 + version = pykka.get_version() + return { + 'name': 'Pykka', + 'version': version, + 'path': pykka.__file__, + } + + +def pyspotify_info(): + dep_info = {'name': 'pyspotify'} + try: + import spotify + if hasattr(spotify, '__version__'): + dep_info['version'] = spotify.__version__ + else: + dep_info['version'] = '< 1.3' + dep_info['path'] = spotify.__file__ + dep_info['other'] = 'Built for libspotify API version %d' % ( + spotify.api_version,) + except ImportError: + pass + return dep_info + + +def pylast_info(): + dep_info = {'name': 'pylast'} + try: + import pylast + dep_info['version'] = pylast.__version__ + dep_info['path'] = pylast.__file__ + except ImportError: + pass + return dep_info + + +def dbus_info(): + dep_info = {'name': 'dbus-python'} + try: + import dbus + dep_info['version'] = dbus.__version__ + dep_info['path'] = dbus.__file__ + except ImportError: + pass + return dep_info + + +def serial_info(): + dep_info = {'name': 'pyserial'} + try: + import serial + dep_info['version'] = serial.VERSION + dep_info['path'] = serial.__file__ + except ImportError: + pass + return dep_info diff --git a/tests/utils/deps_test.py b/tests/utils/deps_test.py new file mode 100644 index 00000000..9898b59f --- /dev/null +++ b/tests/utils/deps_test.py @@ -0,0 +1,113 @@ +import platform + +import pygst +pygst.require('0.10') +import gst +import pykka + +try: + import dbus +except ImportError: + dbus = False + +try: + import pylast +except ImportError: + pylast = False + +try: + import serial +except ImportError: + serial = False + +try: + import spotify +except ImportError: + spotify = False + +from mopidy.utils import deps + +from tests import unittest + + +class DepsTest(unittest.TestCase): + def test_format_dependency_list(self): + adapters = [ + lambda: dict(name='Python', version='FooPython 2.7.3'), + lambda: dict(name='Platform', version='Loonix 4.0.1'), + lambda: dict(name='Pykka', path='/foo/bar/baz.py', other='Quux') + ] + + result = deps.format_dependency_list(adapters) + + self.assertIn('Python: FooPython 2.7.3', result) + self.assertIn('Platform: Loonix 4.0.1', result) + self.assertIn('Pykka: not found', result) + self.assertIn('Imported from: /foo/bar', result) + self.assertNotIn('/baz.py', result) + self.assertIn('Quux', result) + + def test_platform_info(self): + result = deps.platform_info() + + self.assertEquals('Platform', result['name']) + self.assertIn(platform.platform(), result['version']) + + def test_python_info(self): + result = deps.python_info() + + self.assertEquals('Python', result['name']) + self.assertIn(platform.python_implementation(), result['version']) + self.assertIn(platform.python_version(), result['version']) + self.assertIn('python', result['path']) + + def test_gstreamer_info(self): + result = deps.gstreamer_info() + + self.assertEquals('Gstreamer', result['name']) + self.assertEquals('.'.join(map(str, gst.get_gst_version())), result['version']) + self.assertIn('gst', result['path']) + self.assertIn('Python wrapper: gst-python', result['other']) + self.assertIn('.'.join(map(str, gst.get_pygst_version())), result['other']) + self.assertIn('Relevant elements:', result['other']) + + def test_pykka_info(self): + result = deps.pykka_info() + + self.assertEquals('Pykka', result['name']) + self.assertEquals(pykka.__version__, result['version']) + self.assertIn('pykka', result['path']) + + @unittest.skipUnless(spotify, 'pyspotify not found') + def test_pyspotify_info(self): + result = deps.pyspotify_info() + + self.assertEquals('pyspotify', result['name']) + self.assertEquals(spotify.__version__, result['version']) + self.assertIn('spotify', result['path']) + self.assertIn('Built for libspotify API version', result['other']) + self.assertIn(str(spotify.api_version), result['other']) + + @unittest.skipUnless(pylast, 'pylast not found') + def test_pylast_info(self): + result = deps.pylast_info() + + self.assertEquals('pylast', result['name']) + self.assertEquals(pylast.__version__, result['version']) + self.assertIn('pylast', result['path']) + + @unittest.skipUnless(dbus, 'dbus not found') + def test_dbus_info(self): + result = deps.dbus_info() + + self.assertEquals('dbus-python', result['name']) + self.assertEquals(dbus.__version__, result['version']) + self.assertIn('dbus', result['path']) + + @unittest.skipUnless(serial, 'serial not found') + def test_serial_info(self): + result = deps.serial_info() + + self.assertEquals('pyserial', result['name']) + self.assertEquals(serial.VERSION, result['version']) + self.assertIn('serial', result['path'])