From 55eed7c378723cf31963f85e23bf6fd2df7297ad Mon Sep 17 00:00:00 2001 From: Andrzej Ciarkowski Date: Sat, 27 Feb 2016 17:00:34 +0100 Subject: [PATCH] pathrender: Add unit tests module for pathrender.py --- headphones/pathrender.py | 68 ++++++++++++++++------- headphones/pathrender_test.py | 100 ++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 19 deletions(-) create mode 100644 headphones/pathrender_test.py 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')