Package: izy
izyPython functions and classes that make it eveneasier! You will wonder why these are not already built-in in python! :)You can skip README and follow (and run) its equivalentdemo notebook.The sooner youpip install izy, the less time you waste! Just 3 keystrokes to install and import :)pipinstallizyNow that you have it, here are the functionalities:SortingSome operations likeargsortneed 1 or 2 lines to implement in python, but the code is not pythonically readable as it should be. Some other functions liketopkare somehow hidden in the built-in modules, which we expose with ease!Let us have a big list of numbers and give it anizytry:fromizyimport*mylist=[4.2,10,-8,0,4,33,6,97,1,6.,41,-6,0.0]>>>topk(mylist,4)[97,41,33,10]>>>topk(mylist,-3)# as you would expect[-8,-6,0]>>>argmin(mylist),argmax(mylist)(2,7)>>>argsort(mylist)[2,11,3,12,8,4,0,6,9,1,5,10,7]>>>descending(mylist)# I like it more than sorted(x, reverse=True)[97,41,33,10,6,6.0,4.2,4,1,0,0.0,-6,-8]>>>reorder(mylist,argsort(mylist))# like numpy array indexing[-8,-6,0,0.0,1,4,4.2,6,6.0,10,33,41,97]If you have adict(or more precisely aMapping), thearg*functions takekeysas indices:mydict={'a':1,'b':4,'c':-1}>>>argmin(mydict)'c'>>>reorder(mydict,argsort(mydict))# as it should beOrderedDict([('c',-1),('a',1),('b',4)])If you liked whatreorder()did to a dictionary, considerordered()as an alternative to built-insorted().ordered()tries to return the sorted (ordered) variant of the input, while preserving its key properties. The behavior is as follows:Mapping->OrderedDictwhich is aMappingtoo.MutableSequenceorMutableSet->list(equivalent tosorted()in this case)immutableSequenceorSet->tuple>>>ordered(mydict)OrderedDict([('c',-1),('a',1),('b',4)])>>>ordered(frozenset({2,4,-5}))(-5,2,4)ScorerTheScoreris adictsubclass for scoring hashable items. It generalizes functionality of built-inCounterto floating-point numbers with full math operation support.fromizyimportScorers1=Scorer({'a':1,'b':2,'c':5})s1['d']=3>>>s1Scorer({c:5,d:3,b:2,a:1})s2=Scorer('abc',[-2,3,4])>>>s2Scorer({c:4,b:3,a:-2})Mathematical operators (+,-,*,/,//,%,**) are supported for bothScorerand scalar right-hand operands.>>>s1+s2Scorer({c:9,b:5,d:3,a:-1})>>>s1/4Scorer({c:1.25,d:0.75,b:0.5,a:0.25})WARNING!To support partial update with math operations, we carry non-common items from the first operand unchanged, but those from the second operand are ignored.Unary math operators (+,-) are available too.>>>+s2# special usage for useless uniary `+` (only keeps positive itmes)Scorer({c:4,b:3})>>>s1+(+s2)# not equivalent to s1 + s2Scorer({c:9,b:5,d:3,a:1})We also haveabs(),round():>>>abs(s2))Scorer({c:4,b:3,a:2})>>>round(s2/4,1)Scorer({c:1.0,b:0.8,a:-0.5})Logical operators&,|apply element-wise min/max and they are also applicable to scalars. AsScoreris not asetor evenmultiset(likeCounter), we don't refer to these as intersection and union, but the functionalities are still similar.>>>abs(s1&s2)# ~ min (drops non-common items)Scorer({c:4,a:2,b:2})>>>s1&2# drops items with score less than 2Scorer({c:5,d:3,b:2})>>>s1|s2# ~ maxScorer({c:5,b:3,d:3,a:1})About the above warning, note that(s1 | (s1 & s2))gives the subset ofs1which also exists ins2. You can use this to force math operations to return common items only.And finally the ultimate goal of theScoreris to sort its items according to scores and give us thebest,topk(orworst,bottomk).>>>s1.best()('c',5)>>>s1.topk(3)# alias to `s1.best` but more readable when you specify the number of items[('c',5),('d',3),('b',2)]>>>s1.topk(-2)# negative k means bottom k[('a',1),('b',2)]>>>s1.ascending()# prefer this to `best` or `topk` with special values of n or k (None, 0, inf)[('a',1),('b',2),('d',3),('c',5)]>>>s1.median()# if scorer length is even, lower median is returned('b',2)DecoratorsPythonfunctoolsis really interesting, but it really lacks some generally usefull   decorators. There are some sippets scattered around and inPythonDecoratorLibrary, but I wanted to gather my favourites in a single package.First,@returns()and@yields()make your function/generator return/yieldnamedtuples to more pythonically access your function output. It also looks like an easy documentation to me (about themeaningof what function returns and not just thetype).fromizyimport*@returns(x='the input',plus_one='input + 1')defmyfunction(x):returnx,x+1@yields('a','a_doubled')defmygenerator(x):foriinrange(x):yieldi,i*2>>>myfunction(5)myfunction_output(x=5,plus_one=6)>>>myfunction(5).plus_one6>>>myfunction.__doc__Returns:x:theinputplus_one:input+1@returns_time()calculates function runtime and returns it as adatetime.timedeltaobject. You can change time format tomilis(int) orseconds(float). It returns anamedtuplewhich you can rename with using@returns.@returns_time()deftimed(x):z=0foriinrange(x*1000000):z+=i@returns('output','milis')@returns_time(milis=True)deftimed_milis(x):z=0foriinrange(x*1000000):z+=ireturn'done'>>>timed(64)timed_output(output=None,time=datetime.timedelta(seconds=4,microseconds=701631))>>>timed_milis(32)timed_milis_output(output='done',milis=2297)@logs()does what it says at three stages: before calling, after returning and on exceptions. You can control log level for each stage, logto_fileusing default logger (named after the function) or pass your ownlogging.Logger.importloggingroot_logger=logging.getLogger()root_logger.setLevel(logging.DEBUG)@logs(before=logging.DEBUG,after=logging.INFO,to_file='logged.log',file_mode='a')deflogged(x,**kw):foriinrange(1000000):x+=i>>>logged(1,key1=5,key2=7.2)[2022-01-0912:38:41][logged][DEBUG]-Function`logged`calledwithargs:(1,key1=5,key2=7.2)[2022-01-0912:38:41][logged][INFO]-Function`logged`returnedafter0:00:00.075534withoutput:None@fix_this()reminds you to fix something in a stronger way than a passive comment. It raisesUserWarningat runtime and does this everytime :)@fix_this('It always prints ValueError, errors should be raised!')defplease(x):print('ValueError')This one@ignores()exceptions you define (and returnsNone). Who knows when and why you need this? :) You can stack it on top of@logs()to be aware of ignored exceptions.@ignores(ValueError,IndexError)@logs()defignorer(x):foriinrange(x):raiseValueError>>>ignorer(5)# no error raises, only logging it[2022-01-0912:38:41][ignorer][DEBUG]-Function`ignorer`calledwithargs:(5)[2022-01-0912:38:41][ignorer][ERROR]-ValueError()raisedin`ignorer`after0:00:00.003514!Want more useful exception handler?@fallsback()falls back on specified exception to predefined return value or anothercallable. For multiple exceptions, stack it like this:importmath@fallsback(ZeroDivisionError,float('inf'))@fallsback(ValueError,lambdax:x**2)defmultifb(x):ifx<0:raiseValueErrorelifx==0:raiseZeroDivisionErrorelse:return1/math.sqrt(x)>>>multifb(25)0.2>>>multifb(-5)# falls back to callable25>>>multifb(0)# falls back to valueinfBidictIf you already heard about (and used) the greatbidictpackage, you can simply skip this one as this is merely a minimal bidict for those in a hurry :).TheBidirectionaldictionary is an invertible one-to-one mapping (a bijection). Obviously both sides should be valid dictionary keys. Uniqueness in either side is preserved by removing previous confilcting pairs, without any warnings.fromizyimportBidictbd=Bidict({'a':1,'b':2,'c':5})bd=Bidict([('a',1),('b',2),('c',5)])bd=Bidict(a=1,b=2,c=5)# nothing special, just like dict()>>>bd# all above are quivalent toBidict({'a':1,'b':2,'c':5})This is how you would access the mapping, in the direct way or reversed:>>>bd.r# or bd.reverse{1:'a',2:'b',5:'c'}>>>bd['c']5>>>bd.r[5]'c'It is recommended to name the two directions of yourBidictto make your code more readable.bd=Bidict('char','id')# or Bidict('char-id')bd.update({'a':1,'b':2,'c':5})>>>bdBidict({'a':1,'b':2,'c':5})bd=Bidict(char=['a','b','c'],id=[1,2,5])>>>bdBidict({'a':1,'b':2,'c':5})>>>bd.id['c']5>>>bd.char[5]'c'>>>bd.char2id['c']# more expressive access5>>>bd.id2char[5]'c'On evey update, theBidictmakes sure that all values in both directions remain unique. This is done by removing every existing pair which has conflict with the new one (overwritig).bd.char[2]='c'# two conflicting pairs ('b', 2), ('c', 5) are removed>>>bdBidict({'a':1,'c':2})Not to mention that aBidictand its reversed views (bd.ror other accessors) are still adict.>>>isinstance(bd,dict)True>>>forchar,idinbd.r.items():print(char,id)1a2cRegexHere are some functions to help you create complex regular expressions. These functions are especially useful for somehard-to-rememberandhard-to-readpattern syntaxes, like lookahead, lookbehind patterns.p=make_regex(following('then '),# lookbehindcharset('a-z '),'+',followed_by(r' end')# lookahead)>>>p.pattern'(?<=then )[a-z ]+(?= end)'>>>p.search('if condition then do something end; etc')<re.Matchobject;span=(18,30),match='do something'>p=make_regex(any_of('ali','hossein','javad'),'\s+',grouped(any_of('goodarzi','sharafi'),name='family'))>>>p.pattern'(?:hossein|javad|ali)\s+(?P<family>(?:goodarzi|sharafi))'>>>match=p.search('his name is hossein sharafi.')>>>match<re.Matchobject;span=(12,27),match='hossein sharafi'>>>>match.group('family')'sharafi'HeadThis simple functionhead()gives you a brief insight about the structure and contents of a possible large and nested python data object. The key use case is when you load a large.jsonfile for the first time in a jupyter notebook.x=[(1,2,3,4),{1:range(3),2:range(2,40,2),5:6,7:8},range(1000),(7,8,9,10),*range(10)]>>>head(x)[(1,2,3),{1:(0,1,2),2:(2,4,6),5:6,7:8},(0,1,2)]>>>head(x,n=2,ellipsis=True)[(1,2,...),{1:(0,1,...),2:(2,4,...),...:...},...]>>>print(phead(x))[(1,2,3,...),{1:(0,1,2),2:(2,4,6,...),5:6,...:...},(0,1,2,...),...]>>>hprint(x,n=5,width=32,indent=4)[(1,2,3,4),{1:(0,1,2),2:(2,4,6,8,10,...),5:6,7:8},(0,1,2,3,4,...),(7,8,9,10),0,...]TableIf you have a small tabular data and you don't want to installnumpy,pandasjust to handle it, here is the thing for you. TheTableis a simple pure-python container for tabular data:# create table using dict of lists (columns)data={'a':[3,5,21638540,'very-loong-value'],'b':[16,8,0,1],'c':[0,5,6,None],}table=Table(data)>>>print(table.draw())abc============================31605852163854006very-loong-va..1None# ... or a list of dicts (rows)data=[{'a':3,'b':16,'c':0},{'a':5,'b':8,'c':5},{'a':21638540,'b':0,'c':6},{'b':1,'a':'very-loong-value'},]table=Table(data)>>>print(table.draw())abc============================31605852163854006very-loong-va..1NoneYou can easily access columns, rows, or a slice of the table:>>>table['b']# access table columns (by str key)[16,8,0,1]>>>table[2]# access table rows (by int key){'a':21638540,'b':0,'c':6}>>>table[1:3]# or get a slice of tableabc===================5852163854006Concatenate tables:>>>table[:2]+table[2:]abc============================31605852163854006very-loong-va..1NoneAnd manipulate table data:# add/change a column>>>table['d']=[0]*len(table)>>>tableabcd================================31600585021638540060very-loong-va..1None0# manipulate table rows>>>table[1]={'a':5,'b':55,'c':555}>>>table.append({'b':66,'c':666})>>>tableabcd===================================31600555555None21638540060very-loong-va..1None0None66666Nonecsvread/write is alse there:>>>table.to_csv('output.tsv')>>>Table.from_csv('output.tsv')abcd===================================31600555555None21638540060very-loong-va..1None0None66666NonePathYet anotherpathoperations toolkit along withpathlib,path.py. This one is based on the standardpathliband shares most of advantages ofpath.py. What makes it different is the convenientmethod-chainingapi, and its file read/write methods.d=Path('path/to').mkdir()p=(d/'file').with_suffix('.txt').touch()>>>p.copy(d/'copy.txt').name'copy.txt'>>>len(d.glob('*.txt'))2>>>d.parent.rmdir(recursive=True).exists()FalseFilePathis aPathwhich handles some file read/write operations which are not relevant to a Path.f=FilePath('path/to','file.txt')>>>f.mkdir().write_text('hello world!').read_text()'hello world!'>>>f.print({'x':4,'y':5}).aprint({'x':6,'y':7}).get_size()36>>>print(f.read_text()){'x':4,'y':5}{'x':6,'y':7}>>>f.pickle({'x':4,'y':5}).pickle({'x':6,'y':7},append=True).unpickle()[{'x':4,'y':5},{'x':6,'y':7}]Read/write operations forjsonandcsv(usingizy.Table) are also available.