Package: redbreast
Robin's python utilitiesInstallationpip install redbreastQueryListDo you want that sweet Django QuerySet filtering, but your objects aren't in a database, and you also don't want to
write a filter / list comprehension? The QueryList is the object for you!UsageLet's do some setup. We want to investigate the following group of dogs:fromdataclassesimportdataclassfromredbreast.querylistimportQueryList@dataclassclassDog:name:strowner:strnumber:floatfido=Dog(number=15.72,name="Fido",owner="Sam")muttley=Dog(number=31.44,name="Muttley",owner="Robin")biko=Dog(number=47.17,name="Biko",owner="Sam")buster=Dog(number=71.19,name="Buster",owner="Robin")dogs=QueryList([fido,muttley,biko,buster])filterWe canfilterfor strict equality:dogs.filter(name="Muttley").first()# Dog(name='Muttley', owner='Robin', number=31.44)Or we can do django-like filtering with double underscores in the query:dogs.filter(number__gt=30,number__lt=70)# [#     Dog(name='Muttley', owner='Robin', number=31.44),#     Dog(name='Biko', owner='Sam', number=47.17),# ]Some python builtins are supported too:dogs.filter(name__len=4)# [#     Dog(name="Fido", owner="Sam", number=15.72),#     Dog(name="Biko", owner="Sam", number=47.17),# ]dogs.filter(name__len__gt=4)# [#     Dog(name='Muttley', owner='Robin', number=31.44),#     Dog(name='Buster', owner='Robin', number=71.19),# ]You can chain multiple__s to access related objects and their attributes:doggie=Dog(name="doggie",owner="owner",number=69)friend=Dog(name="Friend",owner="Someone else",number=420)doggie.friend=friendfriend.friend=doggiedogs=QueryList([doggie,friend])dogs.get(friend__owner__len__gt=5)# Dog(name='doggie', owner='owner', number=69)excludeexcludeworks too:dogs.exclude(owner="Sam")# [#     Dog(name='Muttley', owner='Robin', number=31.44),#     Dog(name='Buster', owner='Robin', number=71.19),# ]getThegetmethod works like in Django -- it has to match exactly one object or it will raise an exception:# one object matches this querydogs.get(name="Muttley")# Dog(name='Muttley', owner='Robin', number=31.44)# multiple objects match this querydogs.get(owner="Robin")# Traceback (most recent call last):#   File ".../redbreast/querylist.py", line 34, in get#     raise MultipleObjectsReturned# django.core.exceptions.MultipleObjectsReturned# no objects match this querydogs.get(name="Penelope")# Traceback (most recent call last):#   File "/home/binnev/code/redbreast/redbreast/querylist.py", line 32, in get#     raise ObjectDoesNotExist# django.core.exceptions.ObjectDoesNotExistAlso, it's worth noting that QueryList can handle dictionaries (with["key_lookup"]) as well as objects (
with.dot_lookup). There can even be a mix of dictionaries and objects in the QueryList:things=QueryList([{"name":"foo","number":69,"owner":"Jane"},# dictDog(name="bar",number=420,owner="Johnny"),# object])print(things.get(owner="Jane"))# {'name': 'foo', 'number': 69, 'owner': 'Jane'}print(things.get(owner="Johnny"))# Dog(name='bar', owner='Johnny', number=420)order_byorder_byaccepts one or more field names. Prepending a"-"to the field name will reverse the ordering for that field,
just like in Django.result=dogs.order_by("-owner","number")# [#     Dog(name='Fido', owner='Sam', number=15.72),#     Dog(name='Biko', owner='Sam', number=47.17),#     Dog(name='Muttley', owner='Robin', number=31.44),#     Dog(name='Buster', owner='Robin', number=71.19),# ]Attribute getters and related object lookups can be included in the field name just like withfiltercalls:result=dogs.order_by("-name__len")# [#     Dog(name='Muttley', owner='Robin', number=31.44),#     Dog(name='Buster', owner='Robin', number=71.19),#     Dog(name='Fido', owner='Sam', number=15.72),#     Dog(name='Biko', owner='Sam', number=47.17),# ]Adding query methodsIf you want to extend the functionality of QueryList by adding more dunder query methods, you can use
theregister_operationmethod:deflonger_than(item,target_length:int)->bool:returnlen(item)>target_lengthQueryList.register_operation("longerthan",longer_than)dogs=QueryList([dict(name="foo"),dict(name="fooooooooooooooo"),])dogs.filter(name__longerthan=3).first()["name"]# "fooooooooooooooo"CaveatsDjango's QuerySet is "lazy" -- meaning thatfilter/excludecalls do not actually hit the database. Instead they
simply add to an SQL query that the QuerySet remembers. This query is only sent to the database when you callall/first/last/existson the QuerySet.By contrast, the QueryList isnotlazy. It will execute everyfilter/excludecall on the spot (thus reducing the
number of items it contains):dogs=dogs.filter(owner="Robin")print(dogs)# [#     Dog(name='Muttley', owner='Robin', number=31.44),#     Dog(name='Buster', owner='Robin', number=71.19),# ]ParametrizeThis is a thin wrapper aroundpytest.mark.parametrizethat provides better oversight of tests with lots of parameters.Consider the following (totally useless) test:@pytest.mark.parametrize("a, b, c, d, e, f, g, h, i",[(True,2,"foo",[],(),69,7,8,...),(False,2,"bar",[],(),420,7,8,...),(None,2,"baz",[],(),666,7,8,...),(True,2,"baz",[],(),9000,7,8,...),],)deftest_parametrize(a,b,c,d,e,f,g,h,i):assertain[True,False,None]assertb==2assertisinstance(c,str)assertisinstance(d,list)andlen(d)==0assertisinstance(e,tuple)andlen(e)==0assertisinstance(f,int)assertg==7asserth==8asserti==EllipsisIt was difficult to write because I couldn't quickly see which parameter name mapped to which value. Let's rewrite the
test using my parametrize wrapper:fromredbreast.testingimportparametrize,testparams@parametrize(param:=testparams("a","b","c","d","e","f","g","h","i"),[param(a=True,b=2,c="foo",d=[],e=(),f=69,g=7,h=8,i=...),param(a=False,b=2,c="bar",d=[],e=(),f=420,g=7,h=8,i=...),param(a=None,b=2,c="baz",d=[],e=(),f=666,g=7,h=8,i=...),param(a=True,b=2,c="baz",d=[],e=(),f=9000,g=7,h=8,i=...),],)deftest_parametrize(param):assertparam.ain[True,False,None]assertparam.b==2assertisinstance(param.c,str)assertisinstance(param.d,list)andlen(param.d)==0assertisinstance(param.e,tuple)andlen(param.e)==0assertisinstance(param.f,int)assertparam.g==7assertparam.h==8assertparam.i==EllipsisMuch better. I can see at a glance thatg=7, for example.By invokingparam := testparams("a", "b", "c", ...)we are creating a dataclass on the fly, which accepts
arguments"a", "b", "c", ..., and acts as our container for each test case. All arguments are optional, and default
toNoneif no value is supplied. Only keyword arguments are allowed, because the whole point is to make the test more
descriptive. Using positional args --param(1, 2, 3, ...)-- is not allowed.The dataclass has the default argumentdescriptionto encourage you to describe each test case. The description is
also passed to pytest so that it shows up nicely in output. By default, pytest tries to generate a description based on
the items in the list:# test_testing.py::test_parametrize[param0] PASSED                         [ 25%]# test_testing.py::test_parametrize[param1] PASSED                         [ 50%]# test_testing.py::test_parametrize[param2] PASSED                         [ 75%]# test_testing.py::test_parametrize[param3] PASSED                         [100%]If we pass the following descriptions, they will show up in the output instead:@parametrize(param:=testparams("a","b","c","d","e","f","g","h","i"),[param(description="1st test case",...),param(description="2nd test case",...),param(description="3rd test case",...),param(description="4th test case",...),],)deftest_parametrize(param):...# ============================= test session starts ==============================# collecting ... collected 4 items## test_testing.py::test_parametrize[1st test case] PASSED                  [ 25%]# test_testing.py::test_parametrize[2nd test case] PASSED                  [ 50%]# test_testing.py::test_parametrize[3rd test case] PASSED                  [ 75%]# test_testing.py::test_parametrize[4th test case] PASSED                  [100%]## ============================== 4 passed in 0.04s ===============================Admittedly, our descriptions in this example aren't much more useful than pytest's ones, but you can go into as much
detail as you want. I sometimes write a small paragraph describing the motivation / context around a test case.