From 20b457cc4a60ea1ba03438b2de93ae3257a7ae18 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Apr 2015 01:05:45 +0200 Subject: [PATCH 1/6] Move gobject check from __init__ to __main__ Related to #1068 --- mopidy/__init__.py | 16 ---------------- mopidy/__main__.py | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 388bb9f0..8322de32 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, print_function, unicode_literals import platform import sys -import textwrap import warnings @@ -11,21 +10,6 @@ if not (2, 7) <= sys.version_info < (3,): 'ERROR: Mopidy requires Python 2.7, but found %s.' % platform.python_version()) -try: - import gobject # noqa -except ImportError: - print(textwrap.dedent(""" - ERROR: The gobject Python package was not found. - - Mopidy requires GStreamer (and GObject) to work. These are C libraries - with a number of dependencies themselves, and cannot be installed with - the regular Python tools like pip. - - Please see http://docs.mopidy.com/en/latest/installation/ for - instructions on how to install the required dependencies. - """)) - raise - warnings.filterwarnings('ignore', 'could not open display') diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 96e10e18..9ec9769f 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -4,8 +4,23 @@ import logging import os import signal import sys +import textwrap + +try: + import gobject # noqa +except ImportError: + print(textwrap.dedent(""" + ERROR: The gobject Python package was not found. + + Mopidy requires GStreamer (and GObject) to work. These are C libraries + with a number of dependencies themselves, and cannot be installed with + the regular Python tools like pip. + + Please see http://docs.mopidy.com/en/latest/installation/ for + instructions on how to install the required dependencies. + """)) + raise -import gobject gobject.threads_init() try: From 7bda4f835f0055240461da88489cc31cef1992a4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Apr 2015 23:43:32 +0200 Subject: [PATCH 2/6] xdg: Add XDG dir utils --- mopidy/utils/xdg.py | 56 +++++++++++++++++++++++++++++++++ tests/utils/test_xdg.py | 69 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 mopidy/utils/xdg.py create mode 100644 tests/utils/test_xdg.py diff --git a/mopidy/utils/xdg.py b/mopidy/utils/xdg.py new file mode 100644 index 00000000..ffc6de22 --- /dev/null +++ b/mopidy/utils/xdg.py @@ -0,0 +1,56 @@ +from __future__ import absolute_import, unicode_literals + +import ConfigParser +import io +import os + + +def get_dirs(): + """Returns a dict of all the known XDG Base Directories for the current user. + + The keys ``XDG_CACHE_DIR``, ``XDG_CONFIG_DIR``, and ``XDG_DATA_DIR`` is + always available. + + Additional keys, like ``XDG_MUSIC_DIR``, may be available if the + ``$XDG_CONFIG_DIR/user-dirs.dirs`` file exists and is parseable. + + See http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + for the XDG Base Directory specification. + """ + + dirs = { + 'XDG_CACHE_DIR': ( + os.environ.get('XDG_CACHE_HOME') or + os.path.expanduser(b'~/.cache')), + 'XDG_CONFIG_DIR': ( + os.environ.get('XDG_CONFIG_HOME') or + os.path.expanduser(b'~/.config')), + 'XDG_DATA_DIR': ( + os.environ.get('XDG_DATA_HOME') or + os.path.expanduser(b'~/.local/share')), + } + + dirs.update(_get_user_dirs(dirs['XDG_CONFIG_DIR'])) + + return dirs + + +def _get_user_dirs(xdg_config_dir): + dirs_file = os.path.join(xdg_config_dir, 'user-dirs.dirs') + + if not os.path.exists(dirs_file): + return {} + + with open(dirs_file, 'rb') as fh: + data = fh.read() + + data = b'[XDG_USER_DIRS]\n' + data + data = data.replace(b'$HOME', os.path.expanduser(b'~')) + data = data.replace(b'"', b'') + + config = ConfigParser.RawConfigParser() + config.readfp(io.BytesIO(data)) + + return { + k.decode('utf-8').upper(): os.path.abspath(v) + for k, v in config.items('XDG_USER_DIRS') if v is not None} diff --git a/tests/utils/test_xdg.py b/tests/utils/test_xdg.py new file mode 100644 index 00000000..eab595a4 --- /dev/null +++ b/tests/utils/test_xdg.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals + +import os + +import mock + +import pytest + +from mopidy.utils import xdg + + +@pytest.yield_fixture +def environ(): + patcher = mock.patch.dict(os.environ, clear=True) + yield patcher.start() + patcher.stop() + + +def test_cache_dir_default(environ): + assert xdg.get_dirs()['XDG_CACHE_DIR'] == os.path.expanduser(b'~/.cache') + + +def test_cache_dir_from_env(environ): + os.environ['XDG_CACHE_HOME'] = '/foo/bar' + + assert xdg.get_dirs()['XDG_CACHE_DIR'] == '/foo/bar' + + +def test_config_dir_default(environ): + assert xdg.get_dirs()['XDG_CONFIG_DIR'] == os.path.expanduser(b'~/.config') + + +def test_config_dir_from_env(environ): + os.environ['XDG_CONFIG_HOME'] = '/foo/bar' + + assert xdg.get_dirs()['XDG_CONFIG_DIR'] == '/foo/bar' + + +def test_data_dir_default(environ): + assert xdg.get_dirs()['XDG_DATA_DIR'] == os.path.expanduser( + b'~/.local/share') + + +def test_data_dir_from_env(environ): + os.environ['XDG_DATA_HOME'] = '/foo/bar' + + assert xdg.get_dirs()['XDG_DATA_DIR'] == '/foo/bar' + + +def test_user_dirs(environ, tmpdir): + os.environ['XDG_CONFIG_HOME'] = str(tmpdir) + + with open(os.path.join(str(tmpdir), 'user-dirs.dirs'), 'wb') as fh: + fh.write('# Some comments\n') + fh.write('XDG_MUSIC_DIR="$HOME/Music2"\n') + + result = xdg.get_dirs() + + assert result['XDG_MUSIC_DIR'] == os.path.expanduser(b'~/Music2') + assert 'XDG_DOWNLOAD_DIR' not in result + + +def test_user_dirs_when_no_dirs_file(environ, tmpdir): + os.environ['XDG_CONFIG_HOME'] = str(tmpdir) + + result = xdg.get_dirs() + + assert 'XDG_MUSIC_DIR' not in result + assert 'XDG_DOWNLOAD_DIR' not in result From 9becb26f6016067795d0c23a795d56ed091d10fc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Apr 2015 23:44:40 +0200 Subject: [PATCH 3/6] path: Get XDG dirs without using glib Related to #1068 --- mopidy/utils/path.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index e845cd95..37b6cdb1 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -8,25 +8,15 @@ import threading import urllib import urlparse -import glib - from mopidy import compat, exceptions from mopidy.compat import queue -from mopidy.utils import encoding +from mopidy.utils import encoding, xdg logger = logging.getLogger(__name__) -XDG_DIRS = { - 'XDG_CACHE_DIR': glib.get_user_cache_dir(), - 'XDG_CONFIG_DIR': glib.get_user_config_dir(), - 'XDG_DATA_DIR': glib.get_user_data_dir(), - 'XDG_MUSIC_DIR': glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC), -} - -# XDG_MUSIC_DIR can be none, so filter out any bad data. -XDG_DIRS = dict((k, v) for k, v in XDG_DIRS.items() if v is not None) +XDG_DIRS = xdg.get_dirs() def get_or_create_dir(dir_path): From 299bc722ce978cda3e09c23f234158a118e71294 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 8 Apr 2015 00:10:39 +0200 Subject: [PATCH 4/6] listener: Move glib import into function Related to #1068 --- mopidy/listener.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mopidy/listener.py b/mopidy/listener.py index 410558ac..35bd8b73 100644 --- a/mopidy/listener.py +++ b/mopidy/listener.py @@ -2,14 +2,18 @@ from __future__ import absolute_import, unicode_literals import logging -import gobject - import pykka logger = logging.getLogger(__name__) def send_async(cls, event, **kwargs): + # This file is imported by mopidy.backends, which again is imported by all + # backend extensions. By importing modules that are not easily installable + # close to their use, we make some extensions able to run their tests in a + # virtualenv with global site-packages disabled. + import gobject + gobject.idle_add(lambda: send(cls, event, **kwargs)) From ea52e8ffdd140e64254cb32f4ae12dc5935ba60b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 8 Apr 2015 00:15:12 +0200 Subject: [PATCH 5/6] docs: Add #1068 fix to changelog Fixes #1068 --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b8f4f530..60d011d7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,6 +25,12 @@ Internal changes - Tests have been cleaned up to stop using deprecated APIs where feasible. (Partial fix: :issue:`1083`, PR: :issue:`1090`) +- It is now possible to import :mod:`mopidy.backends` without having GObject or + GStreamer installed. In other words, a lot of backend extensions should now + be able to run tests in a virtualenv with global site-packages disabled. This + removes a lot of potential error sources. (Fixes: :issue:`1068`, PR: + :issue:`1115`) + v1.0.1 (UNRELEASED) =================== From 0b8e9426b55932ca9bf671f4c0d51c6404f7b1ec Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 8 Apr 2015 00:29:55 +0200 Subject: [PATCH 6/6] xdg: Fix review comments --- mopidy/utils/xdg.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/xdg.py b/mopidy/utils/xdg.py index ffc6de22..adb43f39 100644 --- a/mopidy/utils/xdg.py +++ b/mopidy/utils/xdg.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -import ConfigParser +import ConfigParser as configparser import io import os @@ -36,6 +36,16 @@ def get_dirs(): def _get_user_dirs(xdg_config_dir): + """Returns a dict of XDG dirs read from + ``$XDG_CONFIG_HOME/user-dirs.dirs``. + + This is used at import time for most users of :mod:`mopidy`. By rolling our + own implementation instead of using :meth:`glib.get_user_special_dir` we + make it possible for many extensions to run their test suites, which are + importing parts of :mod:`mopidy`, in a virtualenv with global site-packages + disabled, and thus no :mod:`glib` available. + """ + dirs_file = os.path.join(xdg_config_dir, 'user-dirs.dirs') if not os.path.exists(dirs_file): @@ -48,7 +58,7 @@ def _get_user_dirs(xdg_config_dir): data = data.replace(b'$HOME', os.path.expanduser(b'~')) data = data.replace(b'"', b'') - config = ConfigParser.RawConfigParser() + config = configparser.RawConfigParser() config.readfp(io.BytesIO(data)) return {