Package: pychoir
pychoir- Python Test Matchers for humansSuper-duper low cognitive overhead matching for Python developers reading or writing tests. Implemented fully in modern & typed Python, without any dependencies. Runs and passes its tests on 3.6, 3.7, 3.8, 3.9, 3.10 and 3.11. PyPy works fine too.pychoirhas mostly been developed for use withpytest, but nothing prevents from using it in any other test framework (like vanillaunittest) or even outside of testing, if you feel like it.InstallationWith pip:pip install pychoirWith pipenv:pipenv install --dev pychoirWith poetry:poetry add --dev pychoirWith PDM:pdm add -dG test pychoirDocumentationCheck out the API Reference on readthedocs for detailed info and examples of all the available Matchershttps://pychoir.readthedocs.io/en/stable/api.htmlWhy?You have probably written quite a few tests where you assert something likeassertthing_under_test()=={'some_fields':'some values'}However, sometimes you do not expectexactequivalence. So you startresult=thing_under_test()result_number=result.pop('number',None)assertresult_numberisNoneorresult_number<3result_list_of_strings=result.pop('list_of_strings',None)assert(result_list_of_stringsisnotNoneandlen(result_list_of_strings)==5andall(isinstance(s,str)forsinresult_list_of_strings))assertresult=={'some_fields':'some values'}...but this is not very convenient for anyone in the long run.This is wherepychoircomes in with matchers:frompychoirimportLessThan,All,HasLength,IsNoneOr,IsInstanceassertthing_under_test()=={'number':IsNoneOr(LessThan(3)),'list_of_strings':HasLength(5)&All(IsInstance(str)),'some_fields':'some values',}It can also be cumbersome to check mocked calls without using matchers:assertmock.call_args[0][0]<3assertisinstance(mock.call_args[0][1],str)assertlen(mock.call_args[0][2])==3but simple and easy when using them:frompychoirimportLessThan,IsInstance,HasLengthmock.assert_called_with(LessThan(3),IsInstance(str),HasLength(3))You can also check many things about the same value: for exampleIsInstance(int) & 5will make sure that the value is not only equal to 5, but is also anint(goodbye to accidental5.0).You can place a matcher almost anywhere where a value can be.pychoirmatchers work well inside lists, tuples, dicts, dataclasses, mock call assertions...You can also place normal values inside matchers, and they will match as with traditional==or!=.A core principle is thatpychoirMatchers are composable and can be used freely in various combinations. For example[LessThan(3) | 5]is "equal to" a list with one item, holding a value equal to 5 or any value less than 3.Can I write custom Matchers of my ownYes, you can!pychoirMatcher baseclass has been designed to be usable by code outside the library. It also takes care of most of the generic plumbing, so your custom matcher typically needs very little code.Here is the implementation ofIsInstanceas an example:fromtypingimportAny,TypefrompychoirimportMatcherclassIsInstance(Matcher):def__init__(self,type_:Type[Any]):super().__init__()self.type=type_def_matches(self,other:Any)->bool:returnisinstance(other,self.type)def_description(self)->str:returnself.type.__name__All you need to take care of is defining the parameters (if any) in__init__(), the match itself in_matches(), and a description of the parameters in_description().Here is an even simpler Anything matcher that does not take parameters and matches literally anything:fromtypingimportAnyfrompychoirimportMatcherclassAnything(Matcher):def_matches(self,_:Any)->bool:returnTruedef_description(self)->str:return''If your custom matcher is generic enough to be useful for everyone, please contribute (fork and make a pull request for now) and have it included inpychoir!Why not <X>?PyHamcrestNothing wrong with hamcrest as such, butpychoiraims to be better integrated with natural Python syntax, meaning for example that you do not need to use a custom assert function.pychoirmatchers are drop-in replacements for your normal values alone or inside structures, even deeply nested ones. You can use hamcrest matchers throughpychoirif you like, wrapping them in theMatches(my_hamcrest_matcher)matcher, although the idea is thatpychoirwould soon come with an equivalent set of matchers.assertpyWhat a nice fluent API for matching, allowing matching multiple things at once. However, you can only match one value at a time. Withpychoiryou'll be matching the whole result at once, be it a single value, list, tuple, dict, dataclass, you name it. Let's see ifpychoirgets some of that fluent stuff going forward as well.???I'd be happy to hear from you about other similar libraries.What is it based on?Python has a rather peculiar way of handling equivalence, which allows customizing it in wild and imaginative ways. This is a very powerful feature, which one should usually avoid overusing.pychoiris built around the idea of using this power to build a lean and mean matcher implementation that looks like a custom DSL but is actually completely standard Python 3.What is the project status?pychoirhas quite a vast range of Matchers built in as well as basic API Reference documenting them. New ideas are still plenty and more can be discussed inDiscussions. Documentation will receive updates as well. Most remarkably fancy examples are missing. Makingpychoireasier to contribute to is also on the list.Where does the name come from?It comes from the French wordpochoirwhich means adrawing techniqueusing templates. For some reason this method of matching in tests reminds me of drawing with those. A French word was chosen because it happens to start with a p and a vowel ;)