diff --git a/test_wrap_obj.py b/test_wrap_obj.py new file mode 100644 index 0000000..6b63659 --- /dev/null +++ b/test_wrap_obj.py @@ -0,0 +1,180 @@ +from wrap_obj import WrapObj as w, Any, OR +import collections +import pytest + + +def test_w_single_value(): + assert(w(5) == 5) + assert(w(5.0) == 5) + assert(w("hello") == "hello") + assert(w(float("inf")) == float("inf")) + assert(w(10.23) == 10.23) + assert(w("abhas") == u"abhas") + + assert(w(object()) != object()) + assert(w(5) != "asd") + assert(w(10) != 5) + assert(w(10) != [10]) + assert(w(10) != {"a": 10}) + assert(w("5") != 5) + + # check that true and false is implemented correctly, i.e. is opp + # todo add to all test collections + assert((w(5) != 5) != (w(5) == 5)) + assert((w(5) != "asd") != (w(5) == "asd")) + + +def test_w_single_type(): + assert(w(int) == 5) + 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) != 2.33) + assert(w(int) != "abhas") + assert(w(int) != [1, 2, 3]) + + assert(w(Any) == 5) + assert(w(Any) == "try") + assert(w(Any) == [[1]]) + + assert(w(Any) == type) + assert(w(Any) == (lambda: None)) + assert(w(Any) == Any) + + +def test_w_list_value(): + assert(w([5]) == [5]) + assert(w([1, 2]) == [1, 2]) + assert(w([1, 2]) == (1, 2)) + assert(w([1, 2]) == list([1, 2])) + + assert(w([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]) + assert(w([5, 25.25]) == [5, 25.25]) + + assert(w([5]) != 5) + + +def test_w_list_mixed(): + assert(w([int, 50]) == [200, 50]) + assert(w([int, 50, int]) == [200, 50, 201]) + assert(w([list, 50]) == [[200], 50]) + assert(w([list, 50]) == [[200, None, "whatever"], 50]) + + assert(w([list, 50]) != [200, 50]) + assert(w([int, 50]) != [200, 500]) + + assert((w([list, 50]) != [[200], 50]) != (w([list, 50]) == [[200], 50])) + + +def test_w_times(): + # write many more + + with pytest.raises(TypeError): + # data in wrapmultiobj must be iterable + assert(w({"a": int}).times(3) == [1, 2, 3]) + + +def test_OR(): + assert(isinstance(5, OR(int)) is True) + assert(isinstance(5, OR(int, int)) is True) + assert(isinstance(5, OR([int, int])) is True) + assert(isinstance(5, OR(int, 5)) is True) + assert(isinstance(5, OR(float, 5)) is True) + + # float(5.0)=5.0 + # float.__eq__(int) is true if they are value equivalent + assert(isinstance(5, OR(float, 5.000)) is True) + assert(isinstance(5, OR(float, float(5.000))) is True) + + assert(isinstance(5, OR([float, int])) is True) + assert(isinstance(5.36, OR([float, int])) is True) + assert(isinstance("5.36", OR([str, int])) is True) + assert(isinstance("5.36", OR(collections.Iterable)) is True) + assert(isinstance([2], OR(collections.Iterable)) is True) + assert(isinstance(2.35, OR(float, collections.Iterable)) is True) + assert(isinstance([2], OR(float, collections.Iterable)) is True) + + assert(isinstance(5, OR(float)) is False) + assert(isinstance(5, OR(float, 6)) is False) + assert(isinstance(5, OR(float, 5.0000000000001)) is False) + assert(isinstance(5.36, OR(collections.Iterable)) is False) + assert(isinstance((2, 3), tuple) is True) + + +def test_OR_internal(): + assert(OR(int).list_match_type == OR([int]).list_match_type) + assert(OR(int, str).list_match_type == (int, str)) + + +def test_OR_times(): + assert((w([OR(int, 2.35).times(2, 5)]) == [9, 2]) is True) + assert((w([OR(int, 2.35).times(2, 5)]) == [9, 2.35]) is True) + assert((w([OR(int, 2.35).times(2, 5)]) == [9, 2.35, 9, 9]) is True) + + assert((w([OR(int, 2.35).times(2, 5)]) == [9, 5.05]) is False) + assert((w([OR(int, 2.35).times(2, 5)]) == [9]) is False) + assert((w([OR(int, 2.35).times(2, 5)]) == [9, 2.35, 9, 9, 9]) is False) + + +def test_ANY_times(): + assert((w([Any.times(2, 5)]) == [9, 2]) is True) + assert((w([Any.times(2, 5)]) == [9, 2.35]) is True) + + assert((w([Any.times(2, 5)]) == [9]) is False) + assert((w([Any.times(2, 5)]) == [9, 2.35, 6, "io", "asd"]) is False) + + +def test_repeat_twice(): + with pytest.raises(TypeError): + assert(w([1, OR(int).times(0, float("inf")), OR(int).times(0, float("inf")), 5]) == [1, 6, 6, 6, 6, 5]) + + # should pass later + with pytest.raises(TypeError): + assert(w([1, OR(int).times(3), OR(int).times(1), 5]) == [1, 6, 6, 6, 6, 5]) + + +def test_w_extra_wrap(): + assert(w([1, w(2)]) == [1, 2]) + assert(w([1, w(2)]) == (1, 2)) + assert(w([w(1), 2]) == list([1, 2])) + assert(OR(int, str).list_match_type == OR([int, str]).list_match_type) + + +def test_dict(): + assert(w({"a": int}) == {"a": 20}) + assert(w({"a": int, "b": float}) == {"a": 20, "b": 25.25}) + assert(w({"a": int, "b": list}) == {"a": 20, "b": [25.25]}) + assert(w({"a": int, "b": [int]}) == {"a": 20, "b": [25]}) + assert(w({"a": int, "b": [w([int]).times(3)]}) == {"a": 20, "b": [25, 3, 67]}) + assert(w({"a": int, "b": {"f": float}}) == {"a": 20, "b": {"f": 25.25}}) + + # shouldnt complain about extras + assert(w({"a": int}) == {"a": 20, "b": 25.25}) + + assert(w({"a": int, "b": list}) != {"a": 20, "b": 25.25}) + assert(w({"a": int, "q": list}) != {"a": 20, "b": [2]}) + assert(w({"a": int, "b": [int]}) != {"a": 20, "b": [25.25]}) + assert(w({"a": int, "b": [float]}) != {"a": 20, "b": ["25.25"]}) + assert(w({"a": int, "b": [w([int]).times(3)]}) != {"a": 20, "b": [25, 67]}) + +# write test- +# mul insted of times + + +def test_as_obj(): + class object2(object): + pass + + test_obj = object2() + test_obj.a = 20 + assert(w({"a": int}).as_obj() == test_obj) + assert(w({"a": int}).as_obj() != {"a": 20}) + assert(w({"a": float}).as_obj() != test_obj) + assert(w({"a": int, "b": Any}).as_obj() != test_obj) + + test_obj.b = "qwerty" + assert(w({"a": int, "b": Any}).as_obj() == test_obj) diff --git a/wrap_obj.py b/wrap_obj.py new file mode 100644 index 0000000..7dea661 --- /dev/null +++ b/wrap_obj.py @@ -0,0 +1,348 @@ +from __future__ import print_function +from enum import Enum +from collections import Mapping, Iterable +from warnings import warn +import logging + +logging.debug = print + + +class MetaAny(type): + + def __instancecheck__(self, instance): + return True + + def times(self, range_low, range_high=None): + return WrapObj([self]).times(range_low, range_high) + + +class Any(object): + __metaclass__ = MetaAny + + +def makeMultiInstanceMatcher(*list_type): + if len(list_type) == 1 and isinstance(list_type[0], Iterable): + list_type = tuple(list_type[0]) + + class MetaMultiInstanceMatcher(type): + # Use (both are same) + # a = MetaMultiInstanceMatcher([int,float]) + # b = MetaMultiInstanceMatcher(int,float) + list_match_type = list_type + + def __instancecheck__(self, instance): + + for each_type in self.list_match_type: + if isinstance(each_type, type) and isinstance(instance, each_type): + # if type, use isinstance + return True + elif each_type == instance: + # if value, check equality + return True + else: + return False + + def times(self, range_low, range_high=None): + return WrapObj([self]).times(range_low, range_high) + + class MultiInstanceMatcher(type): + __metaclass__ = MetaMultiInstanceMatcher + + return MultiInstanceMatcher + +OR = makeMultiInstanceMatcher + + +CHECK_DIR = Enum("Iterable check direction", ["FORWARDS", "BACKWARDS"]) + + +class WrapObj(object): + + def __init__(self, data, DO_TYPECHECK=False): + super(WrapObj, self).__init__() + self.data = data + + self.DO_TYPECHECK = DO_TYPECHECK + self.treat_as_object = False + + def as_obj(self): + try: + assert(isinstance(self.data, Mapping)) + except AssertionError: + raise TypeError("data={data} should be of type Mapping " + "(e.g. dict)".format(data=self.data)) + + self.treat_as_object = True + return self + + def times(self, range_low, range_high=None): + return WrapMultiObj(self.data, range_low, range_high) + + def __mul__(self, range_tuple): + 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 __ne__(self, other): + return not(self.__eq__(other)) + + def __eq__(self, other): + 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, WrapMultiObj): + 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 + + 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 + elif self.treat_as_object: + try: + assert(isinstance(self.data, Mapping)) + except AssertionError: + 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 + else: + # should be True + assert(does_match is True) + return True + + else: + return False + + +# todo allow custom class with __isinstance__ being called for composite type checking +# class int_or_infinite - use in WrapMultiObj __init__ check +class WrapMultiObj(object): + + def __init__(self, data, range_low, range_high): + super(WrapMultiObj, self).__init__() + + logging.debug("Creating WrapMultiObj with data={data}".format(data=data)) + if (isinstance(data, Iterable) and (not isinstance(data, Mapping))): + self.data = data + else: + raise TypeError("data={data} should be iterable".format(data=data)) + + logging.debug("Checking {range_low}, {range_high} is of correct range.. ".format(range_low=range_low, range_high=range_high)) + INT_OR_INF = OR(int, float("inf")) + if WrapObj([INT_OR_INF, INT_OR_INF]) == [range_low, range_high]: + pass + elif WrapObj([[INT_OR_INF, INT_OR_INF], None]) == [range_low, range_high]: + range_low, range_high = range_low + 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") + + logging.debug("OK") + + self.repeat_allowed_range = [range_low, range_high] + + logging.debug("DO_TYPECHECK is off") + self.DO_TYPECHECK = False + + logging.debug("Create OK, returned") + + def __ne__(self, other): + return not(self.__eq__(other)) + + def __eq__(self, other): + 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") + return False + + logging.debug("Asserting matcher and matchee are both of list-ish type.. ") + assert (isinstance(self.data, Iterable) and (not isinstance(self.data, Mapping))) + try: + # the one to be matched with should also be list, if its not, no chance of matching + assert(isinstance(other, Iterable) and (not isinstance(other, Mapping))) + except AssertionError: + return False + logging.debug("OK") + + # if len(other) is not integer multiple len(self.data), they will never match as a whole + logging.debug("Checking length of matchee is multiple of length of matcher.. ") + if (len(other) % len(self.data) != 0): + logging.debug("NOT OK.. returned False") + return False + else: + logging.debug("OK") + + repeat_count = 0 + while True: + logging.debug("Running iteration {repeat_count}".format(repeat_count=repeat_count + 1)) + + if (repeat_count > self.repeat_allowed_range[1]): + logging.debug("Checking not yet complete, but max_allowed_repeated={max_repeat} crossed. " + "So, returning False.".format(max_repeat=self.repeat_allowed_range[1])) + return False + start_other_index = repeat_count * len(self.data) + + if start_other_index == len(other): + # other_list is exhausted, their ends meet exactly + break + + for ele_index in range(0, len(self.data)): + ele_data = self.data[ele_index] + # as long as not multiobj, move on + if isinstance(ele_data, WrapMultiObj): + # raise TypeError(".times not allowed within .times") + raise TypeError("{self_class} not allowed within {self_class}, causes ambiguous condition".format(self_class=self.__class__)) + + try: + ele_other = other[start_other_index + ele_index] + except IndexError: + # To_be_checked list has length less than matching list in this iteration + # Return False as the whole data is not matchable this iteration + 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: + # Return False as the match doesnt work out + logging.debug("{ele_other} with {ele_data} Match retuned False") + return False + + repeat_count += 1 + + 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 + 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) + + # if value_or_type == Any: + # return True + + # if its class, then check if instance of that + if isinstance(value_or_type, type): + return isinstance(to_check, value_or_type) + # try as value, if true return + elif to_check == value_or_type: + return True + +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)