Skip to content

Commit

Permalink
Improve handling of "media_dirs" file extension config
Browse files Browse the repository at this point in the history
This moves almost all of the processing for the "media_dirs" file setting
into the config system, with the aim of making it more robust while also
resolving some bugs. The only thing left for the file library backend to
do manually now is verify that the input directories actually exist as
folders.

The main bug this solves is an issue of whitespace handling around
paths, as detailed in #1965.

To make this possible, the Path config type has been altered to return
pathlib objects rather than strings.
  • Loading branch information
djmattyg007 committed Aug 14, 2021
1 parent 007a582 commit d5b251f
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 32 deletions.
15 changes: 12 additions & 3 deletions mopidy/config/types.py
@@ -1,11 +1,17 @@
import logging
import os
import pathlib
import re
import socket

from mopidy.config import validators
from mopidy.internal import log, path


_is_windows = os.name == "nt"
_ExpandedPathBase = pathlib.WindowsPath if _is_windows else pathlib.PosixPath


def decode(value):
if isinstance(value, bytes):
value = value.decode(errors="surrogateescape")
Expand Down Expand Up @@ -411,9 +417,12 @@ def __init__(self, choices=None, optional=False):
)


# Keep this for backwards compatibility
class _ExpandedPath(_TransformedValue):
pass
class _ExpandedPath(_ExpandedPathBase):
def __new__(cls, original: str, transformed: pathlib.Path):
return super().__new__(cls, transformed)

def __init__(self, original: str, transformed: pathlib.Path):
self.original = original


class Path(ConfigValue):
Expand Down
15 changes: 14 additions & 1 deletion mopidy/file/__init__.py
Expand Up @@ -19,7 +19,20 @@ def get_default_config(self):

def get_config_schema(self):
schema = super().get_config_schema()
schema["media_dirs"] = config.List(optional=True)
schema["media_dirs"] = config.List(
optional=True,
subtype=config.Pair(
optional=False,
optional_pair=True,
subtypes=(
config.Path(),
config.String(
# TODO Mpd client should accept / in dir name
transformer=lambda value: value.replace(os.sep, "+")
),
),
),
)
schema["excluded_file_extensions"] = config.List(optional=True)
schema["show_dotfiles"] = config.Boolean(optional=True)
schema["follow_symlinks"] = config.Boolean(optional=True)
Expand Down
21 changes: 7 additions & 14 deletions mopidy/file/library.py
Expand Up @@ -109,33 +109,26 @@ def lookup(self, uri):
return [track]

def _get_media_dirs(self, config):
for entry in config["file"]["media_dirs"]:
media_dir = {}
media_dir_split = entry.split("|", 1)
local_path = path.expand_path(media_dir_split[0])

for local_path, name in config["file"]["media_dirs"]:
if local_path is None:
logger.debug(
"Failed expanding path (%s) from file/media_dirs config "
"value.",
media_dir_split[0],
local_path.original,
)
continue
elif not local_path.is_dir():
logger.warning(
"%s is not a directory. Please create the directory or "
"update the file/media_dirs config value.",
local_path,
local_path.original
)
continue

media_dir["path"] = local_path
if len(media_dir_split) == 2:
media_dir["name"] = media_dir_split[1]
else:
# TODO Mpd client should accept / in dir name
media_dir["name"] = media_dir_split[0].replace(os.sep, "+")

media_dir = {
"path": local_path,
"name": name,
}
yield media_dir

def _get_media_dirs_refs(self):
Expand Down
25 changes: 13 additions & 12 deletions tests/config/test_types.py
@@ -1,4 +1,5 @@
import logging
import pathlib
import re
import socket
from unittest import mock
Expand Down Expand Up @@ -653,7 +654,7 @@ def test_deserialize_with_custom_subtypes(self):

cv = types.Pair(subtypes=(types.Path(), types.String()))
result = cv.deserialize("/dev/null | empty")
assert result == ("/dev/null", "empty")
assert result == (pathlib.Path("/dev/null"), "empty")

with mock.patch("socket.getaddrinfo") as getaddrinfo_mock:
cv = types.Pair(subtypes=(types.Hostname(), types.Port()))
Expand Down Expand Up @@ -695,7 +696,7 @@ def test_deserialize_with_custom_subtypes_respects_optional_separator(self):
optional_pair=True, subtypes=(types.Path(), types.String())
)
result = cv.deserialize("/dev/null")
assert result == ("/dev/null", "/dev/null")
assert result == (pathlib.Path("/dev/null"), "/dev/null")

cv = types.Pair(
optional_pair=True, subtypes=(types.Port(), types.Port())
Expand Down Expand Up @@ -1377,11 +1378,11 @@ def test_invalid_ports(self):

class TestExpandedPath:
def test_is_str(self):
assert isinstance(types._ExpandedPath(b"/tmp", b"foo"), str)
assert isinstance(types._ExpandedPath("/tmp", pathlib.Path("/tmp")), pathlib.Path)

def test_stores_both_expanded_and_original_path(self):
original = "~"
expanded = "expanded_path"
expanded = pathlib.Path("expanded_path")

result = types._ExpandedPath(original, expanded)

Expand All @@ -1393,34 +1394,34 @@ class TestPath:
def test_deserialize_conversion_success(self):
cv = types.Path()

result = cv.deserialize(b"/foo")
result = cv.deserialize("/foo")

assert result == "/foo"
assert result == pathlib.Path("/foo")
assert isinstance(result, types._ExpandedPath)
assert isinstance(result, str)
assert isinstance(result, pathlib.Path)

def test_deserialize_enforces_required(self):
cv = types.Path()

with pytest.raises(ValueError):
cv.deserialize(b"")
cv.deserialize("")

def test_deserialize_respects_optional(self):
cv = types.Path(optional=True)

assert cv.deserialize(b"") is None
assert cv.deserialize(b" ") is None
assert cv.deserialize("") is None
assert cv.deserialize(" ") is None

def test_serialize_uses_original(self):
cv = types.Path()
path = types._ExpandedPath("original_path", "expanded_path")
path = types._ExpandedPath("original_path", pathlib.Path("expanded_path"))

assert cv.serialize(path) == "original_path"

def test_serialize_plain_string(self):
cv = types.Path()

assert cv.serialize(b"path") == "path"
assert cv.serialize("path") == "path"

def test_serialize_supports_unicode_string(self):
cv = types.Path()
Expand Down
3 changes: 2 additions & 1 deletion tests/file/test_browse.py
Expand Up @@ -11,11 +11,12 @@

@pytest.fixture
def config():
media_dir = (path_to_data_dir(""), "Test Media")
return {
"proxy": {},
"file": {
"show_dotfiles": False,
"media_dirs": [str(path_to_data_dir(""))],
"media_dirs": (media_dir,),
"excluded_file_extensions": [],
"follow_symlinks": False,
"metadata_timeout": 1000,
Expand Down
2 changes: 1 addition & 1 deletion tests/file/test_lookup.py
Expand Up @@ -14,7 +14,7 @@ def config():
"proxy": {},
"file": {
"show_dotfiles": False,
"media_dirs": [],
"media_dirs": tuple(),
"excluded_file_extensions": [],
"follow_symlinks": False,
"metadata_timeout": 1000,
Expand Down

0 comments on commit d5b251f

Please sign in to comment.