diff --git a/headphones/pathrender.py b/headphones/pathrender.py
index 162c6956..48798dd7 100644
--- a/headphones/pathrender.py
+++ b/headphones/pathrender.py
@@ -13,22 +13,23 @@
#
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see .
-'''Path pattern substitution module, see details below for syntax.
+"""
+Path pattern substitution module, see details below for syntax.
- The pattern matching is loosely based on foobar2000 pattern syntax,
- i.e. the notion of escaping characters with \' and optional elements
- enclosed in square brackets [] is taken from there while the
- substitution variable names are Perl-ish or sh-ish. The following
- syntax elements are supported:
- * escaped literal strings, that is everything that is enclosed
- within single quotes (like \'this\');
- * substitution variables, which start with dollar sign ($) and
- extend until next non-alphanumeric+underscore character
- (like $This and $5_that).
- * optional elements enclosed in curly braces, which render
- nonempty value only if any variable or optional inside returned
- nonempty value, ignoring literals (like {\'[\'$That\']\'}).
-'''
+The pattern matching is loosely based on foobar2000 pattern syntax,
+i.e. the notion of escaping characters with \' and optional elements
+enclosed in square brackets [] is taken from there while the
+substitution variable names are Perl-ish or sh-ish. The following
+syntax elements are supported:
+* escaped literal strings, that is everything that is enclosed
+ within single quotes (like 'this');
+* substitution variables, which start with dollar sign ($) and
+ extend until next non-alphanumeric+underscore character
+ (like $This and $5_that).
+* optional elements enclosed in curly braces, which render
+ nonempty value only if any variable or optional inside returned
+ nonempty value, ignoring literals (like {'{'$That'}'}).
+"""
from __future__ import print_function
from enum import Enum
@@ -42,6 +43,9 @@ class _PatternElement(object):
'''Format this _PatternElement into string using provided substitution dictionary.'''
raise NotImplementedError()
+ def __ne__(self, other):
+ return not self == other
+
class _Generator(_PatternElement):
# pylint: disable=abstract-method
@@ -57,11 +61,23 @@ class _Replacement(_Generator):
def render(self, replacement):
# type: (Mapping[str,str]) -> str
- return replacement.get(self._pattern, self._pattern)
+ res = replacement.get(self._pattern, self._pattern)
+ if res is None:
+ return ''
+ else:
+ return res
def __str__(self):
return self._pattern
+ @property
+ def pattern(self):
+ return self._pattern
+
+ def __eq__(self, other):
+ return isinstance(other, _Replacement) and \
+ self._pattern == other.pattern
+
class _LiteralText(_PatternElement):
'''Just a plain piece of text to be rendered "as is".'''
@@ -76,6 +92,13 @@ class _LiteralText(_PatternElement):
def __str__(self):
return self._text
+ @property
+ def text(self):
+ return self._text
+
+ def __eq__(self, other):
+ return isinstance(other, _LiteralText) and self._text == other.text
+
class _OptionalBlock(_Generator):
'''Optional block will render its contents only if any _Generator in its scope did return non-empty result.'''
@@ -87,11 +110,17 @@ class _OptionalBlock(_Generator):
def render(self, replacement):
# type: (Mapping[str,str]) -> str
res = [(isinstance(x, _Generator), x.render(replacement)) for x in self._scope]
- if any((t[0] and len(t[1]) != 0) for t in res):
+ if any((t[0] and t[1] is not None and len(t[1]) != 0) for t in res):
return u"".join(t[1] for t in res)
else:
return u""
+ def __eq__(self, other):
+ """
+ :type other: _OptionalBlock
+ """
+ return isinstance(other, _OptionalBlock) and self._scope == other._scope
+
_OPTIONAL_START = u'{'
_OPTIONAL_END = u'}'
@@ -230,8 +259,9 @@ def render(pattern, replacement):
p = Pattern(pattern)
return p(replacement), p.warnings
+
if __name__ == "__main__":
# primitive test ;)
- p = Pattern(u"[$Disc.]$Track - $Artist - $Title[ '['$Year']'")
+ p = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]}")
d = {'$Disc': '', '$Track': '05', '$Artist': u'Grzegżółka', '$Title': u'Błona kapłona', '$Year': '2019'}
- print(p(d).encode('utf8'), p.warnings)
+ assert p(d) == u"05 - Grzegżółka - Błona kapłona [2019]"
diff --git a/headphones/pathrender_test.py b/headphones/pathrender_test.py
new file mode 100644
index 00000000..4c24b99f
--- /dev/null
+++ b/headphones/pathrender_test.py
@@ -0,0 +1,100 @@
+# encoding=utf8
+# This file is part of Headphones.
+#
+# Headphones is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Headphones is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Headphones. If not, see .
+"""
+Test module for pathrender.
+"""
+import headphones.pathrender as _pr
+from headphones.pathrender import Pattern, Warnings
+
+from unittestcompat import TestCase
+
+
+__author__ = "Andrzej Ciarkowski "
+
+
+class PathRenderTest(TestCase):
+ """
+ Tests for pathrender module.
+ """
+
+ def test_parsing(self):
+ """pathrender: pattern parsing"""
+ pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]}")
+ expected = [
+ _pr._OptionalBlock([
+ _pr._Replacement(u"$Disc"),
+ _pr._LiteralText(u".")
+ ]),
+ _pr._Replacement(u"$Track"),
+ _pr._LiteralText(u" - "),
+ _pr._Replacement(u"$Artist"),
+ _pr._LiteralText(u" - "),
+ _pr._Replacement(u"$Title"),
+ _pr._OptionalBlock([
+ _pr._LiteralText(u" ["),
+ _pr._Replacement(u"$Year"),
+ _pr._LiteralText(u"]")
+ ])
+ ]
+ self.assertEqual(expected, pattern._pattern)
+ self.assertItemsEqual([], pattern.warnings)
+
+ def test_parsing_warnings(self):
+ """pathrender: pattern parsing with warnings"""
+ pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]")
+ self.assertEqual(set([Warnings.UNCLOSED_OPTIONAL]), pattern.warnings)
+ pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]'}")
+ self.assertEqual(set([
+ Warnings.UNCLOSED_ESCAPE,
+ Warnings.UNCLOSED_OPTIONAL
+ ]),
+ pattern.warnings)
+
+ def test_replacement(self):
+ """pathrender: _Replacement variable substitution"""
+ r = _pr._Replacement(u"$Title")
+ subst = {'$Title': 'foo', '$Track': 'bar'}
+ res = r.render(subst)
+ self.assertEqual(res, u'foo', 'check valid replacement')
+ subst = {}
+ res = r.render(subst)
+ self.assertEqual(res, u'$Title', 'check missing replacement')
+ subst = {'$Title': None}
+ res = r.render(subst)
+ self.assertEqual(res, '', 'check render() works with None')
+
+ def test_literal(self):
+ """pathrender: _Literal text rendering"""
+ l = _pr._LiteralText(u"foo")
+ subst = {'$foo': 'bar'}
+ res = l.render(subst)
+ self.assertEqual(res, 'foo')
+
+ def test_optional(self):
+ """pathrender: _OptionalBlock element processing"""
+ o = _pr._OptionalBlock([
+ _pr._Replacement(u"$Title"),
+ _pr._LiteralText(u".foobar")
+ ])
+ subst = {'$Title': 'foo', '$Track': 'bar'}
+ res = o.render(subst)
+ self.assertEqual(res, u'foo.foobar', 'check non-empty replacement')
+ subst = {'$Title': ''}
+ res = o.render(subst)
+ self.assertEqual(res, '', 'check empty replacement')
+ subst = {'$Title': None}
+ res = o.render(subst)
+ self.assertEqual(res, '', 'check render() works with None')