diff --git a/daffodil/__init__.py b/daffodil/__init__.py index d9316c9..e2a7f41 100644 --- a/daffodil/__init__.py +++ b/daffodil/__init__.py @@ -1,4 +1,5 @@ from .predicate import DictionaryPredicateDelegate from .hstore_predicate import HStoreQueryDelegate from .pretty_print import PrettyPrintDelegate +from .key_expectation_delegate import KeyExpectationDelegate from .parser import Daffodil diff --git a/daffodil/base_delegate.py b/daffodil/base_delegate.py new file mode 100644 index 0000000..c4ad438 --- /dev/null +++ b/daffodil/base_delegate.py @@ -0,0 +1,29 @@ +from builtins import object + + +class BaseDaffodilDelegate(object): + + def mk_any(self, children): + raise NotImplementedError() + + def mk_all(self, children): + raise NotImplementedError() + + def mk_not_any(self, children): + raise NotImplementedError() + + def mk_not_all(self, children): + raise NotImplementedError() + + def mk_test(self, test_str): + raise NotImplementedError() + + def mk_comment(self, comment, is_inline): + raise NotImplementedError() + + def mk_cmp(self, key, val, test): + raise NotImplementedError() + + def call(self, predicate, iterable): + raise NotImplementedError() + diff --git a/daffodil/hstore_predicate.py b/daffodil/hstore_predicate.py index d4c0eed..9212b21 100644 --- a/daffodil/hstore_predicate.py +++ b/daffodil/hstore_predicate.py @@ -2,9 +2,11 @@ standard_library.install_aliases() from builtins import str from past.builtins import basestring -from builtins import object from collections import UserString +from .base_delegate import BaseDaffodilDelegate + + RE_CASE = "CASE WHEN ({0}->'{1}' ~ E'" RE_THEN = "') THEN " RE_ELSE = "ELSE -2147483648 END" @@ -38,7 +40,7 @@ def make_sql_array(*strings): ",".join(escape_string_sql(s) for s in strings) ) -class HStoreQueryDelegate(object): +class HStoreQueryDelegate(BaseDaffodilDelegate): def __init__(self, hstore_field_name): self.field = hstore_field_name diff --git a/daffodil/key_expectation_delegate.py b/daffodil/key_expectation_delegate.py new file mode 100644 index 0000000..d8368ac --- /dev/null +++ b/daffodil/key_expectation_delegate.py @@ -0,0 +1,59 @@ +from .base_delegate import BaseDaffodilDelegate + + +class KeyExpectationDelegate(BaseDaffodilDelegate): + """ + Determines which keys in a daffodil are required in data dictionaries + in order to match and which keys have to be omitted to match. + + Useful for making inferences like detecting when a key would never be set + but should (or would be but shouldn't). + """ + def _mk_group(self, children, negate): + expect_present = set() + expect_omitted = set() + + for child in children: + if negate: + child = child[::-1] + expect_present |= child[0] + expect_omitted |= child[1] + + # if we expect a key to be present we can't also expect it not to be + expect_omitted -= expect_present + + return expect_present, expect_omitted + + def mk_any(self, children): + return self._mk_group(children, False) + + def mk_all(self, children): + return self._mk_group(children, False) + + def mk_not_any(self, children): + return self._mk_group(children, True) + + def mk_not_all(self, children): + return self._mk_group(children, True) + + def mk_test(self, test_str): + if test_str != "?=": + return test_str + + def test_fn(k, v, t): + return v + + test_fn.is_datapoint_test = True + test_fn.test_str = test_str + + return test_fn + + def mk_comment(self, comment, is_inline): + return set(), set() + + def mk_cmp(self, key, val, test): + existance = getattr(test, "is_datapoint_test", False) + + if existance and val is False: + return set(), {key} + return {key}, set() \ No newline at end of file diff --git a/daffodil/parser.py b/daffodil/parser.py index 659c198..29ff3f7 100644 --- a/daffodil/parser.py +++ b/daffodil/parser.py @@ -342,11 +342,14 @@ def read_quoted_string(self): class Daffodil(object): def __init__(self, source, delegate=DictionaryPredicateDelegate()): - parse_result = DaffodilParser(source) + if isinstance(source, DaffodilParser): + self.parse_result = source + else: + self.parse_result = DaffodilParser(source) self.keys = set() self.delegate = delegate - self.predicate = self.make_predicate(parse_result.tokens) + self.predicate = self.make_predicate(self.parse_result.tokens) def _handle_group(self, parent, children): lookup = { diff --git a/daffodil/predicate.py b/daffodil/predicate.py index 0006d4a..2da29b0 100644 --- a/daffodil/predicate.py +++ b/daffodil/predicate.py @@ -1,11 +1,11 @@ from builtins import filter from past.builtins import basestring -from builtins import object import operator as op +from .base_delegate import BaseDaffodilDelegate -class DictionaryPredicateDelegate(object): +class DictionaryPredicateDelegate(BaseDaffodilDelegate): def _mk_any_all(self, children, any_all): return lambda data_point: any_all( predicate(data_point) diff --git a/daffodil/pretty_print.py b/daffodil/pretty_print.py index cda2f2d..7c74f4f 100644 --- a/daffodil/pretty_print.py +++ b/daffodil/pretty_print.py @@ -2,9 +2,10 @@ standard_library.install_aliases() from builtins import str from past.builtins import basestring -from builtins import object from collections import UserList +from .base_delegate import BaseDaffodilDelegate + def to_daffodil_primitive(val): if isinstance(val, list): @@ -139,7 +140,7 @@ def format_std(self, children): ) -class PrettyPrintDelegate(object): +class PrettyPrintDelegate(BaseDaffodilDelegate): def __init__(self, dense=True): self.dense = dense diff --git a/setup.py b/setup.py index 45bb1a6..1ba7efa 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='daffodil', - version='0.3.16', + version='0.3.17', author='James Robert', description='A Super-simple DSL for filtering datasets', license='MIT', diff --git a/test/tests.py b/test/tests.py index f94e573..f8aeee0 100644 --- a/test/tests.py +++ b/test/tests.py @@ -8,7 +8,11 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '..')) -from daffodil import Daffodil, PrettyPrintDelegate +from daffodil import ( + Daffodil, + KeyExpectationDelegate, DictionaryPredicateDelegate, + HStoreQueryDelegate, PrettyPrintDelegate +) from daffodil.exceptions import ParseError @@ -33,12 +37,19 @@ def filter(self, daff_src): return Daffodil(daff_src)(self.d) -class ParserGrammarTypesTests(BaseTest): +class ParserGrammarTypesTests(unittest.TestCase): + def parse(self, daff_src, delegate): + return Daffodil(daff_src, delegate=delegate) + def test_existence_doesnt_expect_string(self): - with self.assertRaises(ValueError): - self.filter('whatever ?= "true"') - self.filter('whatever ?= "False"') - self.filter('whatever ?= "any string"') + for delegate in [ + HStoreQueryDelegate(hstore_field_name="dummy_name"), + DictionaryPredicateDelegate(), KeyExpectationDelegate() + ]: + with self.assertRaises(ValueError): + self.parse('whatever ?= "true"', delegate) + self.parse('whatever ?= "False"', delegate) + self.parse('whatever ?= "any string"', delegate) class SATDataTests(BaseTest): @@ -775,6 +786,50 @@ def test_not_matching(self): self.assertFalse(daff.predicate(self.data)) +class KeyExpectationTests(unittest.TestCase): + + def assert_daffodil_expectations(self, dafltr, present=set(), omitted=set()): + daff = Daffodil(dafltr, delegate=KeyExpectationDelegate()) + daff_expected_present, daff_expected_omitted = daff.predicate + self.assertEqual(daff_expected_present, present) + self.assertEqual(daff_expected_omitted, omitted) + + + def test_key_expectations(self): + self.assert_daffodil_expectations( + "x = 1, y = true", + present={"x", "y"} + ) + self.assert_daffodil_expectations( + "x ?= true, y ?= false", + present={"x"}, omitted={"y"} + ) + self.assert_daffodil_expectations( + "!{x ?= true, y ?= false}", + present={"y"}, omitted={"x"} + ) + self.assert_daffodil_expectations( + """ + !{ + [ + x ?= true + y ?= false + ] + ![ + z = "a" + { + a = 1 + b != 2 + c > 10 + d < 9 + } + ] + } + a ?= false + """, + present={"a", "b", "c", "d", "y", "z"}, omitted={"x"} + ) + # input, expected_dense, expected_pretty PRETTY_PRINT_EXPECTATIONS = (