def test_coerce(): underlying_spec = in_range(1, 2) s = coerce(int, underlying_spec) check_spec(s, 1) check_spec(s, "1", expected_conform=1) # TODO: problem contains underlying_spec. Not sure yet if this is the right behaviour check_spec(s, 2, [Problem(path(), 2, underlying_spec, "not between 1 and 2")]) check_spec(s, "2", [Problem(path(), 2, underlying_spec, "not between 1 and 2")]) check_spec(s, "one", [Problem(path(), "one", s, "could not coerce 'one' (str) using coercer: int because:\n" "invalid literal for int() with base 10: 'one'")]) spec_using_a_class_as_a_coercer = coerce(CoercingClass(), underlying_spec) check_spec(spec_using_a_class_as_a_coercer, "one", [Problem(path(), "one", spec_using_a_class_as_a_coercer, "could not coerce 'one' (str) using coercer: CoercingClass because:\n" "invalid literal for int() with base 10: 'one'")]) spec_using_a_lambda_as_a_coercer = coerce(lambda x: int(x), underlying_spec) check_spec(spec_using_a_lambda_as_a_coercer, "one", [Problem(path(), "one", spec_using_a_lambda_as_a_coercer, "could not coerce 'one' (str) using coercer: <lambda> because:\n" "invalid literal for int() with base 10: 'one'")])
def test_less_than(): s = lt(2) check_spec(s, 1) check_spec(s, 2, [Problem(path(), 2, s, "not less than 2")]) check_spec(s, 3, [Problem(path(), 3, s, "not less than 2")])
def test_even(): s = even() check_spec(s, 2) check_spec(s, 3, [Problem(path(), 3, s, "not an even number")]) check_spec(s, "", [Problem(path(), "", s, "not an even number")])
def test_odd(): s = odd() check_spec(s, 3) check_spec(s, 4, [Problem(path(), 4, s, "not an odd number")]) check_spec(s, "", [Problem(path(), "", s, "not an odd number")])
def test_is_none(): s = is_none() check_spec(s, None) check_spec(s, "", [Problem(path(), "", s, "not None")]) check_spec(s, [], [Problem(path(), [], s, "not None")])
def test_greater_than(): s = gt(2) check_spec(s, 3) check_spec(s, 2, [Problem(path(), 2, s, "not greater than 2")]) check_spec(s, 1, [Problem(path(), 1, s, "not greater than 2")])
def test_in_range(): s = in_range(2, 4) check_spec(s, 2) check_spec(s, 3) check_spec(s, 1, [Problem(path(), 1, s, "not between 2 and 4")]) check_spec(s, 4, [Problem(path(), 4, s, "not between 2 and 4")])
def test_greater_than_or_equal_to(): s = gte(2) check_spec(s, 3) check_spec(s, 2) check_spec(s, 1, [Problem(path(), 1, s, "not greater than or equal to 2")]) check_spec(s, 0, [Problem(path(), 0, s, "not greater than or equal to 2")])
def test_less_than_or_equal_to(): s = lte(2) check_spec(s, 1) check_spec(s, 2) check_spec(s, 3, [Problem(path(), 3, s, "not less than or equal to 2")]) check_spec(s, 4, [Problem(path(), 4, s, "not less than or equal to 2")])
def test_is_in_over_dicts(): s = is_in({"a": 1, "b": 2}) check_spec(s, "a") check_spec(s, "b") check_spec(s, "c", [Problem(path(), "c", specize(s), "not in ['a', 'b']")])
def test_sets(): s = {"a", "b"} check_spec(s, "a") check_spec(s, "b") check_spec(s, "c", [Problem(path(), "c", specize(s), "not in ['a', 'b']")])
def test_is_in_over_lists(): s = is_in(["a", "b"]) check_spec(s, "a") check_spec(s, "b") check_spec(s, "c", [Problem(path(), "c", specize(s), "not in ['a', 'b']")])
def check_spec(s: Speccable, value: object, expected_problems: Optional[Iterable[Problem]] = None, expected_conform: object = UNDEFINED): """ Always adds path("inserted_by_check_spec") to explain() call, to ensure paths appear in problems correctly """ if expected_problems: expected_explanation = Explanation.with_problems(*expected_problems) if expected_conform != UNDEFINED: raise ValueError("Conform should always be INVALID if explain() is invalid") expected_conform = INVALID else: expected_problems = [] expected_explanation = None if expected_conform == UNDEFINED: expected_conform = value assert explain_data(s, value) == expected_explanation, "\nexpected:\n{}\n\nbut was:\n{}".format( str(expected_explanation), str(explain_data(s, value))) assert conform(s, value) == expected_conform, "\nexpected:\n{}\n\nbut was:\n{}".format(str(expected_conform), str(conform(s, value))) path_element = "added_by_check_spec" problems_which_should_include_path = specize(s).explain(path(path_element), value) for p in problems_which_should_include_path: assert len(p.path) >= 1 and p.path[0] == path_element, \ "spec {} might not be extending paths correctly in explain".format(type(s))
def test_is_instance(): s = is_instance(int) check_spec(s, 1) check_spec(s, "", [Problem(path(), "", s, "expected an int but got a str")]) assert is_instance(int) == is_instance(int)
def explain_data(s: Speccable, x: object) -> Optional[Explanation]: """ Given a spec and a value x which ought to conform, returns nil if x conforms, else an Explanation, which contains a collection of Problems """ problems = specize(s).explain(path(), x) if problems is None or len(problems) == 0: return None return Explanation.with_problems(*problems)
def test_assert(): s = specize(int) assert_spec(s, 1) try: assert_spec(s, "one") assert False, "Expected exception" except SpecError as e: error = e assert error.explanation == Explanation.with_problems(Problem(path(), "one", s, "expected an int but got a str"))
def test_specizing_builtin(): s = callable check_spec(s, lambda x: x) assert isinvalid(conform(s, "not callable")) explanation = explain_data(s, "clearly-not-callable") assert explanation is not None assert explanation.problems[0].reason == "not callable" assert explanation.problems[0].value == "clearly-not-callable" assert explanation.problems[0].path == path()
def test_specizing_lambda(): s = (lambda x: bool(x)) check_spec(s, True) assert isinvalid(conform(s, False)) explanation = explain_data(s, False) assert explanation is not None # This is obviously not ideal assert explanation.problems[0].reason == "not <lambda>" assert explanation.problems[0].value is False assert explanation.problems[0].path == path()
def test_coll_of(): item_spec = specize(int) s = coll_of(item_spec) check_spec(s, [1]) check_spec(s, [1, 2]) check_spec(s, (1, 2)) try: assert_spec(s, ["one", 2, "three"]) assert False, "Expected exception" except SpecError as e: error = e assert error.explanation == Explanation.with_problems( Problem(path(0), "one", item_spec, "expected an int but got a str"), Problem(path(2), "three", item_spec, "expected an int but got a str")) try: assert_spec(s, 1) assert False, "Expected exception" except SpecError as e: error = e assert error.explanation == Explanation.with_problems(Problem(path(), 1, s, "not iterable"))
def test_dict_example_treats_values_as_equal_to_spec(): expected_value = UUID('80b71e04-9862-462b-ac0c-0c34dc272c7b') s = dict_example({'k': expected_value}) check_spec(s, {'k': expected_value}) wrong_value = UUID('a5bef1a0-d139-49d3-91ff-79a69aa39759') check_spec(s, {'k': wrong_value}, [ Problem( path('k'), wrong_value, equal_to(expected_value), "expected 80b71e04-9862-462b-ac0c-0c34dc272c7b (UUID) but got a5bef1a0-d139-49d3-91ff-79a69aa39759 (UUID)" ) ])
def explain(self, p: Path, x: object) -> List[Problem]: if not _acceptably_dict_like(x): return [Problem(p, x, self, "not a dictionary {}".format(type(x)))] problems = [] for k, s in self._key_to_spec.items(): if k not in x: problems.append("Missing {}".format(k)) continue value = x[k] explanation_path = p + path(k) subspec_problems = s.explain(explanation_path, value) if subspec_problems: problems.extend(subspec_problems) return problems
def test_equal_to(): s = equal_to(1) check_spec(s, 1) check_spec(s, 2, [Problem(path(), 2, s, "expected 1 (int) but got 2 (int)")])
def test_types_as_specs(): s = int check_spec(s, 1) check_spec(s, "", [Problem(path(), "", is_instance(s), "expected an int but got a str")])