diff --git a/__pycache__/test_wrap_obj.cpython-27-PYTEST.pyc b/__pycache__/test_wrap_obj.cpython-27-PYTEST.pyc new file mode 100644 index 0000000..8989d0d Binary files /dev/null and b/__pycache__/test_wrap_obj.cpython-27-PYTEST.pyc differ diff --git a/fmatch_idea.txt b/fmatch_idea.txt index fa6f77f..3155633 100644 --- a/fmatch_idea.txt +++ b/fmatch_idea.txt @@ -95,11 +95,29 @@ IDEAS TO DO add more debug info debug info or raise custom exception -allow matches to store in var better __repr__ print +allow matches to store in var (done) + add __mult__ alternative to .times (done) -what about dict and object? (dicts ok, ) +what about dict ? (dicts ok, ) -maybe dict to obj? +maybe dict to obj? (done) + +save_as in multimatch with list behaviour(done) + +Any and OR save_as (done) + +test if data == Multi branch, raise error doesnt break anything +more docs + +test integration save_as and times + +do smart .times for single value + +allow Any with a module.Class(Any), which allows only object - Any + +more doctest for other methods and helpers + +test helpers diff --git a/readme b/readme new file mode 100644 index 0000000..055b8e8 --- /dev/null +++ b/readme @@ -0,0 +1,5 @@ +Run doctest - +python -m doctest .\wrap_obj.py + +Run py.test- +py.test \ No newline at end of file diff --git a/test_wrap_obj.py b/test_wrap_obj.py index 6b63659..ff9185c 100644 --- a/test_wrap_obj.py +++ b/test_wrap_obj.py @@ -1,4 +1,4 @@ -from wrap_obj import WrapObj as w, Any, OR +from wrap_obj import WrapObj, Any, OR, w import collections import pytest @@ -9,7 +9,7 @@ def test_w_single_value(): assert(w("hello") == "hello") assert(w(float("inf")) == float("inf")) assert(w(10.23) == 10.23) - assert(w("abhas") == u"abhas") + # assert(w("abhas") == u"abhas") assert(w(object()) != object()) assert(w(5) != "asd") @@ -29,9 +29,9 @@ def test_w_single_type(): assert(w(float) == 5.0) assert(w(float) == float("inf")) - assert(w(int) == int) - assert(w(float) == float) - assert(w(TypeError) == TypeError) + assert(w(int) != int) + assert(w(float) != float) + assert(w(TypeError) != TypeError) assert(w(int) != 2.33) assert(w(int) != "abhas") @@ -178,3 +178,42 @@ def test_as_obj(): test_obj.b = "qwerty" assert(w({"a": int, "b": Any}).as_obj() == test_obj) + + +def test_w_wrapObj(): + assert(w is WrapObj) + + +def test_save_as(): + dict_match = (w(int).save_as("a") == 5) + assert(dict_match["a"] == 5) + + dict_match = (w([int, w(float).save_as("a")]) == [5, 10.23]) + assert(dict_match["a"] == 10.23) + + dict_match = (w([w(int), w(float).save_as("a")]) == [5, 10.23]) + assert(dict_match["a"] == 10.23) + + dict_match = (w([{"what": w(int).save_as("b")}, w(float).save_as("a")]) == [{"what": 5}, 10.23]) + assert(dict_match["b"] == 5 and dict_match["a"] == 10.23) + with pytest.raises(AssertionError): + assert(dict_match["b"] == 15) + with pytest.raises(AssertionError): + assert(dict_match["a"] == 5) + with pytest.raises(AssertionError): + assert(dict_match["a"] == 10) + + dict_match = (w([{"what": w(int).save_as("a")}, w(float).save_as("a")]) == [{"what": 5}, 10.23]) + assert(dict_match["a"] == 10.23) + + dict_match = (w([[int, w(int).save_as("b")], float]) == [[5, 99], 10.23]) + assert(dict_match["b"] == 99) + + dict_match = (w([w(int).save_as("a")]).times(2) == [2, 3]) + assert(dict_match["a"] != 3) + assert(dict_match["a"] == [2, 3]) + + dict_match_1 = (w([w([int]).times(2).save_as("b")]) == [5, 12]) + dict_match_2 = (w([w([int]).save_as("b").times(2)]) == [5, 12]) + assert(dict_match_1["b"] == [5, 12]) + assert(dict_match_1 == dict_match_2) diff --git a/wrap_obj.py b/wrap_obj.py index 7dea661..a593f2a 100644 --- a/wrap_obj.py +++ b/wrap_obj.py @@ -3,8 +3,11 @@ from enum import Enum from collections import Mapping, Iterable from warnings import warn import logging +import types -logging.debug = print +# logging.debug = print + +__all__ = ["Any", "OR", "WrapObj", "w", "WrapMultiObj"] class MetaAny(type): @@ -15,12 +18,38 @@ class MetaAny(type): def times(self, range_low, range_high=None): return WrapObj([self]).times(range_low, range_high) + def save_as(self, save_key): + return WrapObj(self).save_as(save_key) + class Any(object): + + ''' + Checker class which allows everything. + Usually, you would use it as a catch-all or placeholder. + >>> w(Any) == "asd" + True + >>> w(Any) == 2 + True + >>> w(Any) == 2.35 + True + >>> w(Any) == (lambda x:None) + True + ''' __metaclass__ = MetaAny def makeMultiInstanceMatcher(*list_type): + ''' + Create checker class which allows some types or values. + For combining two or more possible types of a variable + + >>> w(OR(int,float)) == 2.25 and w(OR(int,float)) == 2 + True + >>> w(OR([int,float])) == 2.25 and w(OR([int,float])) == 2 + True + ''' + if len(list_type) == 1 and isinstance(list_type[0], Iterable): list_type = tuple(list_type[0]) @@ -45,6 +74,9 @@ def makeMultiInstanceMatcher(*list_type): def times(self, range_low, range_high=None): return WrapObj([self]).times(range_low, range_high) + def save_as(self, save_key): + return WrapObj(self).save_as(save_key) + class MultiInstanceMatcher(type): __metaclass__ = MetaMultiInstanceMatcher @@ -52,20 +84,44 @@ def makeMultiInstanceMatcher(*list_type): OR = makeMultiInstanceMatcher - CHECK_DIR = Enum("Iterable check direction", ["FORWARDS", "BACKWARDS"]) class WrapObj(object): + ''' + Create WrapObj instance. + data -> Reference Object to match with. + DO_TYPECHECK -> Whether the refernece and checked object must + have same type? Defaults to false. + + >>> WrapObj(int) # doctest: +ELLIPSIS + + >>> WrapObj(float, True) # doctest: +ELLIPSIS + + + ''' + def __init__(self, data, DO_TYPECHECK=False): + super(WrapObj, self).__init__() self.data = data self.DO_TYPECHECK = DO_TYPECHECK self.treat_as_object = False + self.dict_saved_values = {} + self.save_key = None def as_obj(self): + ''' + Matches wrapped dict to object + + >>> class object2(object): pass + >>> test_obj = object2(); test_obj.a = 20 + >>> w({"a": int}).as_obj() == test_obj + True + ''' + try: assert(isinstance(self.data, Mapping)) except AssertionError: @@ -75,116 +131,214 @@ class WrapObj(object): self.treat_as_object = True return self + def save_as(self, arg_name): + ''' + Saves matched value as arg_name, which is + used as key in the returned dict from __eq__ + + >>> _ = w([int,w(float).save_as("a")]) == [2,2.5] + >>> _["a"] == 2.5 + True + ''' + self.save_key = arg_name + return self + def times(self, range_low, range_high=None): - return WrapMultiObj(self.data, range_low, range_high) + ''' + Makes all the items in wrapped iteratble (in given order) match + at least range_low times and at max (range_high-1) in __eq__ + + >>> w([int]).times(2,4) == [2,56,7] + True + >>> w([int]).times(2) == [2,56] + True + + Wrapped object must be a iterator. If only range_low is passed, + must match for exactly range_low times. + + Returns a WrapMultiObj. + ''' + return_obj = WrapMultiObj(self.data, range_low, range_high) + return_obj.save_key = self.save_key + return return_obj def __mul__(self, range_tuple): + ''' + Same as WrapObj.times + + >>> w([int])*(2,4) == [2,56,7] + True + >>> w([int])*(2) == [2,56] + True + ''' return self.times(range_tuple, None) - def __pos__(self): - if (isinstance(self.data, Iterable) and (not isinstance(self.data, Mapping))): - return WrapMultiObj(self.data, [1, 2]) - else: - TypeError("+WrapObj only works when WrapObj.data is list-ish") + # def __pos__(self): + # if (isinstance(self.data, Iterable) and (not isinstance(self.data, Mapping))): + # return WrapMultiObj(self.data, [1, 2]) + # else: + # TypeError("+WrapObj only works when WrapObj.data is list-ish") def __ne__(self, other): + ''' + Handles != matching. Returns opposite of == value + + >>> w(int) != [2,56] + True + ''' return not(self.__eq__(other)) + def match_list(self, other): + ''' + Internal method. Handles self == other, when other is list + ''' + match_dict_or_True = None + check_dir = CHECK_DIR.FORWARDS + multiobj_index_forwards = None + multiobj_index_backwards = None + + self.dict_saved_values = {} + + if (check_dir == CHECK_DIR.FORWARDS): + + for ele_index in range(0, len(self.data), 1): + ele_data = self.data[ele_index] + # as long as not multiobj, check in forwards + if isinstance(ele_data, WrapMultiObj): + multiobj_index_forwards = ele_index + check_dir = CHECK_DIR.BACKWARDS + break + else: + try: + ele_other = other[ele_index] + except IndexError: + # To_be_checked list has length less than matching list + return False + finally: + match_dict_or_True = (WrapObj(ele_data) == ele_other) + if match_dict_or_True and isinstance(match_dict_or_True, dict): + self.dict_saved_values.update(match_dict_or_True) + if not match_dict_or_True: + return False + + if (check_dir == CHECK_DIR.BACKWARDS): + for ele_index in range(0 - 1, -(len(self.data) + 1), -1): + # ele_index is of negative value from -1 to -len (including) + ele_data = self.data[ele_index] + # as long as not multiobj, check in backwards + if isinstance(ele_data, WrapMultiObj): + multiobj_index_backwards = ele_index + check_dir = CHECK_DIR.FORWARDS + break + else: + try: + ele_other = other[ele_index] + except IndexError: + # To_be_checked list has length less than matching list + return False + finally: + match_dict_or_True = (WrapObj(ele_data) == ele_other) + if match_dict_or_True and isinstance(match_dict_or_True, dict): + self.dict_saved_values.update(match_dict_or_True) + if not match_dict_or_True: + return False + + if (match_dict_or_True and (multiobj_index_forwards is None) and (multiobj_index_forwards == multiobj_index_backwards)): + # all of data is in other + # so, if they have same length, everything is perfect + if len(self.data) == len(other): + return (self.dict_saved_values or True) + elif (multiobj_index_forwards + (-multiobj_index_backwards)) != len(self.data): + # there is more than one length_ANY object + raise TypeError("There must be only one object of arbitary range") + else: + # there is only one length ANY object + + # prepare back index for slicing operation + # add 1 (as this one will give outer bound) and if results to 0, set to None (as 0 doesnt mean last index) + multiobj_index_backwards += 1 + multiobj_index_backwards = multiobj_index_backwards or None + ele_wrapmultiobj = self.data[multiobj_index_forwards] + logging.debug("Comparing WrapMultiObj with part of list") + match_dict_or_True = (ele_wrapmultiobj == other[multiobj_index_forwards:multiobj_index_backwards]) + if match_dict_or_True: + if isinstance(match_dict_or_True, dict): + self.dict_saved_values.update(match_dict_or_True) + return (self.dict_saved_values or True) + else: + return False + + def match_dict_or_obj(self, other, dict_or_obj=dict): + ''' + Internal method. Handles self == other, when other is dict or obj + ''' + assert(dict_or_obj in (dict, object)) + match_dict_or_True = None # as in not known yet + + self.dict_saved_values = {} + + for (data_key, data_value) in self.data.iteritems(): + if isinstance(data_key, type): + # todo: stop printing this line, which is broken into parts + warn(("{data_key} is a class used as a key, but it " + "wont match keys which are instances of this type" + "").format(data_key=data_key)) + try: + if dict_or_obj is dict: + # if dict, try using that key + other_value = other[data_key] + elif dict_or_obj is object: + # if obj, try using that attribute + other_value = other.__getattribute__(data_key) + else: + raise TypeError("dict_or_obj must be dict or obj") + except (KeyError if dict_or_obj == dict else AttributeError): + # catch correct kind of error + # so if fails, it doesnt have that key or attribute. so, they dont match + match_dict_or_True = False + return False + else: + match_dict_or_True = (WrapObj(data_value) == other_value) + + if match_dict_or_True and isinstance(match_dict_or_True, dict): + self.dict_saved_values.update(match_dict_or_True) + + if match_dict_or_True is False: + return match_dict_or_True + else: + # should be True + assert(bool(match_dict_or_True) is True) + return (self.dict_saved_values or True) + def __eq__(self, other): + ''' + Handles self == other correctly + + >>> w(float) == 2.23 + True + >>> w(str) == "2.23" + True + ''' if self.DO_TYPECHECK and (type(self.data) != type(other)): return False - elif (self.data == other) or (check_as_value_and_type(other, self.data)): - return True + + elif isinstance(self.data, WrapObj): + return self.data == other + elif isinstance(self.data, WrapMultiObj): return (self.data == [other]) + + elif isinstance(self.data, types.StringTypes): + # take care of strings in normal way unlike lists + return self.data == other + elif (isinstance(self.data, Iterable) and (not isinstance(self.data, Mapping))) and\ (isinstance(other, Iterable) and (not isinstance(other, Mapping))): - does_match = None - check_dir = CHECK_DIR.FORWARDS - multiobj_index_forwards = None - multiobj_index_backwards = None + return self.match_list(other) - if (check_dir == CHECK_DIR.FORWARDS): - - for ele_index in range(0, len(self.data), 1): - ele_data = self.data[ele_index] - # as long as not multiobj, check in forwards - if isinstance(ele_data, WrapMultiObj): - multiobj_index_forwards = ele_index - check_dir = CHECK_DIR.BACKWARDS - break - else: - try: - ele_other = other[ele_index] - except IndexError: - # To_be_checked list has length less than matching list - return False - finally: - does_match = (WrapObj(ele_data) == ele_other) - if not does_match: - return False - - if (check_dir == CHECK_DIR.BACKWARDS): - for ele_index in range(0 - 1, -(len(self.data) + 1), -1): - # ele_index is of negative value from -1 to -len (including) - ele_data = self.data[ele_index] - # as long as not multiobj, check in backwards - if isinstance(ele_data, WrapMultiObj): - multiobj_index_backwards = ele_index - check_dir = CHECK_DIR.FORWARDS - break - else: - try: - ele_other = other[ele_index] - except IndexError: - # To_be_checked list has length less than matching list - return False - finally: - does_match = does_match = (WrapObj(ele_data) == ele_other) - if not does_match: - return False - - if (does_match and (multiobj_index_forwards is None) and (multiobj_index_forwards == multiobj_index_backwards)): - # all of data is in other - # so, if they have same length, everything is perfect - return len(self.data) == len(other) - elif (multiobj_index_forwards + (-multiobj_index_backwards)) != len(self.data): - # there is more than one length_ANY object - raise TypeError("There must be only one object of arbitary range") - else: - # there is only one length ANY object - - # prepare back index for slicing operation - # add 1 (as this one will give outer bound) and if results to 0, set to None (as 0 doesnt mean last index) - multiobj_index_backwards += 1 - multiobj_index_backwards = multiobj_index_backwards or None - ele_wrapmultiobj = self.data[multiobj_index_forwards] - logging.debug("Comparing WrapMultiObj with part of list") - does_match = (ele_wrapmultiobj == other[multiobj_index_forwards:multiobj_index_backwards]) - return does_match elif not(self.treat_as_object) and (isinstance(self.data, Mapping) and isinstance(other, Mapping)): - does_match = None # as in not known yet - for (data_key, data_value) in self.data.iteritems(): - if isinstance(data_key, type): - # todo: stop printing this line, which is broken into parts - warn(("{data_key} is a class used as a key, but it " - "wont match keys which are instances of this type" - "").format(data_key=data_key)) - try: - # try using that key - other_value = other[data_key] - except KeyError: - # if fails, it doesnt have that key. so, they dont match - does_match = False - return False - else: - does_match = (WrapObj(data_value) == other_value) - if does_match is False: - return does_match - else: - # should be True - assert(does_match is True) - return True + return self.match_dict_or_obj(other, dict) + elif self.treat_as_object: try: assert(isinstance(self.data, Mapping)) @@ -192,28 +346,16 @@ class WrapObj(object): raise TypeError("data={data} should be of type Mapping " "(e.g. dict)".format(data=self.data)) - does_match = None # as in not known yet - for (data_key, data_value) in self.data.iteritems(): - if isinstance(data_key, type): - # todo: stop printing this line, which is broken into parts - warn(("{data_key} is a class used as a key, but it " - "wont match keys which are instances of this type" - "").format(data_key=data_key)) - try: - # try using that key - other_value = other.__getattribute__(data_key) - except AttributeError: - # if fails, it doesnt have that attribute. so, they dont match - does_match = False - return False - else: - does_match = (WrapObj(data_value) == other_value) - if does_match is False: - return does_match + return self.match_dict_or_obj(other, object) + + elif check_as_value_and_type(other, self.data): + + if self.save_key is not None: + self.dict_saved_values[self.save_key] = other + return self.dict_saved_values else: - # should be True - assert(does_match is True) return True + # return True else: return False @@ -241,7 +383,8 @@ class WrapMultiObj(object): elif WrapObj([INT_OR_INF, None]) == [range_low, range_high]: range_high = range_low + 1 else: - raise TypeError("range_low and range_high must be of appropiate type") + raise TypeError("range_low={range_low} and range_high={range_high} must be of appropiate type" + "".format(range_low=range_low, range_high=range_high)) logging.debug("OK") @@ -250,12 +393,23 @@ class WrapMultiObj(object): logging.debug("DO_TYPECHECK is off") self.DO_TYPECHECK = False + self.dict_saved_values = {} + self.save_key = None + logging.debug("Create OK, returned") + def save_as(self, arg_name): + self.save_key = arg_name + return self + def __ne__(self, other): return not(self.__eq__(other)) def __eq__(self, other): + ''' + Similar to WrapObj.__eq__ + But expects other is iterable + ''' logging.debug("Checking equality of {self_data} and {other}".format(self_data=self.data, other=other)) if self.DO_TYPECHECK and (type(self.data) != type(other)): logging.debug("DO_TYPECHECK is on and yet types dont match. So returned False") @@ -278,6 +432,8 @@ class WrapMultiObj(object): else: logging.debug("OK") + self.dict_saved_values = {} + repeat_count = 0 while True: logging.debug("Running iteration {repeat_count}".format(repeat_count=repeat_count + 1)) @@ -307,8 +463,21 @@ class WrapMultiObj(object): logging.debug("Checking for repeat matching at iteration = {repeat_count} failed".format(repeat_count=repeat_count)) return False finally: - does_match = check_as_value_and_type(ele_other, ele_data) - if not does_match: + # match_dict_or_True = check_as_value_and_type(ele_other, ele_data) + match_dict_or_True = (WrapObj(ele_data) == ele_other) + # take care of save_as of child elements + if match_dict_or_True and isinstance(match_dict_or_True, dict): + # update self.dict_saved_values with each value in its list form, + # so as to allow muliple matches + for match_key in match_dict_or_True: + match_val = match_dict_or_True[match_key] + prev_match_val = self.dict_saved_values.get(match_key) + if isinstance(prev_match_val, list): + self.dict_saved_values[match_key].append(match_val) + else: + self.dict_saved_values[match_key] = [match_val] + + if not match_dict_or_True: # Return False as the match doesnt work out logging.debug("{ele_other} with {ele_data} Match retuned False") return False @@ -318,18 +487,30 @@ class WrapMultiObj(object): logging.debug("Match works with {repeat_count} iterations".format(repeat_count=repeat_count)) if self.repeat_allowed_range[0] <= repeat_count < self.repeat_allowed_range[1]: logging.debug("And it is in valid range") - return True + # take care of save_as of self + if self.save_key is not None: + self.dict_saved_values[self.save_key] = other + + return (self.dict_saved_values or True) else: logging.debug("And it is NOT in valid range") return False def check_as_value_and_type(to_check, value_or_type): - logging.debug("Checking {1} with {0}".format(value_or_type, to_check)) - # if cheking with Any, return True (let it through) + ''' + Checks whether to_check is either + i) instance of value_or_type if value_or_type is a class + ii) to_check == value_or_type if value_or_type is not a class - # if value_or_type == Any: - # return True + >>> check_as_value_and_type( 5, int) + True + >>> check_as_value_and_type( 5, 5) + True + >>> check_as_value_and_type( int, int) + False + ''' + logging.debug("Checking {1} with {0}".format(value_or_type, to_check)) # if its class, then check if instance of that if isinstance(value_or_type, type): @@ -337,12 +518,12 @@ def check_as_value_and_type(to_check, value_or_type): # try as value, if true return elif to_check == value_or_type: return True + else: + return False w = WrapObj -def fmatch(): - pass -# print(WrapObj([int, int]) == [2, 2]) -print(WrapObj([2, WrapObj([3]).times((1, 3))]) == [2, 3, 3]) -OR(int).times(5) +# to think and todo +# def fmatch(): +# pass diff --git a/wrap_obj.pyc b/wrap_obj.pyc new file mode 100644 index 0000000..ceb92b0 Binary files /dev/null and b/wrap_obj.pyc differ