From 29d3e63324e61feab5385ab98a626c54ec8ab1a1 Mon Sep 17 00:00:00 2001 From: bytebutcher Date: Thu, 6 Jul 2023 20:11:59 +0200 Subject: [PATCH] #7 Adds support for filtering objects --- pydictdisplayfilter/__init__.py | 3 +- pydictdisplayfilter/display_filters.py | 34 +++++++++- setup.py | 2 +- tests/test_object_display_filter.py | 93 ++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 tests/test_object_display_filter.py diff --git a/pydictdisplayfilter/__init__.py b/pydictdisplayfilter/__init__.py index ac26bd0..e2dceae 100644 --- a/pydictdisplayfilter/__init__.py +++ b/pydictdisplayfilter/__init__.py @@ -14,4 +14,5 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from pydictdisplayfilter.display_filters import DictDisplayFilter, ListDisplayFilter \ No newline at end of file +from pydictdisplayfilter.display_filters import \ + DictDisplayFilter, ListDisplayFilter, ObjectDisplayFilter, SQLDisplayFilter diff --git a/pydictdisplayfilter/display_filters.py b/pydictdisplayfilter/display_filters.py index d852c53..c4a59a2 100644 --- a/pydictdisplayfilter/display_filters.py +++ b/pydictdisplayfilter/display_filters.py @@ -122,7 +122,7 @@ def filter(self, display_filter: str): class DictDisplayFilter(BaseDisplayFilter): - """ Allows to filter a dictionary using a display filter. """ + """ Allows to filter a list of dictionaries using a display filter. """ def __init__(self, data: List[dict], @@ -138,7 +138,7 @@ def __init__(self, self._data = data def filter(self, display_filter: str): - """ Filters the data using the display filter. """ + """ Filters the dictionaries using the display filter. """ expressions = self._display_filter_parser.parse(display_filter) yield from self._filter_data(self._data, expressions) @@ -246,3 +246,33 @@ def filter(self, display_filter: str): expressions = self._display_filter_parser.parse(display_filter) table_data = self._get_table_data() yield from self._filter_data(table_data, expressions) + + +class ObjectDisplayFilter(BaseDisplayFilter): + """ Allows to filter a list of objects using a display filter. """ + + def __init__(self, + data: List[object], + field_names: List[str] = None, + functions: Dict[str, Callable] = None, + slicers: List[BasicSlicer] = None, + evaluator: Evaluator = None): + """ + Initializes the DictDisplayFilter. + :param data: A list of dictionaries to filter on. + """ + super().__init__(field_names=field_names, functions=functions, slicers=slicers, evaluator=evaluator) + self._data = data + + def _filter_data(self, data: List[object], expressions: List[Union[Expression, str]]) -> List: + if expressions: + for item in data: + if self._evaluate_expressions(expressions, item.__dict__): + yield item + else: + yield from data + + def filter(self, display_filter: str): + """ Filters the objects using the display filter. """ + expressions = self._display_filter_parser.parse(display_filter) + yield from self._filter_data(self._data, expressions) diff --git a/setup.py b/setup.py index 2e04169..9ccbab7 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # This call to setup() does all the work setup( name="python-dict-display-filter", - version="1.1.0", + version="1.2.0", description="A Wireshark-like display filter for dictionaries.", long_description=README, long_description_content_type="text/markdown", diff --git a/tests/test_object_display_filter.py b/tests/test_object_display_filter.py new file mode 100644 index 0000000..80ce5bc --- /dev/null +++ b/tests/test_object_display_filter.py @@ -0,0 +1,93 @@ +# vim: ts=8:sts=8:sw=8:noexpandtab +# +# This file is part of python-dict-display-filter. +# +# This program 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 2 of the License, or +# (at your option) any later version. +# +# This program 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 this program. If not, see . +import dataclasses +import unittest +from typing import List + +from parameterized import parameterized + +from pydictdisplayfilter.display_filters import ObjectDisplayFilter + + +class Person: + + def __init__(self, name: str = '', age: int = 0, gender: str = '', killed: bool = False, power: List = None): + self.name = name + self.age = age + self.gender = gender + self.killed = killed + self.power = power + + +class TestObjectDisplayFilter(unittest.TestCase): + # List of objects representing the data to filter on. + data = [ + Person(name="Morpheus", age=38, gender="male", killed=False), + Person(name="Neo", age=35, gender="male", killed=False, power=["flight", "bullet-time"]), + Person(name="Cipher", age=48, gender="male", killed=True), + Person(name="Trinity", age=32, gender="female", killed=False), + ] + + @parameterized.expand([ + # Field existence + ['name', 4], + ['power', 1], + ['not power', 3], + # Comparison operators + ['name == Neo', 1], + ['name == \x4e\x65\x6f', 1], + ['killed == True', 1], + ['gender == male', 3], + ['age == 32', 1], + ['age >= 32', 4], + ['age > 32', 3], + ['age <= 32', 1], + ['age <= 040', 1], # octal value + ['age <= 0x20', 1], # hexadecimal value + ['age < 32', 0], + ['age ~= 3', 3], # contains operator + ['age ~ 3', 3], # matches operator + ['age & 0x20', 4], # bitwise and operator + # In operator + ['age in { 32, 35, 38 }', 3], + ['age in { 30..40 }', 3], + ['age in { 30-40 }', 3], + ['age in { 30.0..40.0 }', 3], + ['name in { "Neo", "Trinity" }', 2], + # logical operators + ['age >= 32 and gender == male', 3], + ['name == Neo or name == Trinity', 2], + ['gender == female xor power', 2], + ['gender == male and (age > 30 and age < 40)', 2], + ['gender == male and not (age > 35)', 1], + ['gender == male and !(age > 35)', 1], + # Functions + ('len(name) == 3', 1), + ('upper(name) == NEO', 1), + ('lower(name) == neo', 1), + # Slice Operator + ('gender[0] == m', 3), + ('gender[-1] == e', 4), + ('gender[0:2] == ma', 3), + ('gender[:2] == ma', 3), + ('gender[2:] == le', 3), + ('gender[1-2] == ma', 3), + ('gender[0,1] == ma', 3), + ('gender[:2,3-4] == male', 3) + ]) + def test_object_display_filter_returns_correct_number_of_items(self, display_filter, no_items): + self.assertEqual(len(list(ObjectDisplayFilter(self.data).filter(display_filter))), no_items)