Image courtesy of Anna Quaglia (Photographer)
Paprika is a python library that reduces boilerplate. It is heavily inspired by Project Lombok.
- Installation
- Usage
- Features & Examples
- Contributing
- Authors
- License
paprika is available on PyPi.
$ pip install paprika
paprika
is 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:
from paprika import *
Alternatively, you can opt to use the longhand notation (i.e. @paprika.data
)
by importing paprika
as follows:
import paprika
The @to_string
decorator automatically overrides __str__
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __str__(self):
return f"{self.__name__}@[name={self.name}, age={self.age}]"
@to_string
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
The @equals_and_hashcode
decorator automatically overrides __eq__
and __hash__
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __eq__(self, other):
return (self.__class__ == other.__class__
and
self.__dict__ == other.__dict__)
def __hash__(self):
return hash((self.name, self.age))
@equals_and_hashcode
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
The @data
decorator creates a dataclass by combining @to_string
and @equals_and_hashcode
and automatically creating a constructor!
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __str__(self):
return f"{self.__name__}@[name={self.name}, age={self.age}]"
def __eq__(self, other):
return (self.__class__ == other.__class__
and
self.__dict__ == other.__dict__)
def __hash__(self):
return hash((self.name, self.age))
@data
class Person:
name: str
age: int
paprika
exposes a NonNull
generic type that can be used in conjunction with
the @data
decorator to enforce that certain arguments passed to the
constructor are not null. The following snippet will raise a ValueError
:
@data
class Person:
name: NonNull[str]
age: int
p = Person(name=None, age=42) # ValueError ❌
The @singleton
decorator 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.
@singleton
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
p1 = Person(name="Rayan", age=19)
p2 = Person()
print(p1 == p2 and p1 is p2) # True âś…
@singleton
can be seamlessly combined with @data
!
@singleton
@data
class Person:
name: str
age: int
p1 = Person(name="Rayan", age=19)
p2 = Person()
print(p1 == p2 and p1 is p2) # True âś…
When combining @singleton
with @data
, @singleton
should come
before @data
. Combining them the other way around will work in most cases but
is not thoroughly tested and relies on assumptions that might not hold.
The @threaded
decorator will run the decorated function in a thread by
submitting it to a ThreadPoolExecutor
. When the decorated function is called,
it will immediately return a Future
object. The result can be extracted by
calling .result()
on that Future
@threaded
def waste_time(sleep_time):
thread_name = threading.current_thread().name
time.sleep(sleep_time)
print(f"{thread_name} woke up after {sleep_time}s!")
return 42
t1 = waste_time(5)
t2 = waste_time(2)
print(t1) # <Future at 0x104130a90 state=running>
print(t1.result()) # 42
ThreadPoolExecutor-0_1 woke up after 2s!
ThreadPoolExecutor-0_0 woke up after 5s!
The @repeat
decorator will run the decorated function consecutively, as many
times as specified.
@repeat(n=5)
def hello_world():
print("Hello world!")
hello_world()
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
The @pickled
decorator 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 optional protocol
argument (e.g. @pickled(protocol=5)
) specifiying the pickle protocol.
class Person:
def __init__(self, name: str):
self.name = name
def __dump__(self, file_path):
with open(file_path, "wb") as f:
pickle_dump(self, f, protocol=5)
@staticmethod
def __load__(file_path):
with open(file_path, "rb") as f:
return pickle.load(f)
@data
@pickled(protocol=5)
class Person:
name: str
The @timeit
decorator times the total execution time of the decorated
function. It uses a timer::perf_timer
by default but that can be replaced by
any object of type Callable[None, int]
.
def time_waster1():
time.sleep(2)
def time_waster2():
time.sleep(5)
@timeit
def test_timeit():
time_waster1()
time_waster2()
test_timeit executed in 7.002189894999999 seconds
Here's how you can replace the default timer:
@timeit(timer: lambda: 0) # Or something actually useful like time.time()
def test_timeit():
time_waster1()
time_waster2()
test_timeit executed in 0 seconds
The @access_counter
displays 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_counter
def test_access_counter(list, dict, person, tuple):
for i in range(500):
list[0] = dict["key"]
dict["key"] = person.age
person.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 |
+------------+----------+-----------+
The @hotspots
automatically runs cProfiler
on the decorated function and
display the top_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 higher n_runs
(default = 1) parameter.
def time_waster1():
time.sleep(2)
def time_waster2():
time.sleep(5)
@hotspots(top_n=5, n_runs=2) # You can also do just @hotspots
def test_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}
The @profile
decorator is simply syntatic sugar that allows to perform both
hotspot analysis and data access analysis. Under the hood, it simply
uses @access_counter
followed by @hotspots
.
The @catch
decorator can be used to wrap a function inside a try/catch
block. @catch
expects to receive in the exceptions
argument at least one
exception that we want to catch.
If no exception is provided, @catch
will by default catch all exceptions (
excluding SystemExit
, KeyboardInterrupt
and GeneratorExit
since they do not subclass the generic Exception
class).
@catch
can take a custom exception handler as a parameter. If no handler is
supplied, a stack trace is logged to stderr
and the program will continue
executing.
@catch(exception=ValueError)
def test_catch1():
raise ValueError
@catch(exception=[EOFError, KeyError])
def test_catch2():
raise ValueError
test_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
ValueError
If provided, a custom exception handler must be of
type Callable[Exception, Generic[T]]
. In other words, its signature must take
one parameter of type Exception.
@catch(exception=ValueError,
handler=lambda x: print(f"Ohno, a {repr(x)} was raised!"))
def test_custom_handler():
raise ValueError
test_custom_handler()
Ohno, a ValueError() was raised!
The @silent_catch
decorator is very similar to the @catch
decorator in its
usage. It takes one or more exceptions but then simply catches them silently.
@silent_catch(exception=[ValueError, TypeError])
def test_silent_catch():
raise TypeError
test_silent_catch()
print("Still alive!")
Still alive!
Encountered a bug? Have an idea for a new feature? This project is open to all
sorts of contribution! Feel free to head to the Issues
tab and describe your
request!
This project requires poetry.
- Initialize a virtual environment:
python -m venv .env
- Enter your virtual environment.
- Install poetry:
pip install poetry
. - Install dependencies:
poetry install
. - Initialize pre-commit:
pre-commit install
.
See also the list of contributors who participated in this project.
This project is licensed under the MIT License - see the LICENSE file for details