diff --git a/docs/changelog.rst b/docs/changelog.rst index a2a73dd3..6a4ab026 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -55,6 +55,9 @@ and improved. - The command option :option:`mopidy --list-settings` is now named :option:`mopidy --show-config`. +- The command option :option:`mopidy --list-deps` is now named + :option:`mopidy --show-deps`. + - What configuration files to use can now be specified through the command option :option:`mopidy --config`. diff --git a/docs/running.rst b/docs/running.rst index 58f7d591..7e0bacbb 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -52,9 +52,9 @@ mopidy command together to show the effective document. Secret values like passwords are masked out. Config for disabled extensions are not included. -.. cmdoption:: --list-deps +.. cmdoption:: --show-deps - List dependencies and their versions. + Show dependencies, their versions and installation location. .. cmdoption:: --config diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 0c0f10da..66b942c9 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -27,7 +27,7 @@ with others for debugging. Installed dependencies ====================== -The command :option:`mopidy --list-deps` will list the paths to and versions of +The command :option:`mopidy --show-deps` will list the paths to and versions of any dependency Mopidy or the extensions might need to work. This is very useful data for checking that you're using the right versions, and that you're using the right installation if you have multiple installations of a dependency on diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 022a59ba..191808db 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -151,9 +151,9 @@ def parse_options(): action='callback', callback=show_config_callback, help='show current config') parser.add_option( - b'--list-deps', - action='callback', callback=deps.list_deps_optparse_callback, - help='list dependencies and their versions') + b'--show-deps', + action='callback', callback=deps.show_deps_optparse_callback, + help='show dependencies and their versions') parser.add_option( b'--config', action='store', dest='config', diff --git a/mopidy/utils/deps.py b/mopidy/utils/deps.py index c83780fb..742536a5 100644 --- a/mopidy/utils/deps.py +++ b/mopidy/utils/deps.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import functools import os import platform import sys @@ -8,16 +9,16 @@ import pygst pygst.require('0.10') import gst -import pykka +import pkg_resources from . import formatting -def list_deps_optparse_callback(*args): +def show_deps_optparse_callback(*args): """ Prints a list of all dependencies. - Called by optparse when Mopidy is run with the :option:`--list-deps` + Called by optparse when Mopidy is run with the :option:`--show-deps` option. """ print format_dependency_list() @@ -26,32 +27,47 @@ def list_deps_optparse_callback(*args): def format_dependency_list(adapters=None): if adapters is None: + dist_names = set([ + ep.dist.project_name for ep in + pkg_resources.iter_entry_points('mopidy.ext') + if ep.dist.project_name != 'Mopidy']) + dist_infos = [ + functools.partial(pkg_info, dist_name) + for dist_name in dist_names] + adapters = [ platform_info, python_info, + functools.partial(pkg_info, 'Mopidy', True) + ] + dist_infos + [ gstreamer_info, - pykka_info, - pyspotify_info, - pylast_info, - dbus_info, - serial_info, - cherrypy_info, - ws4py_info, ] + return '\n'.join([_format_dependency(a()) for a in adapters]) + + +def _format_dependency(dep_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' % ( - formatting.indent(dep_info['other'])),) + + if 'version' not in dep_info: + lines.append('%s: not found' % dep_info['name']) + else: + lines.append('%s: %s from %s' % ( + dep_info['name'], + dep_info['version'], + os.path.dirname(dep_info.get('path', 'none')), + )) + + if 'other' in dep_info: + lines.append(' Detailed information: %s' % ( + formatting.indent(dep_info['other'], places=4)),) + + if dep_info.get('dependencies', []): + for sub_dep_info in dep_info['dependencies']: + sub_dep_lines = _format_dependency(sub_dep_info) + lines.append( + formatting.indent(sub_dep_lines, places=2, singles=True)) + return '\n'.join(lines) @@ -71,13 +87,46 @@ def python_info(): } +def pkg_info(project_name=None, include_extras=False): + if project_name is None: + project_name = 'Mopidy' + distribution = pkg_resources.get_distribution(project_name) + extras = include_extras and distribution.extras or [] + dependencies = [ + pkg_info(d) for d in distribution.requires(extras)] + return { + 'name': distribution.project_name, + 'version': distribution.version, + 'path': distribution.location, + 'dependencies': dependencies, + } + + def gstreamer_info(): other = [] other.append('Python wrapper: gst-python %s' % ( '.'.join(map(str, gst.get_pygst_version())))) - other.append('Relevant elements:') + + found_elements = [] + missing_elements = [] for name, status in _gstreamer_check_elements(): - other.append(' %s: %s' % (name, 'OK' if status else 'not found')) + if status: + found_elements.append(name) + else: + missing_elements.append(name) + + other.append('Relevant elements:') + other.append(' Found:') + for element in found_elements: + other.append(' %s' % element) + else: + other.append(' none') + other.append(' Not found:') + for element in missing_elements: + other.append(' %s' % element) + else: + other.append(' none') + return { 'name': 'GStreamer', 'version': '.'.join(map(str, gst.get_gst_version())), @@ -134,82 +183,3 @@ def _gstreamer_check_elements(): 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(): - return { - 'name': 'Pykka', - 'version': pykka.__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 - - -def cherrypy_info(): - dep_info = {'name': 'cherrypy'} - try: - import cherrypy - dep_info['version'] = cherrypy.__version__ - dep_info['path'] = cherrypy.__file__ - except ImportError: - pass - return dep_info - - -def ws4py_info(): - dep_info = {'name': 'ws4py'} - try: - import ws4py - dep_info['version'] = ws4py.__version__ - dep_info['path'] = ws4py.__file__ - except ImportError: - pass - return dep_info diff --git a/mopidy/utils/formatting.py b/mopidy/utils/formatting.py index ba311fb5..3c313eae 100644 --- a/mopidy/utils/formatting.py +++ b/mopidy/utils/formatting.py @@ -4,13 +4,15 @@ import re import unicodedata -def indent(string, places=4, linebreak='\n'): +def indent(string, places=4, linebreak='\n', singles=False): lines = string.split(linebreak) - if len(lines) == 1: + if not singles and len(lines) == 1: return string - result = '' - for line in lines: - result += linebreak + ' ' * places + line + for i, line in enumerate(lines): + lines[i] = ' ' * places + line + result = linebreak.join(lines) + if not singles: + result = linebreak + result return result diff --git a/tests/utils/deps_test.py b/tests/utils/deps_test.py index 65a1eda1..482a1cf5 100644 --- a/tests/utils/deps_test.py +++ b/tests/utils/deps_test.py @@ -5,37 +5,8 @@ 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 - -try: - import cherrypy -except ImportError: - cherrypy = False - -try: - import ws4py -except ImportError: - ws4py = False +import mock from mopidy.utils import deps @@ -47,17 +18,32 @@ class DepsTest(unittest.TestCase): 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') + lambda: dict( + name='Pykka', version='1.1', + path='/foo/bar/baz.py', other='Quux'), + lambda: dict(name='Foo'), + lambda: dict(name='Mopidy', version='0.13', dependencies=[ + dict(name='pylast', version='0.5', dependencies=[ + dict(name='setuptools', version='0.6') + ]) + ]) ] 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.assertIn('Pykka: 1.1 from /foo/bar', result) self.assertNotIn('/baz.py', result) - self.assertIn('Quux', result) + self.assertIn('Detailed information: Quux', result) + + self.assertIn('Foo: not found', result) + + self.assertIn('Mopidy: 0.13', result) + self.assertIn(' pylast: 0.5', result) + self.assertIn(' setuptools: 0.6', result) def test_platform_info(self): result = deps.platform_info() @@ -85,59 +71,39 @@ class DepsTest(unittest.TestCase): '.'.join(map(str, gst.get_pygst_version())), result['other']) self.assertIn('Relevant elements:', result['other']) - def test_pykka_info(self): - result = deps.pykka_info() + @mock.patch('pkg_resources.get_distribution') + def test_pkg_info(self, get_distribution_mock): + dist_mopidy = mock.Mock() + dist_mopidy.project_name = 'Mopidy' + dist_mopidy.version = '0.13' + dist_mopidy.location = '/tmp/example/mopidy' + dist_mopidy.requires.return_value = ['Pykka'] - self.assertEquals('Pykka', result['name']) - self.assertEquals(pykka.__version__, result['version']) - self.assertIn('pykka', result['path']) + dist_pykka = mock.Mock() + dist_pykka.project_name = 'Pykka' + dist_pykka.version = '1.1' + dist_pykka.location = '/tmp/example/pykka' + dist_pykka.requires.return_value = ['setuptools'] - @unittest.skipUnless(spotify, 'pyspotify not found') - def test_pyspotify_info(self): - result = deps.pyspotify_info() + dist_setuptools = mock.Mock() + dist_setuptools.project_name = 'setuptools' + dist_setuptools.version = '0.6' + dist_setuptools.location = '/tmp/example/setuptools' + dist_setuptools.requires.return_value = [] - 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']) + get_distribution_mock.side_effect = [ + dist_mopidy, dist_pykka, dist_setuptools] - @unittest.skipUnless(pylast, 'pylast not found') - def test_pylast_info(self): - result = deps.pylast_info() + result = deps.pkg_info() - self.assertEquals('pylast', result['name']) - self.assertEquals(pylast.__version__, result['version']) - self.assertIn('pylast', result['path']) + self.assertEquals('Mopidy', result['name']) + self.assertEquals('0.13', result['version']) + self.assertIn('mopidy', result['path']) - @unittest.skipUnless(dbus, 'dbus not found') - def test_dbus_info(self): - result = deps.dbus_info() + dep_info_pykka = result['dependencies'][0] + self.assertEquals('Pykka', dep_info_pykka['name']) + self.assertEquals('1.1', dep_info_pykka['version']) - 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']) - - @unittest.skipUnless(cherrypy, 'cherrypy not found') - def test_cherrypy_info(self): - result = deps.cherrypy_info() - - self.assertEquals('cherrypy', result['name']) - self.assertEquals(cherrypy.__version__, result['version']) - self.assertIn('cherrypy', result['path']) - - @unittest.skipUnless(ws4py, 'ws4py not found') - def test_ws4py_info(self): - result = deps.ws4py_info() - - self.assertEquals('ws4py', result['name']) - self.assertEquals(ws4py.__version__, result['version']) - self.assertIn('ws4py', result['path']) + dep_info_setuptools = dep_info_pykka['dependencies'][0] + self.assertEquals('setuptools', dep_info_setuptools['name']) + self.assertEquals('0.6', dep_info_setuptools['version'])