From 3db5b772c5a35ee099b608072f35544e665741ce Mon Sep 17 00:00:00 2001 From: Federico Ressi Date: Wed, 16 Jun 2021 10:23:35 +0200 Subject: [PATCH] Add Selection.select method Change-Id: Id20887ebd5e463d31334f8f33a40ef273c5acfa0 --- tobiko/common/_select.py | 79 +++++++++------- tobiko/tests/unit/test_select.py | 149 +++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 33 deletions(-) create mode 100644 tobiko/tests/unit/test_select.py diff --git a/tobiko/common/_select.py b/tobiko/common/_select.py index dce20885e..ead7ee4b3 100644 --- a/tobiko/common/_select.py +++ b/tobiko/common/_select.py @@ -13,7 +13,7 @@ # under the License. from __future__ import absolute_import -import typing # noqa +import typing from tobiko import _exception @@ -23,24 +23,38 @@ T = typing.TypeVar('T') class Selection(list, typing.Generic[T]): - def with_attributes(self, **attributes): - return self.create( - filter_by_attributes(self, exclude=False, **attributes)) + def with_attributes(self, **attributes) -> 'Selection[T]': + return self.select(lambda obj: equal_attributes(obj, attributes)) - def without_attributes(self, **attributes): - return self.create( - filter_by_attributes(self, exclude=True, **attributes)) + def without_attributes(self, **attributes) -> 'Selection[T]': + return self.select(lambda obj: equal_attributes(obj, attributes, + inverse=True)) - def with_items(self, **items): - return self.create(filter_by_items(self, exclude=False, **items)) + def with_items(self: 'Selection[typing.Dict]', **items) \ + -> 'Selection[typing.Dict]': + return self.select(lambda obj: equal_items(obj, items)) - def without_items(self, **items): - return self.create(filter_by_items(self, exclude=True, **items)) + def without_items(self: 'Selection[typing.Dict]', **items) -> \ + 'Selection[typing.Dict]': + return self.select(lambda obj: equal_items(obj, items, inverse=True)) @classmethod - def create(cls, objects: typing.Iterable[T]): + def create(cls, objects: typing.Iterable[T]) -> 'Selection[T]': return cls(objects) + def select(self, + predicate: typing.Callable[[T], bool], + expect=True) \ + -> 'Selection[T]': + return self.create(obj + for obj in self + if bool(predicate(obj)) is expect) + + def unselect(self, + predicate: typing.Callable[[T], typing.Any]) \ + -> 'Selection[T]': + return self.select(predicate, expect=False) + @property def first(self) -> T: if self: @@ -56,38 +70,37 @@ class Selection(list, typing.Generic[T]): return self.first def __repr__(self): - return '{!s}({!r})'.format(type(self).__name__, list(self)) + return f'{type(self).__name__}({list(self)!r})' def select(objects: typing.Iterable[T]) -> Selection[T]: return Selection.create(objects) -def filter_by_attributes(objects, exclude=False, **attributes): - exclude = bool(exclude) - for obj in objects: - for key, value in attributes.items(): - matching = value == getattr(obj, key) - if matching is exclude: - break - else: - yield obj +def equal_attributes(obj, + attributes: typing.Dict[str, typing.Any], + inverse=False) \ + -> bool: + for key, value in attributes.items(): + matching = value == getattr(obj, key) + if matching is inverse: + return False + return True -def filter_by_items(dictionaries, exclude=False, **items): - exclude = bool(exclude) - for dictionary in dictionaries: - for key, value in items.items(): - matching = value == dictionary[key] - if matching is exclude: - break - else: - yield dictionary +def equal_items(obj: typing.Dict, + items: typing.Dict, + inverse=False) -> bool: + for key, value in items.items(): + matching = value == obj[key] + if matching is inverse: + return False + return True class ObjectNotFound(_exception.TobikoException): - "Object not found" + message = "Object not found" class MultipleObjectsFound(_exception.TobikoException): - "Multiple objects found: {objects!r}" + message = "Multiple objects found: {objects!r}" diff --git a/tobiko/tests/unit/test_select.py b/tobiko/tests/unit/test_select.py new file mode 100644 index 000000000..191d7689f --- /dev/null +++ b/tobiko/tests/unit/test_select.py @@ -0,0 +1,149 @@ +# Copyright (c) 2021 Red Hat +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import absolute_import + +import collections +import typing + +import tobiko +from tobiko.tests import unit + + +def condition(value): + return value + + +class Obj(typing.NamedTuple): + number: int = 0 + text: str = '' + + +T = typing.TypeVar('T') + + +class SelectionTest(unit.TobikoUnitTest): + + @staticmethod + def create_selection(*args, **kwargs): + return tobiko.Selection(*args, **kwargs) + + def test_selection(self, + objects: typing.Iterable[T] = tuple()) \ + -> tobiko.Selection[T]: + reference = list(objects) + if isinstance(objects, collections.Generator): + # Can't reiterate the same generator twice + objects = (o for o in reference) + assert isinstance(objects, collections.Generator) + elif isinstance(objects, collections.Iterator): + # Can't reiterate the same iterator twice + objects = iter(reference) + assert isinstance(objects, collections.Iterator) + + selection = self.create_selection(objects) + self.assertIsInstance(selection, list) + self.assertIsInstance(selection, tobiko.Selection) + self.assertEqual(reference, selection) + self.assertEqual(selection, reference) + return selection + + def test_selection_with_list(self): + self.test_selection([1, 'a', 3.14]) + + def test_selection_with_tuple(self): + self.test_selection((1, 'a', 3.14)) + + def test_selection_with_generator(self): + self.test_selection(c for c in 'hello') + + def test_selection_with_iterator(self): + self.test_selection(iter('hello')) + + def test_with_attribute(self): + a = Obj(0, 'a') + b = Obj(1, 'b') + c = Obj(1, 'c') + selection = self.create_selection([a, b, c]) + self.assertEqual([a], selection.with_attributes(text='a')) + self.assertEqual([b], selection.with_attributes(text='b')) + self.assertEqual([c], selection.with_attributes(text='c')) + self.assertEqual([a], selection.with_attributes(number=0)) + self.assertEqual([b, c], selection.with_attributes(number=1)) + self.assertEqual([], selection.with_attributes(number=2)) + self.assertEqual([b], selection.with_attributes(number=1, text='b')) + self.assertEqual([], selection.with_attributes(number=1, text='a')) + + def test_without_attribute(self): + a = Obj(0, 'a') + b = Obj(1, 'b') + c = Obj(1, 'c') + selection = self.create_selection([a, b, c]) + self.assertEqual([b, c], selection.without_attributes(text='a')) + self.assertEqual([a, c], selection.without_attributes(text='b')) + self.assertEqual([a, b], selection.without_attributes(text='c')) + self.assertEqual([b, c], selection.without_attributes(number=0)) + self.assertEqual([a], selection.without_attributes(number=1)) + self.assertEqual([a, b, c], selection.without_attributes(number=2)) + self.assertEqual([a], selection.without_attributes(number=1, text='b')) + self.assertEqual([], selection.without_attributes(number=1, text='a')) + + def test_with_items(self): + a = {'number': 0, 'text': 'a'} + b = {'number': 1, 'text': 'b'} + c = {'number': 1, 'text': 'c'} + selection = self.create_selection([a, b, c]) + self.assertEqual([a], selection.with_items(text='a')) + self.assertEqual([b], selection.with_items(text='b')) + self.assertEqual([c], selection.with_items(text='c')) + self.assertEqual([a], selection.with_items(number=0)) + self.assertEqual([b, c], selection.with_items(number=1)) + self.assertEqual([], selection.with_items(number=2)) + self.assertEqual([b], selection.with_items(number=1, text='b')) + self.assertEqual([], selection.with_items(number=1, text='a')) + + def test_without_items(self): + a = {'number': 0, 'text': 'a'} + b = {'number': 1, 'text': 'b'} + c = {'number': 1, 'text': 'c'} + selection = self.create_selection([a, b, c]) + self.assertEqual([b, c], selection.without_items(text='a')) + self.assertEqual([a, c], selection.without_items(text='b')) + self.assertEqual([a, b], selection.without_items(text='c')) + self.assertEqual([b, c], selection.without_items(number=0)) + self.assertEqual([a], selection.without_items(number=1)) + self.assertEqual([a, b, c], selection.without_items(number=2)) + self.assertEqual([a], selection.without_items(number=1, text='b')) + self.assertEqual([], selection.without_items(number=1, text='a')) + + def test_select(self): + a = Obj(0, 'a') + b = Obj(1, 'b') + c = Obj(1, 'c') + selection = self.create_selection([a, b, c]) + self.assertEqual([b, c], selection.select(lambda obj: obj.number == 1)) + + def test_unselect(self): + a = Obj(0, 'a') + b = Obj(1, 'b') + c = Obj(1, 'c') + selection = self.create_selection([a, b, c]) + self.assertEqual([a], selection.unselect(lambda obj: obj.number == 1)) + + +class SelectTest(SelectionTest): + + @staticmethod + def create_selection(*args, **kwargs): + return tobiko.select(*args, **kwargs)