From 11078491b9ccbafa03d5070309aeb89a195e9bc0 Mon Sep 17 00:00:00 2001 From: Kaliko Jack Date: Sat, 8 Apr 2023 19:00:24 +0200 Subject: [PATCH] Improved Range object to deal with window parameter Add unittest for Range object --- CHANGES.txt | 1 + doc/source/use.rst | 13 +++++++++---- musicpd.py | 40 +++++++++++++++++++++++++++------------- test.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 17 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index fc44e05..31706e2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,6 +7,7 @@ Changes in 0.9.0 * Use right SPDX identifier for license headers * mpd_version attribute init to empty string instead of None * Fixed send_noidle (introduced with e8daa719) + * Improved Range object to deal with window parameter Changes in 0.8.0 ---------------- diff --git a/doc/source/use.rst b/doc/source/use.rst index 86c3e72..213491e 100644 --- a/doc/source/use.rst +++ b/doc/source/use.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: 2018-2021 kaliko +.. SPDX-FileCopyrightText: 2018-2023 kaliko .. SPDX-License-Identifier: LGPL-3.0-or-later Using the client library @@ -86,15 +86,18 @@ Command lists are also supported using `command_list_ok_begin()` and Ranges ------ -Provide a 2-tuple as argument for command supporting ranges (cf. `MPD protocol documentation`_ for more details). -Possible ranges are: "START:END", "START:" and ":" : +Some commands (e.g. delete) allow specifying a range in the form `"START:END"` (cf. `MPD protocol documentation`_ for more details). + +Possible ranges are: `"START:END"`, `"START:"` and `":"` : + +Instead of giving the plain string as `"START:END"`, you **can** provide a :py:obj:`tuple` as `(START,END)`. The module is then ensuring the format is correct and raises an :py:obj:`musicpd.CommandError` exception otherwise. Empty start or end can be specified as en empty string `''` or :py:obj:`None`. .. code-block:: python # An intelligent clear # clears played track in the queue, currentsong included pos = client.currentsong().get('pos', 0) - # the 2-tuple range object accepts str, no need to convert to int + # the range object accepts str, no need to convert to int client.delete((0, pos)) # missing end interpreted as highest value possible, pay attention still need a tuple. client.delete((pos,)) # purge queue from current to the end @@ -109,6 +112,8 @@ as a single colon as argument (i.e. sending just ":"): Empty start in range (i.e. ":END") are not possible and will raise a CommandError. +Remember the of the tuple is optional, range can still be specified as single string `START:END`. In case of malformed range a CommandError is still raised. + Iterators ---------- diff --git a/musicpd.py b/musicpd.py index 25abfd9..7d0918c 100644 --- a/musicpd.py +++ b/musicpd.py @@ -75,28 +75,42 @@ class Range: def __init__(self, tpl): self.tpl = tpl + self.lower = '' + self.upper = '' self._check() def __str__(self): - if len(self.tpl) == 0: - return ':' - if len(self.tpl) == 1: - return '{0}:'.format(self.tpl[0]) - return '{0[0]}:{0[1]}'.format(self.tpl) + return f'{self.lower}:{self.upper}' def __repr__(self): - return 'Range({0})'.format(self.tpl) + return f'Range({self.tpl})' + + def _check_element(self, item): + if item is None or item == '': + return '' + try: + return str(int(item)) + except (TypeError, ValueError) as err: + raise CommandError(f'Not an integer: "{item}"') from err + return item def _check(self): if not isinstance(self.tpl, tuple): raise CommandError('Wrong type, provide a tuple') - if len(self.tpl) not in [0, 1, 2]: - raise CommandError('length not in [0, 1, 2]') - for index in self.tpl: - try: - index = int(index) - except (TypeError, ValueError) as err: - raise CommandError('Not a tuple of int') from err + if len(self.tpl) == 0: + return + if len(self.tpl) == 1: + self.lower = self._check_element(self.tpl[0]) + return + if len(self.tpl) != 2: + raise CommandError('Range wrong size (0, 1 or 2 allowed)') + self.lower = self._check_element(self.tpl[0]) + self.upper = self._check_element(self.tpl[1]) + if self.lower == '' and self.upper != '': + raise CommandError(f'Integer expected to start the range: {self.tpl}') + if self.upper.isdigit() and self.lower.isdigit(): + if int(self.lower) > int(self.upper): + raise CommandError(f'Wrong range: {self.lower} > {self.upper}') class _NotConnected: diff --git a/test.py b/test.py index d90bd40..0677905 100755 --- a/test.py +++ b/test.py @@ -614,5 +614,35 @@ class testContextManager(unittest.TestCase): sock.close.assert_not_called() sock.close.assert_called() +class testRange(unittest.TestCase): + + def test_range(self): + tests = [ + ((), ':'), + ((None,None), ':'), + (('',''), ':'), + (('',), ':'), + ((42,42), '42:42'), + ((42,), '42:'), + (('42',), '42:'), + (('42',None), '42:'), + (('42',''), '42:'), + ] + for tpl, result in tests: + self.assertEqual(str(musicpd.Range(tpl)), result) + with self.assertRaises(musicpd.CommandError): + #CommandError: Integer expected to start the range: (None, 42) + musicpd.Range((None,'42')) + with self.assertRaises(musicpd.CommandError): + # CommandError: Not an integer: "foo" + musicpd.Range(('foo',)) + with self.assertRaises(musicpd.CommandError): + # CommandError: Wrong range: 42 > 41 + musicpd.Range(('42',41)) + with self.assertRaises(musicpd.CommandError): + # CommandError: Wrong range: 42 > 41 + musicpd.Range(('42','42','42')) + + if __name__ == '__main__': unittest.main() -- 2.39.5