def test_midway_branch(): # midway branch, but then continues actual = _make_stack(Match(Switch([(1, 1), ('a', 'a'), ([None], T.a)]))) expected = """\ Traceback (most recent call last): File "test_error.py", line ___, in _make_stack glom(target, spec) File "core.py", line ___, in glom raise err glom.core.PathAccessError: error raised while processing, details below. Target-spec trace (most recent last): - Target: [None] - Spec: Match(Switch([(1, 1), ('a', 'a'), ([None], T.a)])) + Spec: Switch([(1, 1), ('a', 'a'), ([None], T.a)]) |\\ Spec: 1 |X glom.matching.MatchError: [None] does not match 1 |\\ Spec: 'a' |X glom.matching.MatchError: [None] does not match 'a' |\\ Spec: [None] || Spec: T.a glom.core.PathAccessError: could not access 'a', part 0 of T.a, got error: AttributeError("'list' object has no attribute 'a'") """ if _PY2: # see https://github.com/pytest-dev/pytest/issues/1347 assert len(actual.split("\n")) == len(expected.split("\n")) else: assert actual == expected # branch and another branch actual = _make_stack( Match( Switch([(1, 1), ('a', 'a'), ([None], Switch([(1, 1), ('a', 'a'), ([None], T.a)]))]))) expected = """\ Traceback (most recent call last): File "test_error.py", line ___, in _make_stack glom(target, spec) File "core.py", line ___, in glom raise err glom.core.PathAccessError: error raised while processing, details below. Target-spec trace (most recent last): - Target: [None] - Spec: Match(Switch([(1, 1), ('a', 'a'), ([None], Switch([(1, 1), ('a', '... + Spec: Switch([(1, 1), ('a', 'a'), ([None], Switch([(1, 1), ('a', 'a'), (... |\\ Spec: 1 |X glom.matching.MatchError: [None] does not match 1 |\\ Spec: 'a' |X glom.matching.MatchError: [None] does not match 'a' |\\ Spec: [None] |+ Spec: Switch([(1, 1), ('a', 'a'), ([None], T.a)]) ||\\ Spec: 1 ||X glom.matching.MatchError: [None] does not match 1 ||\\ Spec: 'a' ||X glom.matching.MatchError: [None] does not match 'a' ||\\ Spec: [None] ||| Spec: T.a glom.core.PathAccessError: could not access 'a', part 0 of T.a, got error: AttributeError("'list' object has no attribute 'a'") """ if _PY2: # see https://github.com/pytest-dev/pytest/issues/1347 assert len(actual.split("\n")) == len(expected.split("\n")) else: assert actual == expected
def test_nested_struct(): """adapted from use case""" import json def _json(spec): return Auto((json.loads, _str_json, Match(spec))) _str_json = Ref( 'json', Match( Or(And(dict, {Ref('json'): Ref('json')}), And(list, [Ref('json')]), And(type(u''), Auto(str)), object))) rule_spec = Match({ 'rule_id': Or('', Regex(r'\d+')), 'rule_name': str, 'effect': Or('approve', 'custom_approvers'), 'rule_approvers': _json([{ 'pk': int, 'level': int }]), 'rule_data': _json([ # list of condition-objects { Optional('value', 'null'): _json(Or(None, int, float, str, [int, float, str])), 'field': Auto(int), # id of row from FilterField 'operator': str, # corresponds to FilterOperator.display_name } ]), Optional('save_as_new', False): Or(str, bool), }) rule = dict(rule_id='1', rule_name='test rule', effect='approve', rule_approvers=json.dumps([{ 'pk': 2, 'level': 1 }]), rule_data=json.dumps([{ 'value': json.dumps([1, 2]), 'field': 2, 'operator': '>' }, { 'field': 2, 'operator': '==' }])) glom(rule, rule_spec) rule['save_as_new'] = 'true' glom(rule, rule_spec)
def test_cruddy_json(): _chk(Match({'int_id?': Auto((int, (M > 0)))}), {'int_id?': '1'}, {'int_id?': '-1'}) # embed a build squished_json = Match({ 'smooshed_json': Auto((json.loads, Match({'sub': Auto((json.loads, M == 1))}))) }) glom({'smooshed_json': json.dumps({'sub': json.dumps(1)})}, squished_json)
def test_sample(): """ test meant to cover a more realistic use """ import datetime data = { 'name': 'item', 'date_added': datetime.datetime.now(), 'desc': 'a data item', 'tags': ['data', 'new'], } spec = Match({ 'name': str, Optional('date_added'): datetime.datetime, 'desc': str, 'tags': [str] }) def good(): glom(data, spec) def bad(): with pytest.raises(MatchError): glom(data, spec) good() # should match del data['date_added'] good() # should still match w/out optional del data['desc'] bad() data['desc'] = 'a data item' data['extra'] = 'will fail on extra' bad() spec.spec[str] = str # now extra str-key/str-val are okay good() data['extra2'] = 2 # but extra str-key/non-str-val are bad bad() # reset data data = { 'name': 'item', 'date_added': datetime.datetime.now(), 'desc': 'a data item', 'tags': ['data', 'new'], } del spec.spec[str] spec.spec[Required(str)] = str # now there MUST be at least one str bad() data['extra'] = 'extra' good()
def test_pattern_matching(): pattern_matcher = Or(And(Match(1), Val('one')), And(Match(2), Val('two')), And(Match(float), Val('float'))) assert glom(1, pattern_matcher) == 'one' assert glom(1.1, pattern_matcher) == 'float' # obligatory fibonacci fib = (M > 2) & (lambda n: glom(n - 1, fib) + glom(n - 2, fib)) | T assert glom(5, fib) == 8 factorial = (lambda t: t + 1, Ref('fact', (lambda t: t - 1, (M == 0) & Fill(1) | (S(r=Ref('fact')), S, lambda s: s['r'] * s[T])))) assert glom(4, factorial) == 4 * 3 * 2
def test_json_ref(): assert glom( {'a': {'b': [0, 1]}}, Ref('json', Match(Or( And(dict, {Ref('json'): Ref('json')}), And(list, [Ref('json')]), And(0, Val(None)), object)))) == {'a': {'b': [None, 1]}}
def test_precedence(): """test corner cases of dict key precedence""" glom({(0, 1): 3}, Match({ (0, 1): Val(1), # this should match (0, int): Val(2), # optional (0, M == 1): Val(3), # optional }) ) with pytest.raises(ValueError): Optional(int) # int is already optional so not valid to wrap
def test_check_ported_tests(): """ Tests ported from Check() to make sure all the functionality has an analogue. """ target = [{'id': 0}, {'id': 1}, {'id': 2}] # check that skipping non-passing values works assert glom(target, [Coalesce(M(T['id']) == 0, default=SKIP)]) == [{'id': 0}] # TODO: should M(subspec, default='') work? I lean no. # NB: this is not a very idiomatic use of Match, just brought over for Check reasons assert glom(target, [Match({'id': And(int, M == 1)}, default=SKIP)]) == [{'id': 1}] assert glom(target, [Match({'id': And(int, M <= 1)}, default=STOP)]) == [{'id': 0}, {'id': 1}] # check that stopping chain execution on non-passing values works spec = (Or(Match(len), Val(STOP)), T[0]) assert glom('hello', spec, glom_debug=True) == 'h' assert glom('', spec) == '' # would fail with IndexError if STOP didn't work target = [1, u'a'] assert glom(target, [Match(unicode, default=SKIP)]) == ['a'] assert glom(target, Match([Or(unicode, int)])) == [1, 'a'] target = ['1'] assert glom(target, [(M(T), int)]) == [1] assert glom(target, M(T)) == ['1'] failing_checks = [({'a': {'b': 1}}, {'a': ('a', 'b', Match(str))}, '''expected type str, not int'''), # TODO: bbrepr at least, maybe include path like Check did ({'a': {'b': 1}}, {'a': ('a', Match({'b': str}))}, '''expected type str, not int'''), # TODO: include subspec path ('b') (1, Match(Or(unicode, bool))), (1, Match(unicode)), (1, Match(0)), (1, Match(Or(0, 2))), ('-3.14', Match(lambda x: int(x) > 0)), # ('-3.14', M(lambda x: int(x) > 0)), # TODO: M doesn't behave quite like Match because it's mode-free ] for fc in failing_checks: if len(fc) == 2: target, check = fc msg = None else: target, check, msg = fc with pytest.raises(MatchError) as exc_info: glom(target, check) if msg is not None: actual_msg = str(exc_info.value) assert actual_msg.find(msg) != -1 assert repr(exc_info.value) return
def nullable_list_of(*items): return default_if_none(Match(list(items)), list)
def in_range(sub_schema, _min, _max): 'check that sub_schema is between _min and _max' return Match(And(sub_schema, _min < M, M < _max))
def none_or(sub_schema): 'allow None or sub_schema' return Match(Or(None, sub_schema))
def test_sets(): with pytest.raises(MatchError): glom({1}, Match({})) with pytest.raises(MatchError): glom(frozenset([1]), Match(frozenset()))
def test_match_default(): default = [] res = glom(None, Match(list, default=default)) assert res is default
def _json(spec): return Auto((json.loads, _str_json, Match(spec)))
def test_nested_dict(): assert glom({1: 2}, Match({A.t: S.t})) == {1: 1}
def test_defaults(): assert glom(1, Match(2, default=3)) == 3 assert glom(1, Or(M == 2, default=3)) == 3 assert glom(1, And(M == 2, default=3)) == 3
def test_basic(): _chk(Match(1), 1, 2) _chk(Match(int), 1, 1.0) # test unordered sequence comparisons _chk(Match([int]), [1], ["1"]) _chk(Match({int}), {1}, [1]) _chk(Match(frozenset({float})), frozenset({}), frozenset({"1"})) _chk(Match(len), [1], []) with pytest.raises(MatchError): glom(None, Match(len)) with pytest.raises(MatchError): glom([1], Match([])) # empty shouldn't match glom({"a": 1, "b": 2}, Match({str: int})) glom(2, M == 2) glom(int, M == int) glom(1.0, M > 0) glom(1.0, M >= 1) glom(1.0, M < 2) glom(1.0, M <= 1) glom(1.0, M != None) glom(1.0, (M > 0) & float) glom(1.0, (M > 100) | float) assert Match(('a', 'b')).matches(('a', 'b', 'c')) is False # test idiom for enum with pytest.raises(MatchError): glom("c", Match("a")) glom("c", Not(Match("a"))) with pytest.raises(MatchError): glom("c", Match(Or("a", "b"))) with pytest.raises(ValueError): And() with pytest.raises(TypeError): And('a', bad_kwarg=True) with pytest.raises(ValueError): Or() with pytest.raises(TypeError): Or('a', bad_kwarg=True) _chk(Match(Or("a", "b")), "a", "c") glom({None: 1}, Match({object: object})) _chk(Match((int, str)), (1, "cat"), (1, 2)) with pytest.raises(MatchError): glom({1: 2}, Match({(): int})) with pytest.raises(MatchError): glom(1, Match({})) Match(M > 0).verify(1.0) assert Match(M).matches(False) is False assert Match(M).matches(True) is True
def test_all_public_errors(): """test that all errors importable from the top-level glom module pass a basic set of standards. When adding a new public error type, this test will be fail unless that type is also tested below. """ import glom import copy err_types = [ t for t in [getattr(glom, name) for name in dir(glom)] if isinstance(t, type) and issubclass(t, Exception) ] non_glomerrors = [ t for t in err_types if not issubclass(t, glom.GlomError) ] assert not non_glomerrors, "expected all public exception types to subclass GlomError" err_types = sorted([t for t in err_types if not t is glom.GlomError], key=lambda t: t.__name__) results = [] def _test_exc(exc_type, target, spec): with pytest.raises(exc_type) as exc_info: glom.glom(target, spec) results.append((target, spec, exc_info.value)) return exc_info.value _test_exc(glom.CheckError, {}, glom.Check(equal_to=[])) _test_exc(glom.FoldError, 2, glom.Flatten()) _test_exc(glom.BadSpec, range(5), glom.grouping.Group([{T: T}])) _test_exc(glom.PathAccessError, {}, 'a.b.c') _test_exc(glom.UnregisteredTarget, 'kurt', [glom.T]) _test_exc(glom.CoalesceError, {}, glom.Coalesce('a', 'b')) _test_exc(glom.PathAssignError, object(), glom.Assign('a', 'b')) _test_exc(glom.PathDeleteError, object(), glom.Delete('a')) _test_exc(MatchError, 1, M == 2) _test_exc(TypeMatchError, 1, Match(str)) for (target, spec, exc) in results: assert copy.copy(exc) is not exc exc_str = str(exc) exc_type_name = exc.__class__.__name__ assert exc_type_name in exc_str assert bbrepr(exc).startswith(exc_type_name) assert bbrepr(target)[:80] in exc_str assert bbrepr(spec)[:80] in exc_str tested_types = [type(exc) for _, _, exc in results] untested_types = set(err_types) - set(tested_types) assert not untested_types, "did not test all public exception types"
def test_ternary(): assert glom('abc', Match(Or(None, 'abc'))) == 'abc'