Package: paprika
Image courtesy of Anna Quaglia (Photographer)PaprikaPaprika is a python library that reduces boilerplate. It is heavily inspired by
Project Lombok.Table of ContentsInstallationUsageFeatures & ExamplesObject-oriented decorators@to_string@equals_and_hashcode@dataOn@dataand NonNull@singletonImportant note on combining@dataand@singletonGeneral utility decorators@threaded@repeat@pickledBenchmark decorators@timeit@access_counter@hotspots@profileError-handling decorators@catch@silent_catchContributingAuthorsLicenseInstallationpaprika is available on PyPi.$pipinstallpaprikaUsagepaprikais a decorator-only library and all decorators are exposed at the
top-level of the module. If you want to use shorthand notation (i.e.@data),
you can import all decorators as follows:frompaprikaimport*Alternatively, you can opt to use the longhand notation (i.e.@paprika.data)
by importingpaprikaas follows:importpaprikaFeatures and ExamplesObject-oriented decorators@to_stringThe@to_stringdecorator automatically overrides__str__PythonclassPerson:def__init__(self,name:str,age:int):self.name=nameself.age=agedef__str__(self):returnf"{self.__name__}@[name={self.name}, age={self.age}]"Python with paprika@to_stringclassPerson:def__init__(self,name:str,age:int):self.name=nameself.age=age@equals_and_hashcodeThe@equals_and_hashcodedecorator automatically overrides__eq__and__hash__PythonclassPerson:def__init__(self,name:str,age:int):self.name=nameself.age=agedef__eq__(self,other):return(self.__class__==other.__class__andself.__dict__==other.__dict__)def__hash__(self):returnhash((self.name,self.age))Python with paprika@equals_and_hashcodeclassPerson:def__init__(self,name:str,age:int):self.name=nameself.age=age@dataThe@datadecorator creates a dataclass by combining@to_stringand@equals_and_hashcodeand automatically creating a constructor!PythonclassPerson:def__init__(self,name:str,age:int):self.name=nameself.age=agedef__str__(self):returnf"{self.__name__}@[name={self.name}, age={self.age}]"def__eq__(self,other):return(self.__class__==other.__class__andself.__dict__==other.__dict__)def__hash__(self):returnhash((self.name,self.age))Python with paprika@dataclassPerson:name:strage:intOn @data and NonNullpaprikaexposes aNonNullgeneric type that can be used in conjunction with
the@datadecorator to enforce that certain arguments passed to the
constructor are not null. The following snippet will raise aValueError:@dataclassPerson:name:NonNull[str]age:intp=Person(name=None,age=42)# ValueError ❌@singletonThe@singletondecorator can be used to enforce that a class only gets
instantiated once within the lifetime of a program. Any subsequent instantiation
will return the original instance.@singletonclassPerson:def__init__(self,name:str,age:int):self.name=nameself.age=agep1=Person(name="Rayan",age=19)p2=Person()print(p1==p2andp1isp2)# True ✅@singletoncan be seamlessly combined with@data!@singleton@dataclassPerson:name:strage:intp1=Person(name="Rayan",age=19)p2=Person()print(p1==p2andp1isp2)# True ✅Important note on combining @data and @singletonWhen combining@singletonwith@data,@singletonshould come
before@data. Combining them the other way around will work in most cases but
is not thoroughly tested and relies on assumptions thatmightnot hold.General utility decorators@threadedThe@threadeddecorator will run the decorated function in a thread by
submitting it to aThreadPoolExecutor. When the decorated function is called,
it will immediately return aFutureobject. The result can be extracted by
calling.result()on thatFuture@threadeddefwaste_time(sleep_time):thread_name=threading.current_thread().nametime.sleep(sleep_time)print(f"{thread_name}woke up after{sleep_time}s!")return42t1=waste_time(5)t2=waste_time(2)print(t1)# <Future at 0x104130a90 state=running>print(t1.result())# 42ThreadPoolExecutor-0_1 woke up after 2s!
ThreadPoolExecutor-0_0 woke up after 5s!@repeatThe@repeatdecorator will run the decorated function consecutively, as many
times as specified.@repeat(n=5)defhello_world():print("Hello world!")hello_world()Hello world!
Hello world!
Hello world!
Hello world!
Hello world!@pickledThe@pickleddecorator adds__dump__and__load__to a class for pickling convenience.__dump__and__load__take in the target and source pickle file paths respectively.This decorator takes in an optionalprotocolargument (e.g.@pickled(protocol=5)) specifiying thepickle protocol.PythonclassPerson:def__init__(self,name:str):self.name=namedef__dump__(self,file_path):withopen(file_path,"wb")asf:pickle_dump(self,f,protocol=5)@staticmethoddef__load__(file_path):withopen(file_path,"rb")asf:returnpickle.load(f)Python with paprika@data@pickled(protocol=5)classPerson:name:strBenchmark decoratorstimeitThe@timeitdecorator times the total execution time of the decorated
function. It uses atimer::perf_timerby default but that can be replaced by
any object of typeCallable[None, int].deftime_waster1():time.sleep(2)deftime_waster2():time.sleep(5)@timeitdeftest_timeit():time_waster1()time_waster2()test_timeitexecutedin7.002189894999999secondsHere's how you can replace the default timer:@timeit(timer:lambda:0)# Or something actually useful like time.time()deftest_timeit():time_waster1()time_waster2()test_timeitexecutedin0seconds@access_counterThe@access_counterdisplays a summary of how many times each of the
structures that are passed to the decorated function are accessed
(number of reads and number of writes).@access_counterdeftest_access_counter(list,dict,person,tuple):foriinrange(500):list[0]=dict["key"]dict["key"]=person.ageperson.age=tuple[0]test_access_counter([1,2,3,4,5],{"key":0},Person(name="Rayan",age=19),(0,0))data access summary for function: test
+------------+----------+-----------+
| Arg Name   |   nReads |   nWrites |
+============+==========+===========+
| list       |        0 |       500 |
+------------+----------+-----------+
| dict       |      500 |       500 |
+------------+----------+-----------+
| person     |      500 |       500 |
+------------+----------+-----------+
| tuple      |      500 |         0 |
+------------+----------+-----------+@hotspotsThe@hotspotsautomatically runscProfileron the decorated function and
display thetop_n(default = 10) most expensive function calls sorted by
cumulative time taken (this metric will be customisable in the future). The
sample error can be reduced by using a highern_runs(default = 1) parameter.deftime_waster1():time.sleep(2)deftime_waster2():time.sleep(5)@hotspots(top_n=5,n_runs=2)# You can also do just @hotspotsdeftest_hotspots():time_waster1()time_waster2()test_hotspots()11 function calls in 14.007 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        2    0.000    0.000   14.007    7.004 main.py:27(test_hot)
        4   14.007    3.502   14.007    3.502 {built-in method time.sleep}
        2    0.000    0.000   10.004    5.002 main.py:23(time_waster2)
        2    0.000    0.000    4.003    2.002 main.py:19(time_waster1)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}@profileThe@profiledecorator is simply syntatic sugar that allows to perform both
hotspot analysis and data access analysis. Under the hood, it simply
uses@access_counterfollowed by@hotspots.Error-handling decorators@catchThe@catchdecorator can be used to wrap a function inside atry/catchblock.@catchexpects to receive in theexceptionsargument at least one
exception that we want to catch.If no exception is provided,@catchwill by default catchallexceptions (
excludingSystemExit,KeyboardInterruptandGeneratorExitsince they do not subclass the genericExceptionclass).@catchcan take a custom exception handler as a parameter. If no handler is
supplied, a stack trace is logged tostderrand the program will continue
executing.@catch(exception=ValueError)deftest_catch1():raiseValueError@catch(exception=[EOFError,KeyError])deftest_catch2():raiseValueErrortest_catch1()print("Still alive!")# This should get printed since we're catching the ValueError.test_catch2()print("Still alive?")# This will not get printed since we're not catching ValueError in this case.Traceback (most recent call last):
  File "/Users/rayan/Desktop/paprika/paprika/__init__.py", line 292, in wrapper_catch
    return func(*args, **kwargs)
  File "/Users/rayan/Desktop/paprika/main.py", line 29, in test_exception1
    raise ValueError
ValueError

Still alive!

Traceback (most recent call last):
  File "/Users/rayan/Desktop/paprika/main.py", line 40, in <module>
    test_exception2()
  File "/Users/rayan/Desktop/paprika/paprika/__init__.py", line 292, in wrapper_catch
    return func(*args, **kwargs)
  File "/Users/rayan/Desktop/paprika/main.py", line 37, in test_exception2
    raise ValueError
ValueErrorUsing a custom exception handlerIf provided, a custom exception handler must be of
typeCallable[Exception, Generic[T]]. In other words, its signature must take
one parameter of type Exception.@catch(exception=ValueError,handler=lambdax:print(f"Ohno, a{repr(x)}was raised!"))deftest_custom_handler():raiseValueErrortest_custom_handler()Ohno, a ValueError() was raised!@silent_catchThe@silent_catchdecorator is very similar to the@catchdecorator in its
usage. It takes one or more exceptions but then simply catches them silently.@silent_catch(exception=[ValueError,TypeError])deftest_silent_catch():raiseTypeErrortest_silent_catch()print("Still alive!")Still alive!ContributingIssuesEncountered a bug? Have an idea for a new feature? This project is open to all
sorts of contribution! Feel free to head to theIssuestab and describe your
request!Development SetupThis project requirespoetry.Recommended StepsInitialize a virtual environment:python -m venv .envEnter your virtual environment.Install poetry:pip install poetry.Install dependencies:poetry install.Initialize pre-commit:pre-commit install.AuthorsRayan Hatout-GitHub|Twitter|LinkedInSee also the list ofcontributorswho
participated in this project.LicenseThis project is licensed under the MIT License - see
theLICENSEfile for details