def test_list_comprehension(): assert c.list_comp(1).gen_converter()(range(5)) == [1] * 5 data = [{"name": "John"}, {"name": "Bill"}, {"name": "Nick"}] assert c.list_comp( c.item("name")).sort(key=lambda n: n).gen_converter()(data) == [ "Bill", "John", "Nick", ] assert c.list_comp(c.item("name")).sort().gen_converter()(data) == [ "Bill", "John", "Nick", ] assert tuple(c.generator_comp(c.item("name")).gen_converter()(data)) == ( "John", "Bill", "Nick", ) assert c.list_comp(c.item("name")).sort( key=lambda n: n, reverse=True).gen_converter()(data) == ["Nick", "John", "Bill"] assert c.list_comp({(c.item("name"), )}, ).execute(data) == [ {("John", )}, {("Bill", )}, {("Nick", )}, ]
def test_list_comprehension(): assert c.list_comp(1).gen_converter()(range(5)) == [1] * 5 data = [{"name": "John"}, {"name": "Bill"}, {"name": "Nick"}] assert c.list_comp(c.item("name")).sort( key=lambda n: n).gen_converter()(data) == ["Bill", "John", "Nick"] assert c.list_comp(c.item("name")).sort().gen_converter()(data) == [ "Bill", "John", "Nick", ] assert tuple(c.generator_comp(c.item("name")).gen_converter()(data)) == ( "John", "Bill", "Nick", ) assert c.list_comp(c.item("name")).sort( key=lambda n: n, reverse=True).gen_converter()(data) == ["Nick", "John", "Bill"] assert c.list_comp({(c.item("name"), )}, ).execute(data) == [ {("John", )}, {("Bill", )}, {("Nick", )}, ] class CustomException(Exception): pass def f(): yield 1 raise CustomException wrapped_generator = c.generator_comp(c.this()).execute(f()) with pytest.raises(CustomException): list(wrapped_generator)
def test_if(): conv1 = c.if_(True, c.this() * 2, c.this() - 1000).gen_converter(debug=False) assert conv1(0) == -1000 assert conv1(10) == 20 conv2 = c.list_comp(c.if_(c.this() % 2 == 0, c.this() * 10, c.this() * 100)).gen_converter(debug=False) conv3 = c.list_comp( c.if_( c.this() % 2 == 0, c.this() * 10, c.this() * 100, no_input_caching=True, )).gen_converter(debug=False) assert conv2([1, 2, 3, 4]) == [100, 20, 300, 40] assert conv3([1, 2, 3, 4]) == [100, 20, 300, 40] conv4 = c.list_comp((c.this() - 5).pipe( c.if_(c.this() % 2 == 0, c.this() * 10, c.this() * 100))).gen_converter(debug=False) assert conv4([1, 2, 3, 4]) == [-40, -300, -20, -100] conv5 = c.if_().gen_converter(debug=False) assert conv5(0) == 0 and conv5(1) == 1 conv6 = c.list_comp( c.if_(c.this(), None, c.this(), no_input_caching=True)).gen_converter(debug=False) assert conv6([1, False, 2, None, 3, 0]) == [ None, False, None, None, None, 0, ] assert PipeConversion.input_is_simple("'abc'") assert PipeConversion.input_is_simple("0") assert PipeConversion.input_is_simple("None") assert PipeConversion.input_is_simple("True") assert PipeConversion.input_is_simple("False") assert PipeConversion.input_is_simple("a[1]") assert PipeConversion.input_is_simple("a['1']") assert PipeConversion.input_is_simple("a[1][2]") assert not PipeConversion.input_is_simple("a[1][2][3]") assert not PipeConversion.input_is_simple("1 + 1") assert not PipeConversion.input_is_simple("x.a") assert not PipeConversion.input_is_simple("x()")
def test_optional_dict(): conv = c.list_comp({ "key1": c.item("key1"), "key2": c.optional(c.item("key2", default=None)), "key3": c.optional(c.item("key1") * 200, skip_value=2000), "key4": c.optional( c.item("key1") * c.input_arg("x") * 300, skip_if=c.item("key1") < 5, ), "key5": c.optional( c.item("key1") * c.input_arg("x") * 300, keep_if=c.item("key1") >= 5, ), c.optional(c.item("key2", default=-1), skip_value=-1): 0, c.optional(c.item("key1") * 400, skip_if=c.item("key1") < 5): c.optional(c.item("key22")), c.optional(c.item("key1") * 500, skip_if=c.item("key1") < 5): c.optional(c.item("key22"), skip_value=20), }).gen_converter(debug=False) assert conv([{ "key1": 1, "key2": 2 }, { "key1": 10, "key22": 20 }], x=1) == [ { "key1": 1, "key2": 2, "key3": 200, 2: 0 }, { "key1": 10, "key4": 3000, "key5": 3000, 4000: 20 }, ] with pytest.raises(Exception): c.list_comp(c.optional(c.item("key1"))).gen_converter() with pytest.raises(Exception): c.optional(c.item("key1"), skip_value=1, skip_if=c.this()) with pytest.raises(Exception): c.this().pipe(c.optional(c.this()))
def test_pipes(): assert c.list_comp(c.inline_expr("{0} ** 2").pass_args(c.this)).pipe( c.call_func(sum, c.this)).pipe( c.call_func( lambda x, a: x + a, c.this, c.naive({ "abc": 10 }).item(c.input_arg("key_name")), )).pipe([c.this, c.this]).execute([1, 2, 3], key_name="abc", debug=False) == [ 24, 24, ] assert c.item(0).pipe(datetime.strptime, "%Y-%m-%d").pipe( c.call_func(lambda dt: dt.date(), c.this)).execute(["2019-01-01"], debug=False) == date(2019, 1, 1) assert c.item(0).pipe(datetime.strptime, "%Y-%m-%d").pipe( c.this.call_method("date")).execute(["2019-01-01"], debug=False) == date(2019, 1, 1) conv = c.dict_comp( c.item("name"), c.item("transactions").pipe( c.list_comp({ "id": c.item(0).as_type(str), "amount": c.item(1).pipe(c.if_(c.this, c.this.as_type(Decimal), None)), })), ).gen_converter(debug=False) assert conv([{ "name": "test", "transactions": [(0, 0), (1, 10)] }]) == { "test": [ { "id": "0", "amount": None }, { "id": "1", "amount": Decimal("10") }, ] } assert c.this.pipe(lambda it: it).filter( c.this).sort().as_type(list).execute((2, 1, 0)) == [1, 2]
def test_optional_list_tuple_set(): conv = c.list_comp([ c.item("key1"), c.optional(c.item("key2", default=None)), c.optional(c.item("key1") * 2, skip_value=20), c.optional(c.item("key1") * 3, skip_if=c.item("key1") < 5), ]).gen_converter(debug=False) assert conv([{ "key1": 1, "key2": 2 }, { "key1": 10, "key22": 20 }]) == [ [1, 2, 2], [10, 30], ] conv = c.list_comp(( c.item("key1"), c.optional(c.item("key2", default=None)), c.optional(c.item("key1") * 2, skip_value=20), c.optional(c.item("key1") * 3, skip_if=c.item("key1") < 5), )).gen_converter(debug=False) assert conv([{ "key1": 1, "key2": 2 }, { "key1": 10, "key22": 20 }]) == [ (1, 2, 2), (10, 30), ] conv = c.list_comp({ c.item("key1"), c.optional(c.item("key2", default=None)), c.optional(c.item("key1") * 2, skip_value=20), c.optional(c.item("key1") * 3, skip_if=c.item("key1") < 5), }).gen_converter(debug=False) assert conv([{ "key1": 1, "key2": 2 }, { "key1": 10, "key22": 20 }]) == [ {1, 2, 2}, {10, 30}, ]
def test_breakpoint(): before = Breakpoint.debug_func l = [] def add_to_list(obj): l.append(obj) return obj Breakpoint.debug_func = staticmethod(add_to_list) try: c.list_comp(c.this.breakpoint()).execute([1, 2, 3]) c.list_comp(c.breakpoint()).execute([3, 4]) finally: Breakpoint.debug_func = before assert l == [1, 2, 3, 3, 4]
def test_labels(): conv1 = c.if_( 1, c.input_arg("y").item("abc").add_label("abc").pipe( c.input_arg("x").pipe( c.inline_expr("{cde} + 10").pass_args( cde=c.this().item("cde")))).pipe( c.inline_expr("{this} + {abc}").pass_args( this=c.this(), abc=c.label("abc"))), 2, ).gen_converter(debug=False) assert conv1(data_=1, x={"cde": 2}, y={"abc": 3}) == 15 list(c.generator_comp(c.this().add_label("a")).execute([1, 2])) c.list_comp(c.this().add_label("a")).execute([1, 2])
def test_mutation_item(): now = datetime.now() assert c.list_comp( { "name": c.item("fullName"), "age": c.item("age").as_type(int), "to_del": 1, } ).pipe( c.list_comp( c.call_func(lambda d: d, c.this).tap( c.Mut.set_item( "name_before", c.label("_input").item(0, "name") ), c.Mut.set_item("name", c.item("name").call_method("lower")), c.Mut.set_item( "name_after", c.label("_input").item(0, "name") ), c.Mut.set_item("_updated", c.input_arg("now")), c.Mut.set_item(c.item("age"), c.item("age") >= 18), c.Mut.del_item("to_del"), c.Mut.custom(c.this.call_method("update", {"to_add": 2})), c.this.call_method("update", {"to_add2": 4}), ) ), label_input="_input", ).execute( [{"fullName": "John", "age": "28"}], debug=False, now=now ) == [ { "name": "john", "name_after": "john", "name_before": "John", "age": 28, "_updated": now, 28: True, "to_add": 2, "to_add2": 4, } ] with pytest.raises(Exception): c.item(c.Mut.set_item("abc", "cde")) with pytest.raises(Exception): conversion = c.item(1) conversion.ensure_conversion( c.Mut.set_item("abc", "cde"), explicitly_allowed_cls=GetItem )
def test_simple_label(): conv1 = (c.tuple(c.item(2).add_label("a"), c.this()).pipe( c.item(1).pipe(c.list_comp( (c.this(), c.label("a"))))).gen_converter(debug=False)) assert conv1([1, 2, 3, 4]) == [(1, 3), (2, 3), (3, 3), (4, 3)] conv2 = (c.tuple(c.item(1).add_label("a"), c.this()).pipe( c.item(1), label_input={ "aa": c.item(0), "bb": c.item(0) }, label_output="collection1", ).pipe( c.label("collection1").pipe( c.aggregate( c.ReduceFuncs.Sum( c.this() + c.label("a") + c.label("aa") + c.input_arg("x") + c.label("collection1").item(0), ))), label_output="b", ).pipe(c.this() + c.label("b")).gen_converter(debug=False)) assert conv2([1, 2, 3, 4], x=10) == 140 conv3 = (c.tuple(c.item("default").add_label("default"), c.this()).pipe( c.item(1).pipe(c.item( "abc", default=c.label("default")))).gen_converter(debug=False)) assert conv3({"default": 1}) == 1 with pytest.raises(c.ConversionException): c.this().pipe(c.this(), label_input=1)
def test_complex_labeling(): conv1 = (c.this().add_label("input").pipe( c.filter(c.this() % 3 == 0), label_input={ "input_type": c.call_func(type, c.this()) }, ).pipe( c.list_comp(c.this().as_type(str)), label_output={ "list_length": c.call_func(len, c.this()), "separator": c.if_(c.label("list_length") > 10, ",", ";"), }, ).pipe({ "result": c.label("separator").call_method("join", c.this()), "input_type": c.label("input_type"), "input_data": c.label("input"), }).gen_converter(debug=False)) assert conv1(range(30)) == { "result": "0;3;6;9;12;15;18;21;24;27", "input_type": range, "input_data": range(0, 30), } assert conv1(range(40)) == { "result": "0,3,6,9,12,15,18,21,24,27,30,33,36,39", "input_type": range, "input_data": range(0, 40), }
def test_pipes(): assert c.list_comp(c.inline_expr("{0} ** 2").pass_args(c.this())).pipe( c.call_func(sum, c.this())).pipe( c.call_func( lambda x, a: x + a, c.this(), c.naive({ "abc": 10 }).item(c.input_arg("key_name")), )).pipe([c.this(), c.this()]).execute([1, 2, 3], key_name="abc", debug=False) == [ 24, 24, ] assert c.item(0).pipe( datetime.strptime, "%Y-%m-%d", ).pipe(c.call_func(lambda dt: dt.date(), c.this())).execute([ "2019-01-01", ], debug=False) == date(2019, 1, 1) assert c.item(0).pipe( datetime.strptime, "%Y-%m-%d", ).pipe(c.this().call_method("date")).execute([ "2019-01-01", ], debug=False) == date(2019, 1, 1) with pytest.raises(c.ConversionException): c.naive(True).pipe(c.item("key1", _predefined_input={"key1": 777}))
def test_memory_freeing(): converter = ( c.this() .pipe( c.list_comp(c.this() + c.label("input_data").item(0)), label_input=dict(input_data=c.this()), ) .gen_converter(debug=True) ) sizes = [] sizes.append(total_size(converter.__dict__)) for i in range(100): l_input = [i + j for j in range(3)] l_out = [j + l_input[0] for j in l_input] assert converter(l_input) == l_out sizes.append(total_size(converter.__dict__)) assert all(sizes[0] == size for size in sizes[1:]), sizes conv2 = ( c.inline_expr("globals().__setitem__('a', {}) or 1") .pass_args(c.this()) .gen_converter() ) with pytest.raises(AssertionError): # should raise because of a memory leak conv2(123)
def test_chunks_by_size(data_for_chunking): assert c.chunk_by(size=5).iter(c.list_comp(c.item("z"))).as_type( list ).execute(data_for_chunking) == [ [10, 11, 12, 13, 14], [15, 16, 17, 18], ] assert c.chunk_by(c.item("x")).iter(c.list_comp(c.item("z"))).as_type( list ).execute(data_for_chunking) == [ [10, 11, 12], [13, 14, 15], [16, 17, 18], ] assert c.chunk_by(c.item("x"), size=2).iter( c.list_comp(c.item("z")) ).as_type(list).execute(data_for_chunking) == [ [10, 11], [12], [13, 14], [15], [16, 17], [18], ] assert c.chunk_by(c.item("x"), c.item("y")).iter( c.list_comp(c.item("z")) ).as_type(list).execute(data_for_chunking) == [ [10, 11], [12], [13], [14, 15], [16, 17], [18], ] assert ( c.chunk_by(c.item("x"), size=2) .aggregate( c.ReduceFuncs.Last(c.item("z")), ) .as_type(list) .execute(data_for_chunking) == [11, 12, 14, 15, 17, 18] )
def test_filter(): assert list(c.naive([1, 2, 3]).filter(c.this.gt(2)).execute(None)) == [3] assert c.filter(c.this.gt(1), cast=list).execute([1, 2, 3]) == [2, 3] assert c.filter(c.this.gt(1), cast=tuple).execute([1, 2, 3]) == (2, 3) assert c.filter(c.this.gt(1), cast=set).execute([1, 2, 3]) == {2, 3} assert c.filter(c.this.gt(1), cast=lambda x: list(x)).execute([1, 2, 3]) == [2, 3] assert c.list_comp(c.this).filter(c.this.gt(1)).execute([1, 2, 3], debug=False) == [ 2, 3, ] assert c.this.filter(c.this.gt(1), cast=list).execute([1, 2, 3], debug=False) == [ 2, 3, ] assert c.list_comp(c.this).filter( c.this > 1, cast=lambda x: list(x)).execute(range(4)) == [2, 3]
def test_name_generation(): c.list_comp({i: c.item(f"test{i}", default=1) for i in range(100)}).gen_converter(debug=False) item = c.this() ctx = c.BaseConversion._init_ctx() for i in range(11): item.gen_name("abc", ctx, i) assert item.gen_name("_", ctx, (1, 2)) == item.gen_name("_", ctx, (1, 2)) obj = object() assert item.gen_name("_", ctx, (1, obj)) == item.gen_name("_", ctx, (1, obj)) obj = (1, []) assert item.gen_name("_", ctx, obj) == item.gen_name( "_", ctx, obj, )
def test_comprehensions_sorting(): assert c.generator_comp(c.this()).sort().execute( [2, 1, 3], debug=False ) == [1, 2, 3] assert c.list_comp(c.this()).sort().execute([2, 1, 3], debug=False) == [ 1, 2, 3, ] assert c.this().pipe(c.list_comp(c.this())).sort().execute( [2, 1, 3], debug=False ) == [ 1, 2, 3, ] assert c.list_comp(c.this()).sort().sort(reverse=True).execute( [2, 1, 3], debug=False ) == [3, 2, 1] assert c.set_comp(c.this()).sort().execute([2, 2, 1, 3], debug=False) == [ 1, 2, 3, ] assert c.tuple_comp(c.this()).sort().execute( [2, 2, 1, 3], debug=False ) == ( 1, 2, 2, 3, ) assert c.dict_comp(c.this() * -1, c.this()).sort().execute( [2, 2, 1, 3], debug=False ) == OrderedDict( [ (-3, 3), (-2, 2), (-1, 1), ] )
def test_comprehension_filter_cast_assumptions(): assert isinstance( c.generator_comp(c.this).filter(c.this).execute(range(10)), GeneratorType, ) assert isinstance( c.generator_comp(c.this).filter(c.this, cast=None).execute(range(10)), GeneratorType, ) assert (c.list_comp(c.this).filter(c.this).execute(range(3))) == [ 1, 2, ] def f(x): f.number_of_calls += 1 if f.number_of_calls > f.max_number_of_calls: raise ValueError return bool(x) f.max_number_of_calls = 2 f.number_of_calls = 0 assert (c.set_comp(c.this).filter(c.call_func(f, c.this)).execute([0, 0, 1])) == { 1, } assert (c.set_comp(c.this).filter(c.this, cast=list).execute([0, 0, 1])) == [ 1, ] assert (c.set_comp(c.this).filter(c.this).execute(range(3))) == { 1, 2, } assert (c.tuple_comp(c.this).filter(c.this).execute(range(3))) == ( 1, 2, ) assert (c.tuple_comp(c.this).filter(c.this, list).execute(range(3))) == [ 1, 2, ] assert (c.dict_comp(c.this, c.this).filter(c.item(0)).execute(range(3))) == { 1: 1, 2: 2, } assert (c.dict_comp(c.this, c.this).filter(c.item(0), dict).execute(range(3))) == { 1: 1, 2: 2, }
def test_filter(): assert list(c.naive([1, 2, 3]).filter(c.this().gt(2)).execute(None)) == [3] assert c.filter(c.this().gt(1), cast=list).execute([1, 2, 3]) == [2, 3] assert c.filter(c.this().gt(1), cast=tuple).execute([1, 2, 3]) == (2, 3) assert c.filter(c.this().gt(1), cast=set).execute([1, 2, 3]) == {2, 3} assert c.filter(c.this().gt(1), cast=lambda x: list(x)).execute([1, 2, 3]) == [2, 3] assert c.list_comp(c.this()).filter(c.this().gt(1)).execute([1, 2, 3]) == [ 2, 3, ] assert c.this().filter(c.this().gt(1), cast=list).execute([1, 2, 3]) == [ 2, 3, ]
def test_pipe_single_call_functions(): class CustomException(Exception): pass def one_off_func(): if one_off_func.first: one_off_func.first = False return 1 raise CustomException one_off_func.first = True assert (c.list_comp( c.call_func(one_off_func).pipe(( c.this + 1, c.this + 2, ))).gen_converter(debug=False)([1]) == [(2, 3)])
def test_pipe_conversion(): from convtools import conversion as c from convtools.base import PipeConversion assert PipeConversion(c.naive([1, 2, 3]), c.item(1)).execute(None) == 2 assert (PipeConversion(c.item("key1"), c.item("key2")).execute({"key1": { "key2": 3 }}, debug=False) == 3) assert (c.this.pipe(c.list_comp(c.this + 1)).filter(c.this > 3).execute( [1, 2, 3, 4, 5, 6], debug=False)) == [4, 5, 6, 7] c.aggregate( c.ReduceFuncs.Array(c.item("key"), default=list).pipe( c.if_( c.call_func(any, c.generator_comp(c.this.is_(None))), c.call_func(list), c.this, ))).gen_converter(debug=False)
def test_chunks_by_condition(data_for_chunking): assert c.chunk_by_condition(c.call_func(len, c.CHUNK) < 5).iter( c.list_comp(c.item("z")) ).as_type(list).execute(data_for_chunking) == [ [10, 11, 12, 13, 14], [15, 16, 17, 18], ] assert c.chunk_by_condition( c.and_(c.call_func(len, c.CHUNK) < 5, c.item("z") < 18) ).aggregate(c.ReduceFuncs.Median(c.item("z"))).as_type(list).execute( data_for_chunking, ) == [ 12, 16, 18, ] assert c.chunk_by_condition(False).as_type(list).execute(range(3)) == [ [0], [1], [2], ]
def test_pipes(): assert c.list_comp(c.inline_expr("{0} ** 2").pass_args(c.this())).pipe( c.call_func(sum, c.this())).pipe( c.call_func( lambda x, a: x + a, c.this(), c.naive({ "abc": 10 }).item(c.input_arg("key_name")), )).pipe([c.this(), c.this()]).execute([1, 2, 3], key_name="abc", debug=False) == [ 24, 24, ] assert c.item(0).pipe(datetime.strptime, "%Y-%m-%d").pipe( c.call_func(lambda dt: dt.date(), c.this())).execute(["2019-01-01"], debug=False) == date(2019, 1, 1) assert c.item(0).pipe(datetime.strptime, "%Y-%m-%d").pipe( c.this().call_method("date")).execute(["2019-01-01"], debug=False) == date(2019, 1, 1) with c.OptionsCtx() as options: max_pipe_length = options.max_pipe_length = 10 with pytest.raises(c.ConversionException): conv = c.this() for i in range(max_pipe_length + 1): conv = c.this().pipe(conv) with c.OptionsCtx() as options2, pytest.raises(c.ConversionException): options2.max_pipe_length = 5 conv.clone() conv = c.dict_comp( c.item("name"), c.item("transactions").pipe( c.list_comp({ "id": c.item(0).as_type(str), "amount": c.item(1).pipe(c.if_(c.this(), c.this().as_type(Decimal), None)), })), ).gen_converter(debug=True) assert conv([{ "name": "test", "transactions": [(0, 0), (1, 10)] }]) == { "test": [ { "id": "0", "amount": None }, { "id": "1", "amount": Decimal("10") }, ] } with c.OptionsCtx() as options: max_pipe_length = options.max_pipe_length = 10 conv1 = c.item(0).pipe(c.item(1).pipe(c.item(2))) def measure_pipe_length(conv): length = 0 for i in range(max_pipe_length): if conv._predefined_input is not None: length += 1 conv = conv._predefined_input else: break return length pipe_length_before = measure_pipe_length(conv1) for i in range(max_pipe_length + 20): c.generator_comp(c.this().pipe(conv1)) pipe_length_after = measure_pipe_length(conv1) assert pipe_length_after == pipe_length_before
def test_doc__index_intro(): # ======== # # GROUP BY # # ======== # input_data = [ { "a": 5, "b": "foo" }, { "a": 10, "b": "foo" }, { "a": 10, "b": "bar" }, { "a": 10, "b": "bar" }, { "a": 20, "b": "bar" }, ] conv = (c.group_by(c.item("b")).aggregate({ "b": c.item("b"), "a_first": c.ReduceFuncs.First(c.item("a")), "a_max": c.ReduceFuncs.Max(c.item("a")), }).gen_converter(debug=True)) assert conv(input_data) == [ { "b": "foo", "a_first": 5, "a_max": 10 }, { "b": "bar", "a_first": 10, "a_max": 20 }, ] # ========= # # AGGREGATE # # ========= # conv = c.aggregate({ # list of "a" values where "b" equals to "bar" "a": c.ReduceFuncs.Array(c.item("a"), where=c.item("b") == "bar"), # "b" value of a row where "a" has Max value "b": c.ReduceFuncs.MaxRow(c.item("a"), ).item("b", default=None), }).gen_converter(debug=True) assert conv(input_data) == {"a": [10, 10, 20], "b": "bar"} # ==== # # JOIN # # ==== # collection_1 = [ { "id": 1, "name": "Nick" }, { "id": 2, "name": "Joash" }, { "id": 3, "name": "Bob" }, ] collection_2 = [ { "ID": "3", "age": 17, "country": "GB" }, { "ID": "2", "age": 21, "country": "US" }, { "ID": "1", "age": 18, "country": "CA" }, ] input_data = (collection_1, collection_2) conv = (c.join( c.item(0), c.item(1), c.and_( c.LEFT.item("id") == c.RIGHT.item("ID").as_type(int), c.RIGHT.item("age") >= 18, ), how="left", ).pipe( c.list_comp({ "id": c.item(0, "id"), "name": c.item(0, "name"), "age": c.item(1, "age", default=None), "country": c.item(1, "country", default=None), })).gen_converter(debug=True)) assert conv(input_data) == [ { "id": 1, "name": "Nick", "age": 18, "country": "CA" }, { "id": 2, "name": "Joash", "age": 21, "country": "US" }, { "id": 3, "name": "Bob", "age": None, "country": None }, ]