diff --git a/beets/dbcore/__init__.py b/beets/dbcore/__init__.py index 100f546b5..093591882 100644 --- a/beets/dbcore/__init__.py +++ b/beets/dbcore/__init__.py @@ -23,5 +23,6 @@ from .types import Type from .queryparse import query_from_strings from .queryparse import sort_from_strings from .queryparse import parse_sorted_query +from .query import InvalidQueryError # flake8: noqa diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 2f90e0398..cd891148e 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -23,12 +23,35 @@ from beets import util from datetime import datetime, timedelta -class InvalidQueryError(ValueError): +class ParsingError(ValueError): + """Abstract class for any unparseable user-requested album/query + specification. + """ + + +class InvalidQueryError(ParsingError): + """Represent any kind of invalid query. + + The query should be a unicode string or a list, which will be space-joined. + """ + def __init__(self, query, explanation): + if isinstance(query, list): + query = " ".join(query) + message = "'{0}': {1}".format(query, explanation) + super(InvalidQueryError, self).__init__(message) + + +class InvalidQueryArgumentTypeError(ParsingError): + """Represent a query argument that could not be converted as expected. + + It exists to be caught in upper stack levels so a meaningful (i.e. with the + query) InvalidQueryError can be raised. + """ def __init__(self, what, expected, detail=None): message = "'{0}' is not {1}".format(what, expected) if detail: message = "{0}: {1}".format(message, detail) - super(InvalidQueryError, self).__init__(message) + super(InvalidQueryArgumentTypeError, self).__init__(message) class Query(object): @@ -160,8 +183,9 @@ class RegexpQuery(StringFieldQuery): self.pattern = re.compile(self.pattern) except re.error as exc: # Invalid regular expression. - raise InvalidQueryError(pattern, "a regular expression", - format(exc)) + raise InvalidQueryArgumentTypeError(pattern, + "a regular expression", + format(exc)) @classmethod def string_match(cls, pattern, value): @@ -228,7 +252,7 @@ class NumericQuery(FieldQuery): try: return float(s) except ValueError: - raise InvalidQueryError(s, "an int or a float") + raise InvalidQueryArgumentTypeError(s, "an int or a float") def __init__(self, field, pattern, fast=True): super(NumericQuery, self).__init__(field, pattern, fast) diff --git a/beets/library.py b/beets/library.py index 40051c354..a57ed1642 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1085,7 +1085,7 @@ def parse_query_string(s, model_cls): try: parts = [p.decode('utf8') for p in shlex.split(s)] except ValueError as exc: - raise ValueError("Cannot parse {0!r} (error was: {1})".format(s, exc)) + raise dbcore.InvalidQueryError(s, exc) return parse_query_parts(parts, model_cls) @@ -1155,11 +1155,14 @@ class Library(dbcore.Database): in the query string the `sort` argument is ignored. """ # Parse the query, if necessary. - parsed_sort = None - if isinstance(query, basestring): - query, parsed_sort = parse_query_string(query, model_cls) - elif isinstance(query, (list, tuple)): - query, parsed_sort = parse_query_parts(query, model_cls) + try: + parsed_sort = None + if isinstance(query, basestring): + query, parsed_sort = parse_query_string(query, model_cls) + elif isinstance(query, (list, tuple)): + query, parsed_sort = parse_query_parts(query, model_cls) + except dbcore.query.InvalidQueryArgumentTypeError as exc: + raise dbcore.InvalidQueryError(query, exc) # Any non-null sort specified by the parsed query overrides the # provided sort. diff --git a/test/test_library.py b/test/test_library.py index 3848f2b7c..0bac0f173 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -31,6 +31,7 @@ from test._common import unittest from test._common import item import beets.library import beets.mediafile +import beets.dbcore.query from beets import util from beets import plugins from beets import config @@ -1171,6 +1172,14 @@ class ItemReadTest(unittest.TestCase): item.read('/thisfiledoesnotexist') +class ParseQueryTest(unittest.TestCase): + def test_parse_invalid_query_string(self): + with self.assertRaises(beets.dbcore.InvalidQueryError) as raised: + beets.library.parse_query_string('foo"', None) + self.assertIsInstance(raised.exception, + beets.dbcore.query.ParsingError) + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/test/test_query.py b/test/test_query.py index 55450d0ad..a9b1058bd 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -24,7 +24,8 @@ from test import helper import beets.library from beets import dbcore from beets.dbcore import types -from beets.dbcore.query import NoneQuery, InvalidQueryError +from beets.dbcore.query import (NoneQuery, ParsingError, + InvalidQueryArgumentTypeError) from beets.library import Library, Item @@ -282,14 +283,15 @@ class GetTest(DummyDataTestCase): self.assertFalse(results) def test_invalid_query(self): - with self.assertRaises(InvalidQueryError) as raised: + with self.assertRaises(InvalidQueryArgumentTypeError) as raised: dbcore.query.NumericQuery('year', '199a') self.assertIn('not an int', unicode(raised.exception)) - with self.assertRaises(InvalidQueryError) as raised: + with self.assertRaises(InvalidQueryArgumentTypeError) as raised: dbcore.query.RegexpQuery('year', '199(') self.assertIn('not a regular expression', unicode(raised.exception)) self.assertIn('unbalanced parenthesis', unicode(raised.exception)) + self.assertIsInstance(raised.exception, ParsingError) class MatchTest(_common.TestCase):